Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-java"
---

Improve Java generation performance by compiling customizations in memory, sharing parsed Java files across customization, partial update, and import ordering, tokenizing import headers for untouched files, using bounded parallel formatting, preserving schema Javadocs without parsing their contents as Java, and writing ARM debug code models only when debugging is enabled. Customizations continue to run before partial update. Formatter parallelism can be limited when generating multiple clients concurrently.
7 changes: 7 additions & 0 deletions .chronus/changes/http-client-java-jdt-java17-2026-09-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: dependencies
packages:
- "@typespec/http-client-java"
---

Update Eclipse JDT Core to 3.47.0, ECJ to 3.46.100, and the Eclipse platform dependencies to their latest Java 17-compatible releases.
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ words:
- WINDOWSVMIMAGE
- workerid
- xdist
- Xdoclint
- xiangyan
- xiaofei
- xlarge
Expand Down
15 changes: 15 additions & 0 deletions packages/http-client-java/generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The **Microsoft Java client generator** tool generates client libraries for acce
- [Prerequisites](#prerequisites)
- [Build](#build)
- [Test](#test)
- [Formatting Performance](#formatting-performance)

## Prerequisites

Expand All @@ -21,6 +22,20 @@ The **Microsoft Java client generator** tool generates client libraries for acce

1. `mvn clean test` (from packages/http-client-java/generator directory)

## Formatting Performance

Files that have not needed customization or partial merging use header-only tokenization for import ordering instead of a full JavaParser AST. Commented or unusual import headers fall back to JavaParser; already parsed files continue to reuse their AST. Customization still runs before partial update.

Unused-import removal and Eclipse formatting run with at most four workers, each with its own formatter. Automatic parallelism allows one worker per 32 files, capped by the available processors. Small batches run sequentially. Results, diagnostics, and file writes retain their input order.

Set `TYPESPEC_JAVA_FORMATTER_PARALLELISM=1` when using spec-level parallel generation, such as the `Generate.ps1` scripts at their default processor-count parallelism, to avoid multiplying the number of CPU workers. A positive integer overrides the automatic setting, still capped at four workers and the available processors. The JVM property `-Dcodegen.java.formatter.parallelism=<count>` takes precedence over the environment variable.

For example, in PowerShell before invoking a regeneration script:

```powershell
$env:TYPESPEC_JAVA_FORMATTER_PARALLELISM = "1"
```

## Debug

### Debugging Java Code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,19 @@
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.27.0</version>
<version>3.28.2</version>
</dependency>
<!-- 1.24.0 is the last version that supports Java 11 -->
<!-- 1.28.0 is the last version that supports Java 17 -->
<dependency>
<groupId>com.google.googlejavaformat</groupId>
<artifactId>google-java-format</artifactId>
<version>1.24.0</version>
<version>1.28.0</version>
</dependency>
<!-- 3.27.0 aligns with the version used when Spotless uses Eclipse 4.21 / 4.21.0. -->
<!-- In the future when this is upgraded it might slightly change code formatting. -->
<!-- JDT Core and ECJ are pinned to Java 17-compatible releases. -->
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>org.eclipse.jdt.core</artifactId>
<version>3.27.0</version>
<version>3.47.0</version>
<exclusions>
<exclusion>
<!-- org.eclipse.team.core is optional, exclude it. -->
Expand All @@ -103,12 +102,16 @@
</exclusion>
</exclusions>
</dependency>
<!-- Need to manually list all transitive dependencies of Eclipse JDT Core as it specified its dependencies with -->
<!-- ranges and newer versions require Java 17, and we want to stay compatible with Java 11 for now. -->
<dependency>
<groupId>org.eclipse.jdt</groupId>
<artifactId>ecj</artifactId>
<version>3.46.100</version>
</dependency>
<!-- Pin Eclipse platform dependencies to retain Java 17 runtime compatibility. -->
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.resources</artifactId>
<version>3.15.100</version>
<version>3.24.100</version>
<exclusions>
<exclusion>
<!-- org.eclipse.ant.core is optional, exclude it. -->
Expand All @@ -120,63 +123,63 @@
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.expressions</artifactId>
<version>3.8.0</version>
<version>3.9.700</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.runtime</artifactId>
<version>3.23.0</version>
<version>3.35.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.jobs</artifactId>
<version>3.12.0</version>
<version>3.15.900</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.equinox.preferences</artifactId>
<version>3.9.0</version>
<version>3.12.100</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.contenttype</artifactId>
<version>3.8.0</version>
<version>3.9.900</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.equinox.app</artifactId>
<version>1.6.0</version>
<version>1.7.600</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.filesystem</artifactId>
<version>1.9.100</version>
<version>1.11.500</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.equinox.registry</artifactId>
<version>3.11.0</version>
<version>3.12.600</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.equinox.common</artifactId>
<version>3.15.0</version>
<version>3.21.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<!-- cSpell:ignore osgi -->
<artifactId>org.eclipse.osgi</artifactId>
<version>3.17.0</version>
<version>3.24.300</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.text</artifactId>
<version>3.12.0</version>
<version>3.14.800</version>
</dependency>
<dependency>
<groupId>org.eclipse.platform</groupId>
<artifactId>org.eclipse.core.commands</artifactId>
<version>3.10.100</version>
<version>3.13.0</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package com.microsoft.typespec.http.client.generator.core.customization;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.util.function.Consumer;

Expand Down Expand Up @@ -34,9 +33,7 @@ public String getClassName() {
* @return This ClassCustomization with the abstract syntax tree changes applied.
*/
public ClassCustomization customizeAst(Consumer<CompilationUnit> astCustomization) {
CompilationUnit astToEdit = StaticJavaParser.parse(editor.getFileContent(fileName));
astCustomization.accept(astToEdit);
editor.replaceFile(fileName, astToEdit.toString());
editor.customizeAst(fileName, astCustomization);

return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@ public abstract class Customization {
* @return the map of files after customization
*/
public final Map<String, String> run(Map<String, String> files, Logger logger) {
Editor editor = new Editor(files);
return run(new Editor(files), logger);
}

/**
* Applies customization while retaining parsed files for partial update and formatting.
*
* @param editor the editor shared by the postprocessing stages
* @param logger the logger
* @return the customized file contents
*/
public final Map<String, String> run(Editor editor, Logger logger) {
customize(new LibraryCustomization(editor), logger);
return editor.getContents();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

package com.microsoft.typespec.http.client.generator.core.customization;

import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
Expand All @@ -16,18 +20,16 @@
public final class Editor {
private final Map<String, String> contents;
private final Map<String, List<String>> lines;
private final Map<String, ParsedFile> parsedFiles = new HashMap<>();

/**
* Creates an editor instance with the file contents and the root directory path.
*
* @param contents the map from file relative paths (starting with "src/main/java") and file contents
*/
public Editor(Map<String, String> contents) {
this.contents = new HashMap<>(contents);
this.contents = new LinkedHashMap<>(contents);
this.lines = new HashMap<>();
for (Map.Entry<String, String> entry : contents.entrySet()) {
lines.put(entry.getKey(), splitContentIntoLines(entry.getValue()));
}
}

/**
Expand Down Expand Up @@ -78,6 +80,81 @@ public Map<String, String> getContents() {
return contents;
}

/**
* Gets the shared AST for the current file content. Text replacements invalidate the cached parse.
* Commit AST changes with {@link #setCompilationUnit(String, CompilationUnit)} before reading the file as text.
*
* @param name the relative file path
* @return the parsed compilation unit
*/
public CompilationUnit getCompilationUnit(String name) {
String content = contents.get(name);
ParsedFile parsedFile = parsedFiles.get(name);
if (parsedFile == null || !parsedFile.content.equals(content)) {
parsedFile = new ParsedFile(content, StaticJavaParser.parse(content), false);
parsedFiles.put(name, parsedFile);
}
return parsedFile.compilationUnit;
}

/**
* Gets an existing AST without parsing a file that has not needed AST processing.
*
* @param name the relative file path
* @return the cached compilation unit, or null if the current text has not been parsed
*/
public CompilationUnit getCachedCompilationUnit(String name) {
ParsedFile parsedFile = parsedFiles.get(name);
return parsedFile != null && parsedFile.content.equals(contents.get(name)) ? parsedFile.compilationUnit : null;
}

/**
* Indicates whether AST edits have made the original source positions unsuitable for text replacements.
*
* @param name the relative file path
* @return whether the current AST has been edited
*/
public boolean isCompilationUnitModified(String name) {
ParsedFile parsedFile = parsedFiles.get(name);
return parsedFile != null && parsedFile.modified && parsedFile.content.equals(contents.get(name));
}

/**
* Releases a cached AST after its final use without discarding the file content.
*
* @param name the relative file path
*/
public void releaseCompilationUnit(String name) {
parsedFiles.remove(name);
}

/**
* Updates the file content while retaining the edited AST for subsequent processing.
*
* @param name the relative file path
* @param compilationUnit the edited compilation unit
*/
public void setCompilationUnit(String name, CompilationUnit compilationUnit) {
String content = compilationUnit.toString();
if (compilationUnit.getModule().isPresent()) {
content = compilationUnit.getOrphanComments().stream().map(Object::toString).collect(Collectors.joining())
+ "\n" + content;
}
replaceFile(name, content);
parsedFiles.put(name, new ParsedFile(content, compilationUnit, true));
}

void customizeAst(String name, Consumer<CompilationUnit> customization) {
CompilationUnit compilationUnit = getCompilationUnit(name);
try {
customization.accept(compilationUnit);
setCompilationUnit(name, compilationUnit);
} catch (RuntimeException | Error exception) {
parsedFiles.remove(name);
throw exception;
}
}

/**
* Adds a new file.
*
Expand All @@ -101,7 +178,8 @@ public void replaceFile(String name, String content) {
private void addOrReplaceFile(String name, String content, boolean isReplace) {
if (isReplace || !contents.containsKey(name)) {
contents.put(name, content);
lines.put(name, splitContentIntoLines(content));
lines.remove(name);
parsedFiles.remove(name);
}
}

Expand All @@ -113,6 +191,7 @@ private void addOrReplaceFile(String name, String content, boolean isReplace) {
public void removeFile(String name) {
contents.remove(name);
lines.remove(name);
parsedFiles.remove(name);
}

/**
Expand All @@ -132,7 +211,9 @@ public String getFileContent(String name) {
* @return the file content split into lines
*/
public List<String> getFileLines(String name) {
return lines.get(name);
return contents.containsKey(name)
? lines.computeIfAbsent(name, fileName -> splitContentIntoLines(contents.get(fileName)))
: null;
}

/**
Expand All @@ -143,7 +224,7 @@ public List<String> getFileLines(String name) {
* @return the file content in this line
*/
public String getFileLine(String name, int line) {
return lines.get(name).get(line);
return getFileLines(name).get(line);
}

private static List<String> splitContentIntoLines(String content) {
Expand All @@ -158,4 +239,16 @@ private static List<String> splitContentIntoLines(String content) {
return res;
}

private static final class ParsedFile {
private final String content;
private final CompilationUnit compilationUnit;
private final boolean modified;

private ParsedFile(String content, CompilationUnit compilationUnit, boolean modified) {
this.content = content;
this.compilationUnit = compilationUnit;
this.modified = modified;
}
}

}
Loading
Loading