Skip to content

Commit 9f35baa

Browse files
Add Heavy Load Benchmark for ByteCodeTranslator
- Added `HeavyLoadBenchmarkTest` in `vm/tests` which runs ByteCodeTranslator against `JavaAPI`. - Implemented `SimpleProfiler` to capture hotspots during translation. - Updated `vm/tests/pom.xml` to include `JavaAPI` dependency and exclude benchmark from default execution. - Updated `.github/workflows/parparvm-tests.yml` to run the benchmark. - Updated `.github/scripts/generate-quality-report.py` to include benchmark results in the PR comment.
1 parent e257e6b commit 9f35baa

File tree

4 files changed

+277
-0
lines changed

4 files changed

+277
-0
lines changed

.github/scripts/generate-quality-report.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,14 @@ def parse_checkstyle() -> Optional[AnalysisReport]:
441441
return AnalysisReport(totals=severities, findings=findings)
442442

443443

444+
def parse_benchmark() -> Optional[str]:
445+
for target_dir in TARGET_DIRS:
446+
candidate = target_dir / "benchmark-results.md"
447+
if candidate.exists():
448+
return candidate.read_text(encoding="utf-8")
449+
return None
450+
451+
444452
def format_tests(totals: Optional[Dict[str, int]]) -> str:
445453
if not totals:
446454
return "- ⚠️ No test results were found."
@@ -650,6 +658,7 @@ def build_report(
650658
spotbugs, spotbugs_generated, spotbugs_error = parse_spotbugs()
651659
pmd = parse_pmd()
652660
checkstyle = parse_checkstyle()
661+
benchmark_report = parse_benchmark()
653662

654663
write_analysis_html("spotbugs", "SpotBugs Findings", spotbugs)
655664
write_analysis_html("pmd", "PMD Findings", pmd)
@@ -672,6 +681,9 @@ def build_report(
672681
"### Test & Coverage",
673682
format_tests(tests),
674683
]
684+
if benchmark_report:
685+
lines.append("")
686+
lines.append(benchmark_report)
675687
lines.extend(
676688
format_coverage(
677689
coverage,

.github/workflows/parparvm-tests.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ jobs:
8686
JDK_21_HOME: ${{ env.JDK_21_HOME }}
8787
JDK_25_HOME: ${{ env.JDK_25_HOME }}
8888

89+
- name: Run ParparVM Benchmark
90+
working-directory: vm
91+
run: mvn -B test -pl tests -Dgroups=benchmark -P !skip-benchmark -Djacoco.skip=true
92+
env:
93+
JDK_8_HOME: ${{ env.JDK_8_HOME }}
94+
JDK_11_HOME: ${{ env.JDK_11_HOME }}
95+
JDK_17_HOME: ${{ env.JDK_17_HOME }}
96+
JDK_21_HOME: ${{ env.JDK_21_HOME }}
97+
JDK_25_HOME: ${{ env.JDK_25_HOME }}
98+
8999
- name: Publish ByteCodeTranslator quality previews
90100
if: ${{ always() && github.server_url == 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
91101
id: publish-bytecode-quality-previews

vm/tests/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
<artifactId>junit-jupiter</artifactId>
3232
<scope>test</scope>
3333
</dependency>
34+
<dependency>
35+
<groupId>com.codename1.parparvm</groupId>
36+
<artifactId>JavaAPI</artifactId>
37+
<version>1.0-SNAPSHOT</version>
38+
<scope>test</scope>
39+
</dependency>
3440
</dependencies>
3541

3642
<build>
@@ -75,4 +81,24 @@
7581
</plugin>
7682
</plugins>
7783
</build>
84+
85+
<profiles>
86+
<profile>
87+
<id>skip-benchmark</id>
88+
<activation>
89+
<activeByDefault>true</activeByDefault>
90+
</activation>
91+
<build>
92+
<plugins>
93+
<plugin>
94+
<groupId>org.apache.maven.plugins</groupId>
95+
<artifactId>maven-surefire-plugin</artifactId>
96+
<configuration>
97+
<excludedGroups>benchmark</excludedGroups>
98+
</configuration>
99+
</plugin>
100+
</plugins>
101+
</build>
102+
</profile>
103+
</profiles>
78104
</project>
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package com.codename1.tools.translator;
2+
3+
import org.junit.jupiter.api.Tag;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.io.TempDir;
7+
8+
import javax.tools.JavaCompiler;
9+
import javax.tools.ToolProvider;
10+
import java.io.File;
11+
import java.io.IOException;
12+
import java.lang.reflect.InvocationTargetException;
13+
import java.lang.reflect.Method;
14+
import java.net.URL;
15+
import java.net.URLClassLoader;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.nio.file.Paths;
19+
import java.util.*;
20+
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.stream.Collectors;
22+
23+
@Tag("benchmark")
24+
public class HeavyLoadBenchmarkTest {
25+
26+
@TempDir
27+
Path tempDir;
28+
29+
@Test
30+
public void benchmarkJavaAPITranslation() throws Exception {
31+
// Locate JavaAPI.jar
32+
Path javaApiJar = Paths.get("..", "JavaAPI", "dist", "JavaAPI.jar").normalize().toAbsolutePath();
33+
if (!Files.exists(javaApiJar)) {
34+
javaApiJar = Paths.get("vm", "JavaAPI", "dist", "JavaAPI.jar").normalize().toAbsolutePath();
35+
}
36+
37+
// Ensure JavaAPI jar exists
38+
Assertions.assertTrue(Files.exists(javaApiJar), "JavaAPI.jar not found at " + javaApiJar + ". Make sure to build JavaAPI first.");
39+
40+
Path outputDir = tempDir.resolve("benchmark-output");
41+
Path srcDir = tempDir.resolve("benchmark-src");
42+
Path classesDir = tempDir.resolve("benchmark-classes");
43+
44+
Files.createDirectories(outputDir);
45+
Files.createDirectories(srcDir);
46+
Files.createDirectories(classesDir);
47+
48+
// Create a heavy main class that references various parts of the API to force traversal
49+
Path pkgDir = srcDir.resolve("com").resolve("benchmark");
50+
Files.createDirectories(pkgDir);
51+
Path javaFile = pkgDir.resolve("BenchmarkMain.java");
52+
String source = "package com.benchmark;\n" +
53+
"public class BenchmarkMain {\n" +
54+
" public static void main(String[] args) {\n" +
55+
" java.util.ArrayList l = new java.util.ArrayList();\n" +
56+
" l.add(\"Hello\");\n" +
57+
" java.util.HashMap m = new java.util.HashMap();\n" +
58+
" m.put(\"Key\", l);\n" +
59+
" System.out.println(m);\n" +
60+
" java.util.Vector v = new java.util.Vector();\n" +
61+
" v.addElement(new java.util.Date());\n" +
62+
" try {\n" +
63+
" Class.forName(\"java.util.TimeZone\");\n" +
64+
" } catch (Exception e) {}\n" +
65+
" }\n" +
66+
"}";
67+
Files.write(javaFile, source.getBytes());
68+
69+
// Compile the benchmark main
70+
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
71+
int result = compiler.run(null, null, null, "-cp", javaApiJar.toString(), "-d", classesDir.toString(), javaFile.toString());
72+
Assertions.assertEquals(0, result, "Compilation of BenchmarkMain failed");
73+
74+
// Unzip JavaAPI into classesDir to avoid Jar scanning issues and ensure heavy load
75+
unzip(javaApiJar, classesDir);
76+
77+
SimpleProfiler profiler = new SimpleProfiler(Thread.currentThread());
78+
profiler.start();
79+
80+
long startTime = System.currentTimeMillis();
81+
82+
try {
83+
// Classpath is just the classesDir now, containing App + JavaAPI
84+
String classpath = classesDir.toString();
85+
System.out.println("Testing classpath: " + classpath);
86+
runTranslator(classpath, outputDir);
87+
} finally {
88+
profiler.stopProfiling();
89+
}
90+
91+
long duration = System.currentTimeMillis() - startTime;
92+
System.out.println("Translation took: " + duration + "ms");
93+
94+
writeReport(duration, profiler.getHotspots(20));
95+
}
96+
97+
private void runTranslator(String classpath, Path outputDir) throws Exception {
98+
Path translatorResources = Paths.get("..", "ByteCodeTranslator", "src").normalize().toAbsolutePath();
99+
if (!Files.exists(translatorResources)) {
100+
translatorResources = Paths.get("vm", "ByteCodeTranslator", "src").normalize().toAbsolutePath();
101+
}
102+
103+
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
104+
URL[] systemUrls;
105+
if (systemLoader instanceof URLClassLoader) {
106+
systemUrls = ((URLClassLoader) systemLoader).getURLs();
107+
} else {
108+
String[] paths = System.getProperty("java.class.path").split(System.getProperty("path.separator"));
109+
systemUrls = new URL[paths.length];
110+
for (int i=0; i<paths.length; i++) {
111+
systemUrls[i] = Paths.get(paths[i]).toUri().toURL();
112+
}
113+
}
114+
115+
URL[] urls = Arrays.copyOf(systemUrls, systemUrls.length + 1);
116+
urls[systemUrls.length] = translatorResources.toUri().toURL();
117+
URLClassLoader loader = new URLClassLoader(urls, null);
118+
119+
ClassLoader originalLoader = Thread.currentThread().getContextClassLoader();
120+
Thread.currentThread().setContextClassLoader(loader);
121+
122+
try {
123+
Class<?> translatorClass = Class.forName("com.codename1.tools.translator.ByteCodeTranslator", true, loader);
124+
Method main = translatorClass.getMethod("main", String[].class);
125+
126+
String[] args = new String[]{
127+
"clean",
128+
classpath,
129+
outputDir.toString(),
130+
"BenchmarkApp",
131+
"com.benchmark",
132+
"Benchmark",
133+
"1.0",
134+
"ios",
135+
"none"
136+
};
137+
138+
main.invoke(null, (Object) args);
139+
140+
} catch (InvocationTargetException ite) {
141+
Throwable cause = ite.getCause() != null ? ite.getCause() : ite;
142+
throw (Exception) cause;
143+
} finally {
144+
Thread.currentThread().setContextClassLoader(originalLoader);
145+
try { loader.close(); } catch(IOException e){}
146+
}
147+
}
148+
149+
private void unzip(Path zipFile, Path outputDir) throws IOException {
150+
try (java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(Files.newInputStream(zipFile))) {
151+
java.util.zip.ZipEntry ze = zis.getNextEntry();
152+
while (ze != null) {
153+
Path newFile = outputDir.resolve(ze.getName());
154+
if (ze.isDirectory()) {
155+
Files.createDirectories(newFile);
156+
} else {
157+
Files.createDirectories(newFile.getParent());
158+
Files.copy(zis, newFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
159+
}
160+
ze = zis.getNextEntry();
161+
}
162+
}
163+
}
164+
165+
private void writeReport(long duration, List<String> hotspots) throws IOException {
166+
Path targetDir = Paths.get("target");
167+
if (!Files.exists(targetDir)) Files.createDirectories(targetDir);
168+
Path reportFile = targetDir.resolve("benchmark-results.md");
169+
170+
StringBuilder sb = new StringBuilder();
171+
sb.append("### Benchmark Results\n\n");
172+
sb.append("- **Execution Time:** ").append(duration).append(" ms\n");
173+
sb.append("- **Hotspots (Top 20 sampled methods):**\n");
174+
for (String s : hotspots) {
175+
sb.append(" - `").append(s).append("`\n");
176+
}
177+
178+
Files.write(reportFile, sb.toString().getBytes());
179+
System.out.println("Benchmark report written to " + reportFile.toAbsolutePath());
180+
}
181+
182+
static class SimpleProfiler extends Thread {
183+
private final Thread targetThread;
184+
private final AtomicBoolean running = new AtomicBoolean(true);
185+
private final Map<String, Integer> counts = new HashMap<>();
186+
private int totalSamples = 0;
187+
188+
public SimpleProfiler(Thread target) {
189+
this.targetThread = target;
190+
}
191+
192+
@Override
193+
public void run() {
194+
while (running.get()) {
195+
StackTraceElement[] stack = targetThread.getStackTrace();
196+
if (stack.length > 0) {
197+
// Simple sampling: just top of stack
198+
StackTraceElement top = stack[0];
199+
String signature = top.getClassName() + "." + top.getMethodName();
200+
counts.merge(signature, 1, Integer::sum);
201+
totalSamples++;
202+
}
203+
try {
204+
Thread.sleep(5); // 5ms sample rate
205+
} catch (InterruptedException e) {
206+
break;
207+
}
208+
}
209+
}
210+
211+
public void stopProfiling() {
212+
running.set(false);
213+
try {
214+
this.join();
215+
} catch (InterruptedException e) {
216+
Thread.currentThread().interrupt();
217+
}
218+
}
219+
220+
public List<String> getHotspots(int limit) {
221+
if (totalSamples == 0) return Collections.emptyList();
222+
return counts.entrySet().stream()
223+
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
224+
.limit(limit)
225+
.map(e -> String.format("%.2f%% %s (%d samples)", (e.getValue() * 100.0 / totalSamples), e.getKey(), e.getValue()))
226+
.collect(Collectors.toList());
227+
}
228+
}
229+
}

0 commit comments

Comments
 (0)