diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java index ff008acd94..9612a3396f 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/BytecodeMethod.java @@ -2068,5 +2068,29 @@ public SignatureSet nextSignature() { return null; } + public void populateMethodUsageIndex(Map> index) { + if(isEliminated()) return; + + Set uniqueCalls = new HashSet(); + + for(Instruction ins : instructions) { + String sname = ins.getMethodName(); + if(sname != null) { + String sig = ins.getSignature(); + if (sig != null) { + String key = sname + sig; + if (!uniqueCalls.contains(key)) { + uniqueCalls.add(key); + List callers = index.get(key); + if (callers == null) { + callers = new ArrayList(); + index.put(key, callers); + } + callers.add(this); + } + } + } + } + } } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java index dbc8d9f510..4064ff2518 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/Parser.java @@ -50,11 +50,13 @@ public class Parser extends ClassVisitor { private String clsName; private static String[] nativeSources; private static List classes = new ArrayList(); + private static Map> usageIndex; private int lambdaCounter; public static void cleanup() { nativeSources = null; classes.clear(); LabelInstruction.cleanup(); + usageIndex = null; } public static void parse(File sourceFile) throws Exception { if(ByteCodeTranslator.verbose) { @@ -434,7 +436,9 @@ public static void writeOutput(File outputDirectory) throws Exception { if (BytecodeMethod.optimizerOn) { System.out.println("Optimizer On: Removing unused methods and classes..."); Date now = new Date(); + buildMethodUsageIndex(); neliminated += eliminateUnusedMethods(); + usageIndex = null; Date later = new Date(); long dif = later.getTime()-now.getTime(); System.out.println("unusued Method cull removed "+neliminated+" methods in "+(dif/1000)+" seconds"); @@ -488,6 +492,15 @@ public boolean accept(File file) { } + private static void buildMethodUsageIndex() { + usageIndex = new HashMap>(); + for(ByteCodeClass bc : classes) { + for(BytecodeMethod m : bc.getMethods()) { + m.populateMethodUsageIndex(usageIndex); + } + } + } + private static int eliminateUnusedMethods() { return(eliminateUnusedMethods(false, 0)); } @@ -613,6 +626,24 @@ private static boolean isMethodUsed(BytecodeMethod m, ByteCodeClass cls) { if (!m.isEliminated() && m.isMethodUsedByNative(nativeSources, cls)) { return true; } + + if (usageIndex != null) { + String name = m.getMethodName(); + if ("__INIT__".equals(name)) { + name = ""; + } + String key = name + m.getSignature(); + List callers = usageIndex.get(key); + if (callers != null) { + for (BytecodeMethod caller : callers) { + if (!caller.isEliminated() && caller != m) { + return true; + } + } + } + return false; + } + for(ByteCodeClass bc : classes) { for(BytecodeMethod mtd : bc.getMethods()) { if(mtd.isEliminated() || mtd == m) { diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/StressTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/StressTest.java new file mode 100644 index 0000000000..61d7f8bffc --- /dev/null +++ b/vm/tests/src/test/java/com/codename1/tools/translator/StressTest.java @@ -0,0 +1,142 @@ +package com.codename1.tools.translator; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class StressTest { + + private Path tempDir; + private String originalOptimizer; + + @BeforeEach + public void setup() throws Exception { + Parser.cleanup(); + tempDir = Files.createTempDirectory("stress-test"); + originalOptimizer = System.getProperty("optimizer"); + System.setProperty("optimizer", "on"); + // Force re-initialization of optimizerOn flag in BytecodeMethod if needed, + // but it's static final initialized at load time. + // Actually BytecodeMethod.optimizerOn is static but not final, but it is initialized in static block. + // We might need to reflectively set it if the class is already loaded. + // However, tests run in separate forks or BytecodeMethod might be reloaded? + // Let's assume we might need to toggle it via reflection if it's already loaded. + + try { + java.lang.reflect.Field f = BytecodeMethod.class.getDeclaredField("optimizerOn"); + f.setAccessible(true); + f.set(null, true); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @AfterEach + public void teardown() throws Exception { + Parser.cleanup(); + deleteDir(tempDir.toFile()); + if (originalOptimizer != null) { + System.setProperty("optimizer", originalOptimizer); + } else { + System.clearProperty("optimizer"); + } + } + + private void deleteDir(File file) { + if (file.isDirectory()) { + for (File f : file.listFiles()) { + deleteDir(f); + } + } + file.delete(); + } + + @Test + public void testChainOfMethods() throws Exception { + // Creates a long chain of dependencies: Class0.main -> Class0.m0 -> Class0.m1 ... -> Class0.mN -> Class1.m0 ... + int numClasses = 50; + int methodsPerClass = 50; + + System.out.println("Generating " + numClasses + " classes with " + methodsPerClass + " methods each..."); + for (int i = 0; i < numClasses; i++) { + createClass(i, numClasses, methodsPerClass); + } + + System.out.println("Parsing classes..."); + File[] files = tempDir.toFile().listFiles((d, n) -> n.endsWith(".class")); + for (File f : files) { + Parser.parse(f); + } + + System.out.println("Running optimizer..."); + long start = System.currentTimeMillis(); + File outputDir = Files.createTempDirectory("stress-output").toFile(); + try { + // Mock ByteCodeTranslator.output to C or something valid + ByteCodeTranslator.output = ByteCodeTranslator.OutputType.OUTPUT_TYPE_IOS; + + Parser.writeOutput(outputDir); + } finally { + deleteDir(outputDir); + } + long end = System.currentTimeMillis(); + System.out.println("Optimization and write took: " + (end - start) + "ms for " + (numClasses * methodsPerClass) + " methods."); + } + + private void createClass(int index, int totalClasses, int methodsPerClass) throws Exception { + String className = "Class" + index; + ClassWriter cw = new ClassWriter(0); + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null); + + // Add constructor + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + + // Add main method if index 0 + if (index == 0) { + mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); + mv.visitCode(); + // Call method 0 of class 0 + mv.visitMethodInsn(Opcodes.INVOKESTATIC, className, "method0", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + + // Add methods + for (int m = 0; m < methodsPerClass; m++) { + mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "method" + m, "()V", null, null); + mv.visitCode(); + + // Call next method or method in next class to create dependency chain + if (m < methodsPerClass - 1) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, className, "method" + (m + 1), "()V", false); + } else if (index < totalClasses - 1) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "Class" + (index + 1), "method0", "()V", false); + } + + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(1, 0); + mv.visitEnd(); + } + + cw.visitEnd(); + + FileOutputStream fos = new FileOutputStream(new File(tempDir.toFile(), className + ".class")); + fos.write(cw.toByteArray()); + fos.close(); + } +}