diff --git a/maven/core-unittests/src/test/java/com/codename1/junit/UITestBase.java b/maven/core-unittests/src/test/java/com/codename1/junit/UITestBase.java index d4ddd85f24..95dd0c5809 100644 --- a/maven/core-unittests/src/test/java/com/codename1/junit/UITestBase.java +++ b/maven/core-unittests/src/test/java/com/codename1/junit/UITestBase.java @@ -63,6 +63,7 @@ public Object createImplementation() { implementation = TestCodenameOneImplementation.getInstance(); implementation.setLocalizationManager(new SafeL10NManager("en", "US")); } + display = Display.getInstance(); Util.setImplementation(implementation); } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java index ff008acd94..589d97a3cf 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java @@ -98,9 +98,10 @@ public static void setAcceptStaticOnEquals(boolean aAcceptStaticOnEquals) { private boolean virtualOverriden; private boolean finalMethod; private boolean synchronizedMethod; - private final static Set virtualMethodsInvoked = new TreeSet(); + private final static Set virtualMethodsInvoked = new TreeSet(); private String desc; private boolean eliminated; + private Set calledMethodSignatureKeys; static boolean optimizerOn; @@ -347,7 +348,39 @@ public boolean isMethodUsedByNative(String[] nativeSources, ByteCodeClass cls) { usedByNative = false; return false; } - + + private static String toMethodKey(String signature, String name) { + if (signature == null || name == null) { + return null; + } + if ("__INIT__".equals(name)) { + return signature + "."; + } + if ("__CLINIT__".equals(name)) { + return signature + "."; + } + return signature + "." + name; + } + + public String getMethodUsageKey() { + return toMethodKey(desc, methodName); + } + + public Set getCalledMethodSignatureKeys() { + if (calledMethodSignatureKeys == null) { + calledMethodSignatureKeys = new HashSet(); + for (Instruction ins : instructions) { + String name = ins.getMethodName(); + String signature = ins.getSignature(); + String key = toMethodKey(signature, name); + if (key != null) { + calledMethodSignatureKeys.add(key); + } + } + } + return calledMethodSignatureKeys; + } + private Set usedMethods; public boolean isMethodUsedOldWay(BytecodeMethod bm) { if(usedMethods == null) { diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index dbc8d9f510..127bb05668 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -50,11 +50,17 @@ public class Parser extends ClassVisitor { private String clsName; private static String[] nativeSources; private static List classes = new ArrayList(); + private static Map methodUsageCounts; + private static Map> methodCallMap; + private static Set removedFromUsageIndex; private int lambdaCounter; public static void cleanup() { - nativeSources = null; - classes.clear(); - LabelInstruction.cleanup(); + nativeSources = null; + classes.clear(); + methodUsageCounts = null; + methodCallMap = null; + removedFromUsageIndex = null; + LabelInstruction.cleanup(); } public static void parse(File sourceFile) throws Exception { if(ByteCodeTranslator.verbose) { @@ -485,14 +491,83 @@ public boolean accept(File file) { nativeSources[iter] = new String(dat, "UTF-8"); } System.out.println("Native files total "+(size/1024)+"K"); - + } - + + private static String buildMethodUsageKey(BytecodeMethod method) { + return method.getMethodUsageKey(); + } + + private static void ensureMethodUsageIndex() { + if (methodUsageCounts != null) { + return; + } + buildMethodUsageIndex(); + } + + private static Set collectCalledMethods(BytecodeMethod caller) { + Set calls = new HashSet(); + String callerKey = buildMethodUsageKey(caller); + for (String key : caller.getCalledMethodSignatureKeys()) { + if (!key.equals(callerKey)) { + calls.add(key); + } + } + return calls; + } + + private static void buildMethodUsageIndex() { + methodUsageCounts = new HashMap(); + methodCallMap = new HashMap>(); + removedFromUsageIndex = new HashSet(); + + for (ByteCodeClass bc : classes) { + for (BytecodeMethod method : bc.getMethods()) { + if (method.isEliminated()) { + continue; + } + Set calls = collectCalledMethods(method); + methodCallMap.put(method, calls); + for (String key : calls) { + Integer count = methodUsageCounts.get(key); + methodUsageCounts.put(key, count == null ? 1 : count + 1); + } + } + } + } + + private static void removeFromMethodUsageIndex(BytecodeMethod method) { + if (removedFromUsageIndex == null) { + removedFromUsageIndex = new HashSet(); + } + if (removedFromUsageIndex.contains(method)) { + return; + } + ensureMethodUsageIndex(); + Set calls = methodCallMap.remove(method); + if (calls == null) { + calls = collectCalledMethods(method); + } + for (String key : calls) { + Integer count = methodUsageCounts.get(key); + if (count == null) { + continue; + } + if (count <= 1) { + methodUsageCounts.remove(key); + } else { + methodUsageCounts.put(key, count - 1); + } + } + removedFromUsageIndex.add(method); + } + private static int eliminateUnusedMethods() { return(eliminateUnusedMethods(false, 0)); } private static int eliminateUnusedMethods(boolean forceFound, int depth) { + ensureMethodUsageIndex(); int nfound = cullMethods(); nfound += cullClasses(nfound>0 || forceFound, depth); return(nfound); @@ -519,6 +594,7 @@ private static int cullMethods() { continue; } mtd.setEliminated(true); + removeFromMethodUsageIndex(mtd); nfound++; } } @@ -596,6 +672,9 @@ private static int cullClasses(boolean found, int depth) { int nfound = 0; for (ByteCodeClass cls : removedClasses) { nfound += cls.setEliminated(true); + for (BytecodeMethod method : cls.getMethods()) { + removeFromMethodUsageIndex(method); + } } classes = tmp; return nfound + eliminateUnusedMethods(nfound > 0, depth + 1); @@ -613,17 +692,9 @@ private static boolean isMethodUsed(BytecodeMethod m, ByteCodeClass cls) { if (!m.isEliminated() && m.isMethodUsedByNative(nativeSources, cls)) { return true; } - for(ByteCodeClass bc : classes) { - for(BytecodeMethod mtd : bc.getMethods()) { - if(mtd.isEliminated() || mtd == m) { - continue; - } - if(mtd.isMethodUsed(m)) { - return true; - } - } - } - return false; + ensureMethodUsageIndex(); + Integer count = methodUsageCounts.get(buildMethodUsageKey(m)); + return count != null && count > 0; } private static void writeFile(ByteCodeClass cls, File outputDir, ConcatenatingFileOutputStream writeBufferInstead) throws Exception { diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CullPerformanceTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CullPerformanceTest.java new file mode 100644 index 0000000000..a97214eda8 --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CullPerformanceTest.java @@ -0,0 +1,149 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class CullPerformanceTest { + + @Test + void cullsLargeUnusedGraphQuickly() throws Exception { + Parser.cleanup(); + + Path classesDir = Files.createTempDirectory("cull-stress-classes"); + int totalNodes = 300; + List generated = new ArrayList(); + generated.add(writeStub(classesDir, "java/lang/Object")); + + for (int i = 0; i < totalNodes; i++) { + generated.add(writeNode(classesDir, i, totalNodes)); + } + generated.add(writeEntryPoint(classesDir, totalNodes)); + + for (Path classFile : generated) { + Parser.parse(classFile.toFile()); + } + + List parsedClasses = getParsedClasses(); + for (ByteCodeClass bc : parsedClasses) { + bc.updateAllDependencies(); + } + ByteCodeClass.markDependencies(parsedClasses, null); + List reachable = ByteCodeClass.clearUnmarked(parsedClasses); + setParsedClasses(reachable); + + Method eliminateUnusedMethods = Parser.class.getDeclaredMethod("eliminateUnusedMethods"); + eliminateUnusedMethods.setAccessible(true); + + assertTimeoutPreemptively(Duration.ofSeconds(10), () -> eliminateUnusedMethods.invoke(null)); + + long remaining = reachable.stream().filter(bc -> !bc.isEliminated()).count(); + assertTrue(remaining < totalNodes / 4, "Most generated classes should be culled to keep optimization fast"); + } + + private Path writeStub(Path classesDir, String internalName) throws Exception { + ClassWriter cw = new ClassWriter(0); + String superName = "java/lang/Object".equals(internalName) ? null : "java/lang/Object"; + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, superName, null); + addDefaultConstructor(cw, superName); + cw.visitEnd(); + return writeClass(classesDir, internalName, cw.toByteArray()); + } + + private Path writeNode(Path classesDir, int index, int totalNodes) throws Exception { + String name = "com/example/stress/Node" + index; + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, "java/lang/Object", null); + + addDefaultConstructor(cw, "java/lang/Object"); + + MethodVisitor value = cw.visitMethod(Opcodes.ACC_PUBLIC, "value", "(I)I", null, null); + value.visitCode(); + value.visitVarInsn(Opcodes.ILOAD, 1); + value.visitIntInsn(Opcodes.SIPUSH, index); + value.visitInsn(Opcodes.IADD); + + if (index + 1 < totalNodes && index % 3 == 0) { + String next = "com/example/stress/Node" + (index + 1); + value.visitTypeInsn(Opcodes.NEW, next); + value.visitInsn(Opcodes.DUP); + value.visitMethodInsn(Opcodes.INVOKESPECIAL, next, "", "()V", false); + value.visitVarInsn(Opcodes.ILOAD, 1); + value.visitMethodInsn(Opcodes.INVOKEVIRTUAL, next, "value", "(I)I", false); + value.visitInsn(Opcodes.IADD); + } + + value.visitInsn(Opcodes.IRETURN); + value.visitMaxs(3, 2); + value.visitEnd(); + + cw.visitEnd(); + return writeClass(classesDir, name, cw.toByteArray()); + } + + private Path writeEntryPoint(Path classesDir, int totalNodes) throws Exception { + String name = "com/example/stress/StressEntry"; + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, name, null, "java/lang/Object", null); + + addDefaultConstructor(cw, "java/lang/Object"); + + MethodVisitor main = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); + main.visitCode(); + main.visitTypeInsn(Opcodes.NEW, "com/example/stress/Node0"); + main.visitInsn(Opcodes.DUP); + main.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/example/stress/Node0", "", "()V", false); + main.visitInsn(Opcodes.ICONST_0); + main.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/example/stress/Node0", "value", "(I)I", false); + main.visitInsn(Opcodes.POP); + main.visitInsn(Opcodes.RETURN); + main.visitMaxs(3, 1); + main.visitEnd(); + + cw.visitEnd(); + return writeClass(classesDir, name, cw.toByteArray()); + } + + private void addDefaultConstructor(ClassWriter cw, String superName) { + MethodVisitor ctor = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + ctor.visitCode(); + if (superName != null) { + ctor.visitVarInsn(Opcodes.ALOAD, 0); + ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, superName, "", "()V", false); + } + ctor.visitInsn(Opcodes.RETURN); + ctor.visitMaxs(superName != null ? 1 : 0, 1); + ctor.visitEnd(); + } + + private Path writeClass(Path classesDir, String internalName, byte[] data) throws Exception { + Path classFile = classesDir.resolve(internalName + ".class"); + Files.createDirectories(classFile.getParent()); + Files.write(classFile, data); + return classFile; + } + + @SuppressWarnings("unchecked") + private List getParsedClasses() throws Exception { + Field f = Parser.class.getDeclaredField("classes"); + f.setAccessible(true); + return (List) f.get(null); + } + + private void setParsedClasses(List updated) throws Exception { + Field f = Parser.class.getDeclaredField("classes"); + f.setAccessible(true); + f.set(null, updated); + } +}