Skip to content

Commit c2ea228

Browse files
shai-almogclaude
andcommitted
js-port translator: elide clinit guards for classes with no initializer (TeaVM parity)
GETSTATIC/PUTSTATIC/NEW/static-call sites emitted `_I(owner)` (ensureClassInitialized) unconditionally, even when the owner's entire hierarchy has no `<clinit>` and no deferred static-field initialization -- in which case the runtime helper just walks the supertypes, finds nothing to run, and returns. A TeaVM build of hellocodenameone emits ~90 class-init guards; ParparVM emitted ~8900. Add classNeedsInitialization(className): walks the class + superclasses + (transitive) superinterfaces and returns true iff any has an explicit clinit or deferred static init. Over-approximates (returns true) on unknown classes / hierarchy cycles so a needed guard is never dropped; static fields are default-initialised at defineClass time, so a clinit-less class is safe to read unguarded. Consulted at all four `_I(...)` emission sites (straight-line, interpreter, static-method entry, static wrapper). Result cached per translation run (cleared in setClassIndex). hellocodenameone: clinit guards 8906 -> 6075 (-32%); translated_app.js 5127 -> 5101 KB raw, 1075 -> 1070 gzip; fewer runtime ensureClassInitialized calls (startup perf). Verified: clean boot, UI renders, no errors. The remaining guards are on genuinely init-bearing hierarchies; closing the rest of the gap to TeaVM needs whole-program "already-initialised" propagation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2af6a3f commit c2ea228

1 file changed

Lines changed: 72 additions & 2 deletions

File tree

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

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ final class JavascriptMethodGenerator {
113113
private JavascriptMethodGenerator() {
114114
}
115115

116+
// Memoises classNeedsInitialization(); keyed on the sanitized class name.
117+
// Cleared whenever classIndex changes so a stale run can't leak.
118+
private static final Map<String, Boolean> classNeedsInitCache = new java.util.concurrent.ConcurrentHashMap<String, Boolean>();
119+
116120
static void setClassIndex(List<ByteCodeClass> allClasses) {
121+
classNeedsInitCache.clear();
117122
if (allClasses == null) {
118123
classIndex = null;
119124
referencedStaticFields = null;
@@ -664,6 +669,62 @@ private static String resolveStaticFieldOwner(String owner, String fieldName) {
664669
return start;
665670
}
666671

672+
/**
673+
* True when {@code _I(className)} (ensureClassInitialized) can have any
674+
* observable effect: the class or one of its supertypes (superclass or a
675+
* transitive superinterface) has an explicit {@code <clinit>} or deferred
676+
* static-field initialization. When false the guard is pure overhead — the
677+
* runtime helper walks the hierarchy, finds no clinit, and returns — so the
678+
* emitter can omit it entirely. This mirrors TeaVM, which only guards
679+
* init-bearing classes (a hellocodenameone build emits ~90 guards; ParparVM
680+
* emitted ~8900 because it guarded unconditionally). Over-approximates
681+
* (returns {@code true}) for unknown classes / hierarchy cycles so a needed
682+
* guard is never dropped; static fields themselves are default-initialised
683+
* at {@code defineClass} time, so a clinit-less class is safe to read
684+
* without a guard.
685+
*/
686+
private static boolean classNeedsInitialization(String className) {
687+
Map<String, ByteCodeClass> idx = classIndex;
688+
if (className == null || idx == null) {
689+
return true;
690+
}
691+
String start = JavascriptNameUtil.sanitizeClassName(className);
692+
Boolean cached = classNeedsInitCache.get(start);
693+
if (cached != null) {
694+
return cached;
695+
}
696+
boolean needs = false;
697+
java.util.Set<String> seen = new java.util.HashSet<String>();
698+
java.util.Deque<String> stack = new java.util.ArrayDeque<String>();
699+
stack.push(start);
700+
while (!stack.isEmpty()) {
701+
String cur = stack.pop();
702+
if (cur == null || !seen.add(cur)) {
703+
continue;
704+
}
705+
ByteCodeClass cls = idx.get(cur);
706+
if (cls == null) {
707+
needs = true; // unknown class -> keep the guard
708+
break;
709+
}
710+
if (hasExplicitClinit(cls) || hasDeferredStaticInitialization(cls)) {
711+
needs = true;
712+
break;
713+
}
714+
String base = cls.getBaseClass();
715+
if (base != null) {
716+
stack.push(JavascriptNameUtil.sanitizeClassName(base));
717+
}
718+
if (cls.getBaseInterfaces() != null) {
719+
for (String iface : cls.getBaseInterfaces()) {
720+
stack.push(JavascriptNameUtil.sanitizeClassName(iface));
721+
}
722+
}
723+
}
724+
classNeedsInitCache.put(start, needs);
725+
return needs;
726+
}
727+
667728
static String generateClassJavascript(ByteCodeClass cls, List<ByteCodeClass> allClasses) {
668729
// Populate the resolution index lazily on first call and keep it
669730
// alive for the rest of the generation pass. The size check is
@@ -2477,7 +2538,8 @@ private static void appendMethodImpl(StringBuilder out, StringBuilder regs, Byte
24772538
out.append("__cn1Arg").append(i + 1);
24782539
}
24792540
out.append("){\n");
2480-
if (!wrappedStaticMethod && method.isStatic() && !"__CLINIT__".equals(method.getMethodName())) {
2541+
if (!wrappedStaticMethod && method.isStatic() && !"__CLINIT__".equals(method.getMethodName())
2542+
&& classNeedsInitialization(cls.getClsName())) {
24812543
out.append(" _I(\"").append(cls.getClsName()).append("\");\n");
24822544
}
24832545
if ("__CLINIT__".equals(method.getMethodName())) {
@@ -2819,7 +2881,9 @@ private static void appendWrappedStaticMethod(StringBuilder out, ByteCodeClass c
28192881
out.append(suspending ? "function* " : "function ").append(wrapperName).append("(");
28202882
appendMethodParameters(out, method);
28212883
out.append("){\n");
2822-
out.append(" _I(\"").append(cls.getClsName()).append("\");\n");
2884+
if (classNeedsInitialization(cls.getClsName())) {
2885+
out.append(" _I(\"").append(cls.getClsName()).append("\");\n");
2886+
}
28232887
out.append(" return ").append(suspending ? "yield* " : "").append(bodyName).append("(");
28242888
appendMethodParameterArguments(out, method);
28252889
out.append(");\n");
@@ -4790,6 +4854,9 @@ private static boolean appendStraightLineFieldInstruction(StringBuilder out, Fie
47904854
}
47914855

47924856
private static void appendStraightLineEnsureClassInitialized(StringBuilder out, StraightLineContext ctx, String owner) {
4857+
if (!classNeedsInitialization(owner)) {
4858+
return;
4859+
}
47934860
if (ctx.initializedClasses.add(owner)) {
47944861
out.append(" _I(\"").append(owner).append("\");\n");
47954862
}
@@ -6635,6 +6702,9 @@ private static void appendInterpreterEnsureClassInitialized(StringBuilder out, S
66356702
// any of its methods can run, so both are already live by the
66366703
// time this method body executes. ~30% of the 7.7k ``jvm.eI``
66376704
// sites resolve to the containing class or its ancestors.
6705+
if (!classNeedsInitialization(owner)) {
6706+
return;
6707+
}
66386708
if (isClassAlreadyInitializedForCurrentEmission(owner)) {
66396709
return;
66406710
}

0 commit comments

Comments
 (0)