Skip to content

Commit df6f60c

Browse files
shai-almogclaude
andauthored
perf(parparvm-tests): diagonal JDK matrix, cache JavaAPI, parallel forks (#5198)
The vm/tests suite ran ~60 min serially. Three changes cut that without losing test coverage: 1. Diagonal compiler matrix. BytecodeInstructionIntegrationTest#provide- CompilerConfigs (the shared @MethodSource for RuntimeSemantics, Target, Bytecode, CleanTarget, Smoke, NativeAudit, FileClass, ...) built the full triangular (compiler-JDK x target-bytecode) cross-product: 15 combos, so every parameterized method ran 15x. The translator consumes bytecode, which is governed by the *target* level, so compiling a given target with five different JDKs mostly re-tested identical bytecode shapes. New CompilerHelper.getDiagonalCompilers() keeps each target exercised by its matching JDK (8->8, 11->11, 17->17, 21->21, 25->25) = 5 combos. The biggest class (JavascriptRuntimeSemanticsTest, ~24 min) scales directly with this. 2. Cache the compiled JavaAPI. compileJavaAPI() re-ran a ~259-source javac on every parameterized invocation (hundreds of times); its output depends only on (jdkVersion, targetVersion). It is now compiled once per combo and copied into each caller's dir. Coverage-neutral. 3. Parallel surefire forks. forkCount=1C, reuseForks=true. Parallelism is process-level by necessity: Parser holds static mutable translator state, so in-JVM thread parallelism would corrupt it; separate forks each run classes sequentially. Tests use unique temp dirs and the only shared path (target/benchmark-dependencies) is populated by maven before the test phase and only read, so forks do not collide. jacoco now writes one exec file per fork (${surefire.forkNumber}) and merges them before the report so the ByteCodeTranslator quality/coverage report is unchanged. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 288c8b5 commit df6f60c

3 files changed

Lines changed: 94 additions & 7 deletions

File tree

vm/tests/pom.xml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@
106106
<version>3.2.5</version>
107107
<configuration>
108108
<useModulePath>false</useModulePath>
109+
<!-- Run test classes across parallel JVM forks. Parallelism MUST be
110+
process-level: the translator (Parser) keeps static mutable state,
111+
so in-JVM thread parallelism would corrupt it. Each test writes to
112+
unique createTempDirectory paths and the only shared path
113+
(target/benchmark-dependencies) is populated by maven-dependency-plugin
114+
before the test phase and only read here, so forks never collide.
115+
1C = one fork per available CPU core. -->
116+
<forkCount>1C</forkCount>
117+
<reuseForks>true</reuseForks>
109118
</configuration>
110119
</plugin>
111120
<plugin>
@@ -117,6 +126,34 @@
117126
<goals>
118127
<goal>prepare-agent</goal>
119128
</goals>
129+
<configuration>
130+
<!-- One exec file per surefire fork. surefire substitutes
131+
${surefire.forkNumber} in the agent argLine per forked JVM;
132+
without this, parallel forks would clobber a single
133+
jacoco.exec and the coverage report would be corrupt/empty. -->
134+
<destFile>${project.build.directory}/jacoco-fork-${surefire.forkNumber}.exec</destFile>
135+
</configuration>
136+
</execution>
137+
<execution>
138+
<!-- Merge the per-fork exec files back into one before the report.
139+
Declared before the report execution so it runs first in the
140+
test phase. -->
141+
<id>merge-forks</id>
142+
<phase>test</phase>
143+
<goals>
144+
<goal>merge</goal>
145+
</goals>
146+
<configuration>
147+
<fileSets>
148+
<fileSet>
149+
<directory>${project.build.directory}</directory>
150+
<includes>
151+
<include>jacoco-fork-*.exec</include>
152+
</includes>
153+
</fileSet>
154+
</fileSets>
155+
<destFile>${project.build.directory}/jacoco.exec</destFile>
156+
</configuration>
120157
</execution>
121158
<execution>
122159
<id>report</id>

vm/tests/src/test/java/com/codename1/tools/translator/BytecodeInstructionIntegrationTest.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,13 @@
3939

4040
class BytecodeInstructionIntegrationTest {
4141

42+
// Shared @MethodSource for the integration suites (RuntimeSemantics, Target,
43+
// Bytecode, CleanTarget, Smoke, NativeAudit, FileClass, ...). Uses the
44+
// diagonal compiler set -- each bytecode target compiled by its matching JDK
45+
// (8->8 .. 25->25) -- rather than the full (compiler x target) cross-product,
46+
// which re-tested the same bytecode shapes. See CompilerHelper.getDiagonalCompilers.
4247
static Stream<CompilerHelper.CompilerConfig> provideCompilerConfigs() {
43-
List<CompilerHelper.CompilerConfig> configs = new ArrayList<>();
44-
configs.addAll(CompilerHelper.getAvailableCompilers("1.8"));
45-
configs.addAll(CompilerHelper.getAvailableCompilers("11"));
46-
configs.addAll(CompilerHelper.getAvailableCompilers("17"));
47-
configs.addAll(CompilerHelper.getAvailableCompilers("21"));
48-
configs.addAll(CompilerHelper.getAvailableCompilers("25"));
49-
return configs.stream();
48+
return CompilerHelper.getDiagonalCompilers().stream();
5049
}
5150

5251
@ParameterizedTest

vm/tests/src/test/java/com/codename1/tools/translator/CompilerHelper.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,29 @@ public static List<CompilerConfig> getAvailableCompilers(String targetVersion) {
190190
return compilers;
191191
}
192192

193+
// Diagonal compiler set: each supported bytecode target compiled only by the
194+
// JDK whose major version matches that target (8->8, 11->11, 17->17, 21->21,
195+
// 25->25). The translator consumes bytecode, which is governed by the target
196+
// level, so the full (compiler x target) cross-product mostly re-tests the
197+
// same bytecode shapes. Restricting to the diagonal keeps every target level
198+
// exercised while cutting the per-method parameter count from up to 15 to 5.
199+
// A target whose matching JDK is not installed locally is simply skipped
200+
// (CI installs all five). Pairs use {target, jdkMajor}; "1.8" maps to JDK 8.
201+
public static List<CompilerConfig> getDiagonalCompilers() {
202+
String[][] pairs = { {"1.8", "8"}, {"11", "11"}, {"17", "17"}, {"21", "21"}, {"25", "25"} };
203+
List<CompilerConfig> out = new ArrayList<>();
204+
for (String[] pair : pairs) {
205+
int wantMajor = parseJavaMajor(pair[1]);
206+
for (CompilerConfig config : getAvailableCompilers(pair[0])) {
207+
if (getJdkMajor(config) == wantMajor) {
208+
out.add(config);
209+
break;
210+
}
211+
}
212+
}
213+
return out;
214+
}
215+
193216
private static boolean canCompile(String compilerVersion, String targetVersion) {
194217
int compilerMajor = parseJavaMajor(compilerVersion);
195218
int targetMajor = parseJavaMajor(targetVersion);
@@ -385,7 +408,35 @@ public static boolean compileAndRun(String code, String expectedOutput) throws E
385408
}
386409
}
387410

411+
// The compiled JavaAPI is identical for a given (jdkVersion, targetVersion),
412+
// yet it was previously re-compiled (a ~259-source javac run) on every
413+
// parameterized test invocation -- hundreds of times across the suite. Cache
414+
// the compiled output per combo and copy it into each caller's outputDir
415+
// instead. This removes the dominant repeated cost with zero change to what
416+
// is tested. Within a surefire fork tests run sequentially, so the only
417+
// contention is the compile-once guard below.
418+
private static final java.util.Map<String, Path> JAVA_API_CACHE =
419+
new java.util.concurrent.ConcurrentHashMap<>();
420+
388421
public static void compileJavaAPI(Path outputDir, CompilerConfig config) throws IOException, InterruptedException {
422+
Files.createDirectories(outputDir);
423+
copyDirectory(getCachedJavaApi(config), outputDir);
424+
}
425+
426+
private static synchronized Path getCachedJavaApi(CompilerConfig config) throws IOException, InterruptedException {
427+
String key = config.jdkVersion + "->" + config.targetVersion;
428+
Path cached = JAVA_API_CACHE.get(key);
429+
if (cached != null && Files.isDirectory(cached)) {
430+
return cached;
431+
}
432+
Path cacheDir = Files.createTempDirectory(
433+
"java-api-cache-" + config.jdkVersion + "-" + config.targetVersion.replaceAll("[^A-Za-z0-9]", "_") + "-");
434+
compileJavaApiInto(cacheDir, config);
435+
JAVA_API_CACHE.put(key, cacheDir);
436+
return cacheDir;
437+
}
438+
439+
private static void compileJavaApiInto(Path outputDir, CompilerConfig config) throws IOException, InterruptedException {
389440
Files.createDirectories(outputDir);
390441
Path javaApiRoot = Paths.get("..", "JavaAPI", "src").normalize().toAbsolutePath();
391442
List<String> sources = new ArrayList<>();

0 commit comments

Comments
 (0)