Skip to content

Commit 89eee9c

Browse files
committed
Optimize method culling graph and add stress test
1 parent 7affe5a commit 89eee9c

File tree

3 files changed

+271
-18
lines changed

3 files changed

+271
-18
lines changed

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,10 @@ public static void setAcceptStaticOnEquals(boolean aAcceptStaticOnEquals) {
9898
private boolean virtualOverriden;
9999
private boolean finalMethod;
100100
private boolean synchronizedMethod;
101-
private final static Set<String> virtualMethodsInvoked = new TreeSet<String>();
101+
private final static Set<String> virtualMethodsInvoked = new TreeSet<String>();
102102
private String desc;
103103
private boolean eliminated;
104+
private Set<String> calledMethodSignatureKeys;
104105

105106

106107
static boolean optimizerOn;
@@ -347,7 +348,39 @@ public boolean isMethodUsedByNative(String[] nativeSources, ByteCodeClass cls) {
347348
usedByNative = false;
348349
return false;
349350
}
350-
351+
352+
private static String toMethodKey(String signature, String name) {
353+
if (signature == null || name == null) {
354+
return null;
355+
}
356+
if ("__INIT__".equals(name)) {
357+
return signature + ".<init>";
358+
}
359+
if ("__CLINIT__".equals(name)) {
360+
return signature + ".<clinit>";
361+
}
362+
return signature + "." + name;
363+
}
364+
365+
public String getMethodUsageKey() {
366+
return toMethodKey(desc, methodName);
367+
}
368+
369+
public Set<String> getCalledMethodSignatureKeys() {
370+
if (calledMethodSignatureKeys == null) {
371+
calledMethodSignatureKeys = new HashSet<String>();
372+
for (Instruction ins : instructions) {
373+
String name = ins.getMethodName();
374+
String signature = ins.getSignature();
375+
String key = toMethodKey(signature, name);
376+
if (key != null) {
377+
calledMethodSignatureKeys.add(key);
378+
}
379+
}
380+
}
381+
return calledMethodSignatureKeys;
382+
}
383+
351384
private Set<String> usedMethods;
352385
public boolean isMethodUsedOldWay(BytecodeMethod bm) {
353386
if(usedMethods == null) {

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

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,17 @@ public class Parser extends ClassVisitor {
5050
private String clsName;
5151
private static String[] nativeSources;
5252
private static List<ByteCodeClass> classes = new ArrayList<ByteCodeClass>();
53+
private static Map<String, Integer> methodUsageCounts;
54+
private static Map<BytecodeMethod, Set<String>> methodCallMap;
55+
private static Set<BytecodeMethod> removedFromUsageIndex;
5356
private int lambdaCounter;
5457
public static void cleanup() {
55-
nativeSources = null;
56-
classes.clear();
57-
LabelInstruction.cleanup();
58+
nativeSources = null;
59+
classes.clear();
60+
methodUsageCounts = null;
61+
methodCallMap = null;
62+
removedFromUsageIndex = null;
63+
LabelInstruction.cleanup();
5864
}
5965
public static void parse(File sourceFile) throws Exception {
6066
if(ByteCodeTranslator.verbose) {
@@ -485,14 +491,83 @@ public boolean accept(File file) {
485491
nativeSources[iter] = new String(dat, "UTF-8");
486492
}
487493
System.out.println("Native files total "+(size/1024)+"K");
488-
494+
489495
}
490-
496+
497+
private static String buildMethodUsageKey(BytecodeMethod method) {
498+
return method.getMethodUsageKey();
499+
}
500+
501+
private static void ensureMethodUsageIndex() {
502+
if (methodUsageCounts != null) {
503+
return;
504+
}
505+
buildMethodUsageIndex();
506+
}
507+
508+
private static Set<String> collectCalledMethods(BytecodeMethod caller) {
509+
Set<String> calls = new HashSet<String>();
510+
String callerKey = buildMethodUsageKey(caller);
511+
for (String key : caller.getCalledMethodSignatureKeys()) {
512+
if (!key.equals(callerKey)) {
513+
calls.add(key);
514+
}
515+
}
516+
return calls;
517+
}
518+
519+
private static void buildMethodUsageIndex() {
520+
methodUsageCounts = new HashMap<String, Integer>();
521+
methodCallMap = new HashMap<BytecodeMethod, Set<String>>();
522+
removedFromUsageIndex = new HashSet<BytecodeMethod>();
523+
524+
for (ByteCodeClass bc : classes) {
525+
for (BytecodeMethod method : bc.getMethods()) {
526+
if (method.isEliminated()) {
527+
continue;
528+
}
529+
Set<String> calls = collectCalledMethods(method);
530+
methodCallMap.put(method, calls);
531+
for (String key : calls) {
532+
Integer count = methodUsageCounts.get(key);
533+
methodUsageCounts.put(key, count == null ? 1 : count + 1);
534+
}
535+
}
536+
}
537+
}
538+
539+
private static void removeFromMethodUsageIndex(BytecodeMethod method) {
540+
if (removedFromUsageIndex == null) {
541+
removedFromUsageIndex = new HashSet<BytecodeMethod>();
542+
}
543+
if (removedFromUsageIndex.contains(method)) {
544+
return;
545+
}
546+
ensureMethodUsageIndex();
547+
Set<String> calls = methodCallMap.remove(method);
548+
if (calls == null) {
549+
calls = collectCalledMethods(method);
550+
}
551+
for (String key : calls) {
552+
Integer count = methodUsageCounts.get(key);
553+
if (count == null) {
554+
continue;
555+
}
556+
if (count <= 1) {
557+
methodUsageCounts.remove(key);
558+
} else {
559+
methodUsageCounts.put(key, count - 1);
560+
}
561+
}
562+
removedFromUsageIndex.add(method);
563+
}
564+
491565
private static int eliminateUnusedMethods() {
492566
return(eliminateUnusedMethods(false, 0));
493567
}
494568

495569
private static int eliminateUnusedMethods(boolean forceFound, int depth) {
570+
ensureMethodUsageIndex();
496571
int nfound = cullMethods();
497572
nfound += cullClasses(nfound>0 || forceFound, depth);
498573
return(nfound);
@@ -519,6 +594,7 @@ private static int cullMethods() {
519594
continue;
520595
}
521596
mtd.setEliminated(true);
597+
removeFromMethodUsageIndex(mtd);
522598
nfound++;
523599
}
524600
}
@@ -596,6 +672,9 @@ private static int cullClasses(boolean found, int depth) {
596672
int nfound = 0;
597673
for (ByteCodeClass cls : removedClasses) {
598674
nfound += cls.setEliminated(true);
675+
for (BytecodeMethod method : cls.getMethods()) {
676+
removeFromMethodUsageIndex(method);
677+
}
599678
}
600679
classes = tmp;
601680
return nfound + eliminateUnusedMethods(nfound > 0, depth + 1);
@@ -613,17 +692,9 @@ private static boolean isMethodUsed(BytecodeMethod m, ByteCodeClass cls) {
613692
if (!m.isEliminated() && m.isMethodUsedByNative(nativeSources, cls)) {
614693
return true;
615694
}
616-
for(ByteCodeClass bc : classes) {
617-
for(BytecodeMethod mtd : bc.getMethods()) {
618-
if(mtd.isEliminated() || mtd == m) {
619-
continue;
620-
}
621-
if(mtd.isMethodUsed(m)) {
622-
return true;
623-
}
624-
}
625-
}
626-
return false;
695+
ensureMethodUsageIndex();
696+
Integer count = methodUsageCounts.get(buildMethodUsageKey(m));
697+
return count != null && count > 0;
627698
}
628699

629700
private static void writeFile(ByteCodeClass cls, File outputDir, ConcatenatingFileOutputStream writeBufferInstead) throws Exception {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package com.codename1.tools.translator;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.objectweb.asm.ClassWriter;
5+
import org.objectweb.asm.MethodVisitor;
6+
import org.objectweb.asm.Opcodes;
7+
8+
import java.lang.reflect.Field;
9+
import java.lang.reflect.Method;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.time.Duration;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.*;
17+
18+
class CullPerformanceTest {
19+
20+
@Test
21+
void cullsLargeUnusedGraphQuickly() throws Exception {
22+
Parser.cleanup();
23+
24+
Path classesDir = Files.createTempDirectory("cull-stress-classes");
25+
int totalNodes = 300;
26+
List<Path> generated = new ArrayList<Path>();
27+
generated.add(writeStub(classesDir, "java/lang/Object"));
28+
29+
for (int i = 0; i < totalNodes; i++) {
30+
generated.add(writeNode(classesDir, i, totalNodes));
31+
}
32+
generated.add(writeEntryPoint(classesDir, totalNodes));
33+
34+
for (Path classFile : generated) {
35+
Parser.parse(classFile.toFile());
36+
}
37+
38+
List<ByteCodeClass> parsedClasses = getParsedClasses();
39+
for (ByteCodeClass bc : parsedClasses) {
40+
bc.updateAllDependencies();
41+
}
42+
ByteCodeClass.markDependencies(parsedClasses, null);
43+
List<ByteCodeClass> reachable = ByteCodeClass.clearUnmarked(parsedClasses);
44+
setParsedClasses(reachable);
45+
46+
Method eliminateUnusedMethods = Parser.class.getDeclaredMethod("eliminateUnusedMethods");
47+
eliminateUnusedMethods.setAccessible(true);
48+
49+
assertTimeoutPreemptively(Duration.ofSeconds(10), () -> eliminateUnusedMethods.invoke(null));
50+
51+
long remaining = reachable.stream().filter(bc -> !bc.isEliminated()).count();
52+
assertTrue(remaining < totalNodes / 4, "Most generated classes should be culled to keep optimization fast");
53+
}
54+
55+
private Path writeStub(Path classesDir, String internalName) throws Exception {
56+
ClassWriter cw = new ClassWriter(0);
57+
String superName = "java/lang/Object".equals(internalName) ? null : "java/lang/Object";
58+
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, superName, null);
59+
addDefaultConstructor(cw, superName);
60+
cw.visitEnd();
61+
return writeClass(classesDir, internalName, cw.toByteArray());
62+
}
63+
64+
private Path writeNode(Path classesDir, int index, int totalNodes) throws Exception {
65+
String name = "com/example/stress/Node" + index;
66+
ClassWriter cw = new ClassWriter(0);
67+
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, "java/lang/Object", null);
68+
69+
addDefaultConstructor(cw, "java/lang/Object");
70+
71+
MethodVisitor value = cw.visitMethod(Opcodes.ACC_PUBLIC, "value", "(I)I", null, null);
72+
value.visitCode();
73+
value.visitVarInsn(Opcodes.ILOAD, 1);
74+
value.visitIntInsn(Opcodes.SIPUSH, index);
75+
value.visitInsn(Opcodes.IADD);
76+
77+
if (index + 1 < totalNodes && index % 3 == 0) {
78+
String next = "com/example/stress/Node" + (index + 1);
79+
value.visitTypeInsn(Opcodes.NEW, next);
80+
value.visitInsn(Opcodes.DUP);
81+
value.visitMethodInsn(Opcodes.INVOKESPECIAL, next, "<init>", "()V", false);
82+
value.visitVarInsn(Opcodes.ILOAD, 1);
83+
value.visitMethodInsn(Opcodes.INVOKEVIRTUAL, next, "value", "(I)I", false);
84+
value.visitInsn(Opcodes.IADD);
85+
}
86+
87+
value.visitInsn(Opcodes.IRETURN);
88+
value.visitMaxs(3, 2);
89+
value.visitEnd();
90+
91+
cw.visitEnd();
92+
return writeClass(classesDir, name, cw.toByteArray());
93+
}
94+
95+
private Path writeEntryPoint(Path classesDir, int totalNodes) throws Exception {
96+
String name = "com/example/stress/StressEntry";
97+
ClassWriter cw = new ClassWriter(0);
98+
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, "java/lang/Object", null);
99+
100+
addDefaultConstructor(cw, "java/lang/Object");
101+
102+
MethodVisitor main = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
103+
main.visitCode();
104+
main.visitTypeInsn(Opcodes.NEW, "com/example/stress/Node0");
105+
main.visitInsn(Opcodes.DUP);
106+
main.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/example/stress/Node0", "<init>", "()V", false);
107+
main.visitInsn(Opcodes.ICONST_0);
108+
main.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/example/stress/Node0", "value", "(I)I", false);
109+
main.visitInsn(Opcodes.POP);
110+
main.visitInsn(Opcodes.RETURN);
111+
main.visitMaxs(3, 1);
112+
main.visitEnd();
113+
114+
cw.visitEnd();
115+
return writeClass(classesDir, name, cw.toByteArray());
116+
}
117+
118+
private void addDefaultConstructor(ClassWriter cw, String superName) {
119+
MethodVisitor ctor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
120+
ctor.visitCode();
121+
if (superName != null) {
122+
ctor.visitVarInsn(Opcodes.ALOAD, 0);
123+
ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, superName, "<init>", "()V", false);
124+
}
125+
ctor.visitInsn(Opcodes.RETURN);
126+
ctor.visitMaxs(superName != null ? 1 : 0, 1);
127+
ctor.visitEnd();
128+
}
129+
130+
private Path writeClass(Path classesDir, String internalName, byte[] data) throws Exception {
131+
Path classFile = classesDir.resolve(internalName + ".class");
132+
Files.createDirectories(classFile.getParent());
133+
Files.write(classFile, data);
134+
return classFile;
135+
}
136+
137+
@SuppressWarnings("unchecked")
138+
private List<ByteCodeClass> getParsedClasses() throws Exception {
139+
Field f = Parser.class.getDeclaredField("classes");
140+
f.setAccessible(true);
141+
return (List<ByteCodeClass>) f.get(null);
142+
}
143+
144+
private void setParsedClasses(List<ByteCodeClass> updated) throws Exception {
145+
Field f = Parser.class.getDeclaredField("classes");
146+
f.setAccessible(true);
147+
f.set(null, updated);
148+
}
149+
}

0 commit comments

Comments
 (0)