Skip to content

Commit e46fb40

Browse files
Optimize recursive culling process in ByteCodeTranslator
Created a method usage index before starting the culling process in `Parser.java`. This avoids the O(N^2) complexity of `isMethodUsed` which previously scanned all methods to find usages. The index maps method signatures (name + descriptor) to a list of callers. `BytecodeMethod.java` was updated to include logic for populating this index. Added a stress test `StressTest.java` to verify performance and correctness on a generated codebase.
1 parent 3c3a5fe commit e46fb40

File tree

3 files changed

+197
-0
lines changed

3 files changed

+197
-0
lines changed

vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,5 +2068,29 @@ public SignatureSet nextSignature() {
20682068
return null;
20692069
}
20702070

2071+
public void populateMethodUsageIndex(Map<String, List<BytecodeMethod>> index) {
2072+
if(isEliminated()) return;
2073+
2074+
Set<String> uniqueCalls = new HashSet<String>();
2075+
2076+
for(Instruction ins : instructions) {
2077+
String sname = ins.getMethodName();
2078+
if(sname != null) {
2079+
String sig = ins.getSignature();
2080+
if (sig != null) {
2081+
String key = sname + sig;
2082+
if (!uniqueCalls.contains(key)) {
2083+
uniqueCalls.add(key);
2084+
List<BytecodeMethod> callers = index.get(key);
2085+
if (callers == null) {
2086+
callers = new ArrayList<BytecodeMethod>();
2087+
index.put(key, callers);
2088+
}
2089+
callers.add(this);
2090+
}
2091+
}
2092+
}
2093+
}
2094+
}
20712095

20722096
}

vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ public class Parser extends ClassVisitor {
5252
private String clsName;
5353
private static String[] nativeSources;
5454
private static List<ByteCodeClass> classes = new ArrayList<ByteCodeClass>();
55+
private static Map<String, List<BytecodeMethod>> usageIndex;
5556
private int lambdaCounter;
5657
public static void cleanup() {
5758
nativeSources = null;
5859
classes.clear();
5960
LabelInstruction.cleanup();
61+
usageIndex = null;
6062
}
6163
public static void parse(File sourceFile) throws Exception {
6264
if(ByteCodeTranslator.verbose) {
@@ -432,7 +434,9 @@ public static void writeOutput(File outputDirectory) throws Exception {
432434
if (BytecodeMethod.optimizerOn) {
433435
System.out.println("Optimizer On: Removing unused methods and classes...");
434436
Date now = new Date();
437+
buildMethodUsageIndex();
435438
neliminated += eliminateUnusedMethods();
439+
usageIndex = null;
436440
Date later = new Date();
437441
long dif = later.getTime()-now.getTime();
438442
System.out.println("unusued Method cull removed "+neliminated+" methods in "+(dif/1000)+" seconds");
@@ -488,6 +492,15 @@ public boolean accept(File file) {
488492

489493
}
490494

495+
private static void buildMethodUsageIndex() {
496+
usageIndex = new HashMap<String, List<BytecodeMethod>>();
497+
for(ByteCodeClass bc : classes) {
498+
for(BytecodeMethod m : bc.getMethods()) {
499+
m.populateMethodUsageIndex(usageIndex);
500+
}
501+
}
502+
}
503+
491504
private static int eliminateUnusedMethods() {
492505
return(eliminateUnusedMethods(false, 0));
493506
}
@@ -613,6 +626,24 @@ private static boolean isMethodUsed(BytecodeMethod m, ByteCodeClass cls) {
613626
if (!m.isEliminated() && m.isMethodUsedByNative(nativeSources, cls)) {
614627
return true;
615628
}
629+
630+
if (usageIndex != null) {
631+
String name = m.getMethodName();
632+
if ("__INIT__".equals(name)) {
633+
name = "<init>";
634+
}
635+
String key = name + m.getSignature();
636+
List<BytecodeMethod> callers = usageIndex.get(key);
637+
if (callers != null) {
638+
for (BytecodeMethod caller : callers) {
639+
if (!caller.isEliminated() && caller != m) {
640+
return true;
641+
}
642+
}
643+
}
644+
return false;
645+
}
646+
616647
for(ByteCodeClass bc : classes) {
617648
for(BytecodeMethod mtd : bc.getMethods()) {
618649
if(mtd.isEliminated() || mtd == m) {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.codename1.tools.translator;
2+
3+
import org.junit.jupiter.api.AfterEach;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.objectweb.asm.ClassWriter;
7+
import org.objectweb.asm.MethodVisitor;
8+
import org.objectweb.asm.Opcodes;
9+
10+
import java.io.File;
11+
import java.io.FileOutputStream;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
15+
public class StressTest {
16+
17+
private Path tempDir;
18+
private String originalOptimizer;
19+
20+
@BeforeEach
21+
public void setup() throws Exception {
22+
Parser.cleanup();
23+
tempDir = Files.createTempDirectory("stress-test");
24+
originalOptimizer = System.getProperty("optimizer");
25+
System.setProperty("optimizer", "on");
26+
// Force re-initialization of optimizerOn flag in BytecodeMethod if needed,
27+
// but it's static final initialized at load time.
28+
// Actually BytecodeMethod.optimizerOn is static but not final, but it is initialized in static block.
29+
// We might need to reflectively set it if the class is already loaded.
30+
// However, tests run in separate forks or BytecodeMethod might be reloaded?
31+
// Let's assume we might need to toggle it via reflection if it's already loaded.
32+
33+
try {
34+
java.lang.reflect.Field f = BytecodeMethod.class.getDeclaredField("optimizerOn");
35+
f.setAccessible(true);
36+
f.set(null, true);
37+
} catch (Exception e) {
38+
e.printStackTrace();
39+
}
40+
}
41+
42+
@AfterEach
43+
public void teardown() throws Exception {
44+
Parser.cleanup();
45+
deleteDir(tempDir.toFile());
46+
if (originalOptimizer != null) {
47+
System.setProperty("optimizer", originalOptimizer);
48+
} else {
49+
System.clearProperty("optimizer");
50+
}
51+
}
52+
53+
private void deleteDir(File file) {
54+
if (file.isDirectory()) {
55+
for (File f : file.listFiles()) {
56+
deleteDir(f);
57+
}
58+
}
59+
file.delete();
60+
}
61+
62+
@Test
63+
public void testChainOfMethods() throws Exception {
64+
// Creates a long chain of dependencies: Class0.main -> Class0.m0 -> Class0.m1 ... -> Class0.mN -> Class1.m0 ...
65+
int numClasses = 50;
66+
int methodsPerClass = 50;
67+
68+
System.out.println("Generating " + numClasses + " classes with " + methodsPerClass + " methods each...");
69+
for (int i = 0; i < numClasses; i++) {
70+
createClass(i, numClasses, methodsPerClass);
71+
}
72+
73+
System.out.println("Parsing classes...");
74+
File[] files = tempDir.toFile().listFiles((d, n) -> n.endsWith(".class"));
75+
for (File f : files) {
76+
Parser.parse(f);
77+
}
78+
79+
System.out.println("Running optimizer...");
80+
long start = System.currentTimeMillis();
81+
File outputDir = Files.createTempDirectory("stress-output").toFile();
82+
try {
83+
// Mock ByteCodeTranslator.output to C or something valid
84+
ByteCodeTranslator.output = ByteCodeTranslator.OutputType.OUTPUT_TYPE_IOS;
85+
86+
Parser.writeOutput(outputDir);
87+
} finally {
88+
deleteDir(outputDir);
89+
}
90+
long end = System.currentTimeMillis();
91+
System.out.println("Optimization and write took: " + (end - start) + "ms for " + (numClasses * methodsPerClass) + " methods.");
92+
}
93+
94+
private void createClass(int index, int totalClasses, int methodsPerClass) throws Exception {
95+
String className = "Class" + index;
96+
ClassWriter cw = new ClassWriter(0);
97+
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
98+
99+
// Add constructor
100+
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
101+
mv.visitCode();
102+
mv.visitVarInsn(Opcodes.ALOAD, 0);
103+
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
104+
mv.visitInsn(Opcodes.RETURN);
105+
mv.visitMaxs(1, 1);
106+
mv.visitEnd();
107+
108+
// Add main method if index 0
109+
if (index == 0) {
110+
mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
111+
mv.visitCode();
112+
// Call method 0 of class 0
113+
mv.visitMethodInsn(Opcodes.INVOKESTATIC, className, "method0", "()V", false);
114+
mv.visitInsn(Opcodes.RETURN);
115+
mv.visitMaxs(1, 1);
116+
mv.visitEnd();
117+
}
118+
119+
// Add methods
120+
for (int m = 0; m < methodsPerClass; m++) {
121+
mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "method" + m, "()V", null, null);
122+
mv.visitCode();
123+
124+
// Call next method or method in next class to create dependency chain
125+
if (m < methodsPerClass - 1) {
126+
mv.visitMethodInsn(Opcodes.INVOKESTATIC, className, "method" + (m + 1), "()V", false);
127+
} else if (index < totalClasses - 1) {
128+
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "Class" + (index + 1), "method0", "()V", false);
129+
}
130+
131+
mv.visitInsn(Opcodes.RETURN);
132+
mv.visitMaxs(1, 0);
133+
mv.visitEnd();
134+
}
135+
136+
cw.visitEnd();
137+
138+
FileOutputStream fos = new FileOutputStream(new File(tempDir.toFile(), className + ".class"));
139+
fos.write(cw.toByteArray());
140+
fos.close();
141+
}
142+
}

0 commit comments

Comments
 (0)