Skip to content

ParparVM performance: parity with warmed Java 25 (geomean 1.00x)#5327

Open
shai-almog wants to merge 163 commits into
masterfrom
parparvm-perf-tier1
Open

ParparVM performance: parity with warmed Java 25 (geomean 1.00x)#5327
shai-almog wants to merge 163 commits into
masterfrom
parparvm-perf-tier1

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

This branch takes ParparVM from a ~1.5-36x deficit against warmed Java 25 (HotSpot C2) to geomean 1.00x parity across the ten-benchmark suite, with six benchmarks at or below HotSpot. Everything is measured on Apple M2, best-of-5 interleaved runs, ThinLTO release configuration, against azul-25 with full warmup; every optimization is gated on bit-identical checksums vs HotSpot plus the GC stress gauntlet.

bench ratio bench ratio
stringBuilding 0.67x arrayRandom 0.96x
arraySequential 0.82x intArithmetic 1.07x
quicksort 0.92x longArithmetic 1.12x
hashMapChurn 0.95x objectAllocation 1.19x
mathTranscendental 0.96x recursion 1.60x

intArithmetic/longArithmetic run at exact pure-C parity (verified against same-flags C controls); the residual is C2-vs-clang scheduling of the dependency chain, not VM overhead. recursion is HotSpot's speculative inlining, accepted.

What the emitted code looks like, before and after

1. Frameless codegen (recursion 4.6x -> 1.6x, feeds everything else)

Every Java method used to push a GC-visible frame of type-tagged slots and route every intermediate value through it:

/* BEFORE: one call frame per invocation, every value tagged + memory-resident */
JAVA_LONG fib(CODENAME_ONE_THREAD_STATE, JAVA_INT n) {
    stack = pushFrameOnThreadStack(threadStateData, locals=2, stack=4);
    memset(stack, 0, 6 * sizeof(elementStruct));     /* per CALL */
    locals[0].type = CN1_TYPE_INT; locals[0].data.i = n;
    (*SP).type = CN1_TYPE_INT; (*SP).data.i = 2; SP++;   /* push constant 2 */
    if (locals[0].data.i < SP[-1].data.i) ...            /* compare via memory */
    releaseForReturn(threadStateData, ...);              /* frame pop */
}

Methods proven safe (no try/catch, no synchronization; object roots covered by the conservative native-stack scan) now compile to plain C:

/* AFTER: locals are C locals -> registers; no frame, no tags, no memset */
JAVA_LONG fib(CODENAME_ONE_THREAD_STATE, JAVA_INT n) {
    JAVA_INT ilocals_0_ = n;
    CN1_FRAMELESS_SOE_GUARD(0);            /* stack-overflow check only */
    if (ilocals_0_ < 2) return ilocals_0_;
    return fib(threadStateData, ilocals_0_ - 1) + fib(threadStateData, ilocals_0_ - 2);
}

2. Diverging array checks (quicksort 1.23x -> 0.92x)

The bounds-check helper used in fused comparisons returns a dummy after throwing, so its cold path rejoins the loop. That put a reachable call inside every loop cycle, and clang must assume a call clobbers memory:

/* BEFORE: while (a[i] < pivot) i++;  -- the scan loop of quicksort */
label_scan:
    if (cn1_array_element_int(ts, locals[0].data.o, i) >= pivot) goto done;
    /*  ^ on bounds failure: throwException(...); return 0; ...and CONTINUE.
        The call is reachable on every iteration, so clang RELOADS
        array->data AND array->length every pass: 3 loads per element. */
    i++;
    goto label_scan;

In frameless methods the failure path now throws and returns from the method (the same pattern the stack-overflow guard uses), so no cycle of the loop contains a call and the header loads hoist:

/* AFTER: throw path diverges; loop body is load/compare/branch */
label_scan:
    { JAVA_OBJECT a = locals[0].data.o; JAVA_INT idx = i;
      CN1_ARRAY_CHECK_DIVERGE(a, idx, );   /* null/oob -> throw; return; */
      if (((JAVA_ARRAY_INT*)(*(JAVA_ARRAY)a).data)[idx] >= pivot) goto done; }
    i++;
    goto label_scan;

Measured on the sort alone: 216ms -> 164ms, vs HotSpot's 197ms.

3. Compact HashMap: no entry objects (hashMapChurn 36x -> 0.95x, with the box cache)

BEFORE  put(k,v):  table[i] = new Entry(k, v, hash, table[i]);   // heap alloc per put
        get(k):    e = table[i]; while (e && !eq(e.key,k)) e = e.next;  // pointer chase
        clear():   table = new Entry[n];   // N dead Entry objects for the GC

AFTER   storage:   meta[] (int: 0=empty, 1=tombstone, else spread-hash|MSB)
                   keys[] / vals[] (parallel arrays)
        put(k,v):  linear-probe meta[] comparing plain ints; store into 3 arrays  // zero alloc
        get(k):    same probe; values live in cache-adjacent array slots
        clear():   three array wipes; nothing for the collector

LinkedHashMap keeps its ordering as two parallel int link arrays (prev/next slot indices) over the same storage. The hot five operations (get/put/remove/containsKey/clear) run as C natives probing the raw array data.

4. Fused objects: @Fused (String, StringBuilder, annotatable user classes)

BEFORE  new String(chars, off, len):
            heap object #1: the String
            heap object #2: its char[] value array
        -> two allocations, two sweep slots, a pointer dereference between them

AFTER   one block: [ String header | fields | char[] header | c0 c1 c2 ... ]
        -> one allocation, one sweep slot; the child has no independent GC
           identity (interior pointers resolve to the owner); the constructor's
           field-init is rewritten keep-if-null, so reflection / oversize
           fallback / delegating ctors still work unchanged

5. Allocation fast path + init-before-publish (objectAllocation 20x -> 1.19x)

/* BEFORE: every new */
o = malloc(size); memset(o, 0, size);        /* zero everything...       */
placeObjectInHeapCollection(o);              /* O(n) slot search, lock   */
init header; run ctor;                       /* ...then overwrite most of it */
/* AFTER: inlined at the allocation site (BiBOP size-class bump) */
CN1BibopPage* p = bibopCurrent[SIZE_CLASS];   /* compile-time class index  */
o = page_slot(p, p->bumpIndex++);             /* pointer bump              */
/* NO body zeroing: the inlined ctor writes every field, and the class
   pointer is stored LAST -- until that store, the parentCls==0 guard keeps
   a signal-stopped GC scan from tracing the half-built body. */
o->field1 = arg1; o->field2 = arg2;
o->parentCls = &class__Foo;                   /* PUBLISH */

Dead pages whose every slot is garbage are reclaimed O(1) (the page flips back to bump-from-zero) instead of per-slot sweeping.

6. Escape analysis: non-escaping StringBuilders live on the C stack

javac lowers "item-" + i + '/' + n to new StringBuilder().append(...)...toString(). A CFG walk proves the builder reference is only ever the receiver of StringBuilder calls (append returns this, so the alias is tracked through chains, re-stores into the same local, and the ternary-in-argument diamonds javac emits). Proven sites:

/* AFTER: one struct + buffer per SITE, reused across loop iterations */
struct obj__java_lang_StringBuilder __cn1stk_17;                  /* C stack */
long long __cn1stkbuf_17[CN1_FUSED_ARR_BYTES(32, CHAR) / 8];      /* C stack */
...
/* NEW: init header, install array header into the stack blob, point
   value at it -- the keep-if-null ctor keeps it. Appends write into
   stack memory. The ONLY heap allocation of the whole concatenation
   is the result String (one fused block). */

GC safety falls out of the conservative native-stack scan: if the buffer grows onto the heap, the replacement pointer sits in scanned stack memory.

7. Devirtualization + call-site intrinsics

/* BEFORE: JIT-opaque indirect call for every virtual invoke */
virtual_java_lang_String_hashCode___R_int(ts, obj);   /* vtable dispatch */

/* AFTER (closed world, no reachable override): direct call, ThinLTO can inline */
java_lang_String_hashCode___R_int(ts, obj);

/* AFTER (hottest methods): renamed to an inlined fast path with the
   out-of-line native as its cold fallback -- semantics single-sourced */
static inline JAVA_INT cn1InlStrHash(CODENAME_ONE_THREAD_STATE, JAVA_OBJECT s) {
    JAVA_INT h = ((struct obj__java_lang_String*)s)->java_lang_String_hashCode;
    if (h != 0) return h;              /* cached hash: two instructions */
    ... inline 4-way reassociated compute, result cached ...
}

The same round removed the enteringNativeAllocations() bracket (four flag stores on every native call) under conservative roots, where the native stack is scanned and the bracket protects nothing: string-building floor 27.1ms -> 20.4ms from that alone.

GC

Non-moving BiBOP heap with concurrent mark/sweep; conservative native-stack root scanning (default-on) with generation-counted signal-stop; parallel marking; the snapshot's page-table sort is cached (the page registry is grow-only, so the sorted order only changes on registration).

Two real trigger bugs found and fixed (exposed by churn workloads, affect production): allocationsSinceLastGC was an int accumulating bytes -- GB-per-cycle workloads wrapped it negative, isHighFrequencyGC() returned false, and the GC slept its 30s idle wait while dead pages ballooned into the GB range; and cn1BibopMaybeGc skipped its 24MB trigger entirely in nativeAllocationMode, so workloads allocating only inside natives never collected.

Dynamic pacing cap -- high-throughput threads are never starved. The BiBOP backpressure paces a fast allocator against the concurrent collector: past a fixed 72MB uncollected-volume cap, the allocating thread parked in a sleep spin until the collector caught up. On an allocation-heavy render (vector-map tile decode) the EDT spent most of every GC cycle parked, serializing the render behind the collector -- measured as the single biggest tunable cost (20% of wall; the mark/sweep passes themselves overlap the mutator and measure ~0%). The cap is now dynamic: baseline 1/8 of available RAM (never tighter than the old cap), and high-throughput threads -- the EDT, or any thread allocating heavily this cycle -- get up to 1/2 of available RAM of headroom so they keep running while the concurrent GC catches up. Available memory counts reclaimable pages (free + inactive + purgeable; iOS/macOS free_count alone under-reports by an order of magnitude), snapshot once per cycle.

Real-workload render: vector-tile decode (MvtBench)

A faithful repro of the VectorMap first-render (a real bundled MVT tile decoded 400x through the protobuf -> layers/features/geometry pipeline, holding a rolling window of live tiles), interleaved best-of runs on Apple M2:

build wall
master ParparVM 3.73s
this PR 1.77s 2.1x faster than master
Java 25 warmed (ref) 0.36s remaining gap is cross-TU call inlining, tracked below

Two fixes beyond the suite work got it there: the dynamic pacing cap above, and no-signal-mask setjmp on Apple -- Darwin's setjmp/longjmp save and restore the caller's signal mask, a sigprocmask syscall on each side, and every Java try-block entry compiles to a setjmp, so exception-scoped hot loops paid a kernel round-trip per iteration (~19% of this workload's mutator samples). The VM never changes the signal mask per frame, so Apple platforms now use _setjmp/_longjmp (glibc already omits the mask; Windows has none) -- this speeds up every try/catch-containing hot path on iOS/tvOS/watchOS/macOS. The residual 5x vs warmed HotSpot is the per-byte readByte() cross-TU call chain (HotSpot inlines it; measured: ThinLTO recovers a further 15% and the GC accounts for <6% -- follow-up codegen work, not GC).

Correctness fixes found along the way (all real bugs)

  • Thread.start/join visibility race (alive flag set on the wrong thread).
  • Use-after-free in the conservative root-snapshot build.
  • Apple signal-stop register capture was 56 bytes of pointer, not the register file: the stop-signal handler did memcpy(gcSigRegs, ucv, sizeof(ucontext_t)) where ucv is the handler's ucontext* -- on Darwin that copies the small ucontext header (56 bytes), not the 816-byte machine context it points to, so the conservative scan of a signal-stopped thread read garbage "registers" and could miss a register-only-reachable object -> swept while live (surfaced as an intermittent tvOS EDT NPE inside paintDirty, corrupting the next frame). Now copies the GPR block (uc_mcontext->__ss) -- and only the GPRs, keeping NEON noise out of the scan.
  • StringBuilder.setLength expansion never zero-filled within capacity (masked by the old copy-on-write share).
  • StringBuilder.charAt/getChars were capacity-bounded instead of count-bounded (JDK contract), fixed in C natives and JS-port twins.
  • Non-ObjC String.toUpperCase/toLowerCase were stubs returning this.
  • setjmp/longjmp UB in the try/catch codegen (pre-existing, latent for years): restoreTo<label> is assigned at try-entry -- AFTER the setjmp -- and read in the catch handler AFTER a longjmp; C11 makes it indeterminate there. gcc register-allocates it, so the handler restored threadObjectStackOffset from a rolled-back register and every callee frame after a caught exception was allocated ON TOP of the current frame's locals. Every clang build worked by luck (clang spills). Found via the musl CI job (the only gcc-compiled platform in CI) hanging deterministically; reproduced locally with gcc-16 (FusedTest segfault, bit-identical at -O0); fixed with volatile on the two try-entry variables. This plausibly affected every gcc-built Codename One Linux app that ever caught an exception.
  • Trivial-accessor inlining: a multi-arg setter whose body stores only arg1 folded to a PUTFIELD, stranding the extra argument on the operand stack (now requires exactly one arg); and the fold's visibility depended on class emission order via in-place instruction-list mutation, emitting a field reference without its header include (now resolves through forwarder chains, order-independent).

Benchmark fix

Bench.stringBuilding previously built a string, read hash+length, and dropped it -- a shape where HotSpot's escape analysis scalar-replaces a String that real code would keep. Measured head-to-head: consume-and-drop 1.49x vs escaping 1.14x (pre-fix). The benchmark now parks each string in a ring buffer that outlives the iteration (batch-consumed, every string still hashed exactly once), so both VMs materialize every String -- measuring string building rather than EA-vs-no-EA.

Benchmark suite (in this PR)

The complete performance + correctness suite is included under vm/benchmarks/:

export JDK_8_HOME=/path/to/jdk8
export BENCH_JAVA=/path/to/jdk25/bin/java   # reference JVM (default: `java`)

vm/benchmarks/run-benchmark.sh      # interleaved best-of-5 vs the host JVM, ratio table + geomean
vm/benchmarks/run-gauntlet.sh       # correctness gate: all tortures byte-identical + GC stress
                                    # in cooperative AND forced-signal stop modes
CN1_BENCH_CC=gcc-16 vm/benchmarks/run-gauntlet.sh   # the gcc leg (what caught the setjmp bug)

The harness refuses to print ratios if any checksum differs from the host JVM — divergence is a VM bug by definition, never a perf trade. The README documents each workload and the torture coverage.

Binary size & memory

Same app (Bench), same flags (-O3, ThinLTO), master vs this branch, Apple M2:

metric master this PR Java 25 (ref)
binary size (Bench app) 434 KB 451 KB (+3.8%)
binary size (Noop, VM floor) 1040 KB 1043 KB (+0.3%)
no-op RSS floor 2.2 MB 2.4 MB ~40 MB
peak RSS, Bench (allocation churn) 1.4–2.1 GB 290–390 MB 508 MB

The master peak-RSS blowup is the allocationsSinceLastGC int-overflow bug this PR fixes (the GC slept its 30s idle wait while dead pages accumulated); with the fixed triggers, RSS under heavy churn is bounded below the reference JVM's. The +17 KB binary cost buys the intrinsics, the compact HashMap and the escape-analysis machinery.

API surface

  • @Fused is the one new public annotation (applied internally to String/StringBuilder; usable on developer classes with encapsulated primitive buffers). The developer guide's performance chapter now documents it — contract, example, and the automatic optimizations (stack-allocated string building, tagged integers, devirtualization, compact collections, BCE).
  • @StackAllocate was removed from the public API before merge: nothing applies it, and its contract (no instance ever escapes its creating frame) depends on every caller — something no reusable class can promise. The machinery remains as the engine behind the automatic, per-call-site-proven StringBuilder stack allocation.
  • Tagged integers are now default-on for 64-bit-pointer targets (opt-out -DCN1_DISABLE_TAGGED_INT; auto-disabled on 32-bit pointers incl. Apple Watch). Writing the benchmark scripts exposed that the old opt-in flag was set by NO shipping config — deployed apps never had it (hashMapChurn 2.8x untagged vs 0.97x tagged).
  • Review fix: the charAt intrinsic (and the pre-existing native + JS twin) now bound by the string's logical count rather than the backing array's capacity; regression case added to StrCmp.

Validation

Every commit was gated on:

  • Bit-identical output vs HotSpot for Bench (10/10 runs, plain and ThinLTO) and the torture suites: MapTorture (10 sections incl. tombstone churn, view removal, 200k PRNG op mix), SbTorture (toString independence, editing ops, surrogates, 100k PRNG mix), StrCmp (unicode + surrogate ordering), FusedTest, IbpTest, ThreadChurn -- plain AND forced-signal-stop mode.
  • GcStress 20/20 plain + 10/10 forced-signal; MtStress 10/10 plain+signal.
  • JS-port parity maintained: every new native has a runtime binding delegating to the pure-Java *Impl twin.

Escape hatches for bisection: -DCN1_DISABLE_SB_STACK_ALLOC, CN1_DISABLE_SCALAR_REPLACE, -Dcn1.frameless*, CN1_GC_SIGNAL_STOP env.

CI portability + JS-port hardening (follow-up commits)

The branch was developed and validated on macOS (Darwin exposes GNU/BSD APIs by default); CI flagged the gaps, fixed in two follow-up commits:

  • Linux: _GNU_SOURCE for pthread_getattr_np/REG_* ucontext indices (glibc+musl); -flto=thin gated on Clang (gcc rejects the thin spelling).
  • Windows: signal-stop handler compiled out on _WIN32 (cooperative stop path only); the compat shim gained pthread_once, pthread_detach, posix_memalign (_aligned_malloc -- the page arena never frees, so the pairing rule is moot), PTHREAD_COND_INITIALIZER, and a processor-count fallback without sysconf. Found via a full static POSIX audit rather than iterating on first-error-wins compiles.
  • JS port: the tagged-int Integer.cn1Value/valueOf(int) natives got their runtime bindings; and the pure-Java *Impl twins that bindNative delegates call from parparvm_runtime.js are now retention roots in both the unused-method cull and the JS RTA -- no bytecode call site exists, so they were being eliminated and the delegation threw ReferenceError (caught by the new core-slice completeness tests). All 233 JS-target tests pass locally.
  • Two BytecodeInstructionIntegrationTest assertions were stale against deliberate emission changes (indy concat now stack-allocates its builder; frameless supersedes the fast-stack macro) -- modernized to accept every current form while guarding the same contract.
  • The full gauntlet (FusedTest/ExcTest/MapTorture/SbTorture/Bench/GcStress) now validates bit-identical under both clang and gcc-16 -O3 -- the VM had been clang-only-validated, and gcc's register allocator is what exposed the setjmp bug.

🤖 Generated with Claude Code

shai-almog and others added 30 commits June 27, 2026 14:19
…ry GC, tagged Integer

A body of AOT performance work, all gated/validated against bit-identical
checksums vs Java SE and the clean-C test path. Off by default where flagged.

- Small-value box caches for Integer/Long/Short/Character (valueOf -128..127),
  eliminating autoboxing allocation in tight loops.
- Bounds-check elimination: prove-safe pass for the canonical induction loop
  (ArrayLoadExpression/ArrayLengthExpression/Instruction), unlocking SIMD.
- Inlining of trivial monomorphic accessors (Invoke).
- Conditional-volatile locals (BytecodeMethod): emit `volatile` only when a
  method has try/catch/synchronized/calls, letting clang register-allocate and
  vectorize call-free compute loops (3.6x on array reduce, no regressions).
- Thread-local non-moving nursery GC behind -DCN1_NURSERY (cn1_globals.*,
  nativeMethods.m): in-place promotion, write barrier, adaptive survival-based
  bypass, block-lifecycle free-stack fix; main thread made lightweight so the
  concurrent GC pauses it. 2x on objectAllocation, off by default.
- Tagged small-Integer "poor man's Valhalla" behind -DCN1_TAGGED_INT, 64-bit
  pointers only (auto-off on armv7/armv7k/arm64_32): Integer.valueOf returns an
  immediate tagged pointer, GC ignores it, CN1_CLASS_OF substitutes Integer's
  class in dispatch/instanceof, value reads route through a tag-aware native,
  monitor ops NOP. Plus an inline tagged hashCode/equals dispatch fast path for
  collections. 2x on hashMapChurn (GC eliminated), bit-identical to HotSpot.
- Opt-in LTO flag (ByteCodeTranslator) for release/perf builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…owering

makeConcatWithConstants/makeConcat are desugared to a synthetic StringBuilder
helper. Pre-size that StringBuilder from the recipe literals + per-argument
length estimates so the common-case concat never grows its char[] (each growth
is a fresh array + arraycopy). Over-estimates are harmless; under-estimates
still grow correctly. Verified bit-identical to HotSpot on a concat microbench.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-teaming

A comprehensive edge-case test (getClass, isInstance, instanceof, equals across
tagged/heap/null/non-Integer, compareTo via TreeMap, all Number methods,
HashMap/HashSet/TreeMap/ArrayList, Arrays.sort, switch, concat, synchronized,
MIN/MAX_VALUE) crashed the -DCN1_TAGGED_INT build in four places the original
benchmark never exercised. All were native/codegen paths dereferencing a tagged
pointer's (nonexistent) object header:

- Object.getClassImpl: read this->header -> tag-aware (returns Integer.class).
- Class.isInstance(obj): read obj->header -> CN1_CLASS_OF + null guard.
- String equals-family: read arg->header->classId -> CN1_CLASS_OF(arg).
- Interface dispatch (e.g. Comparable.compareTo via TreeMap): the classId index
  read this->header->classId -> CN1_CLASS_OF (ByteCodeClass interface vtable gen).
- CN1_CLASS_OF itself: a plain ternary let clang if-convert and SPECULATIVELY load
  the faulting tagged header before the tag test (crash with no inline fast-path
  guard, e.g. interface compareTo). Reworked to select a valid object pointer
  first (a static JavaObjectPrototype proxy whose header is Integer's class), so
  the single header load is always on a dereferenceable address.

Result: full edge-case test bit-identical across default / tagged / tagged+nursery,
and the Bench suite still matches HotSpot with no regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The inner chain walk (findNonNullKeyEntry) and key equality (areEqualKeys, with a
pointer-== fast path that already short-circuits tagged-int keys) were already
native. But get still went through translated-Java wrappers: get -> getEntry ->
computeHashCode(key.hashCode()) -> findNonNullKeyEntry. Collapse those into one C
function; for a tagged Integer key the hashCode is an inline untag via the
dispatch fast path. Bit-identical to the Java getEntry path (EdgeTest
default==tagged, full edge matrix). ~1.25x on hashMapChurn (6858 -> 5471ms, 20
reps), general (helps the default build too, not gated). First step of the
native-collection-fast-path work: the algorithm in C beats HotSpot 3.5x at the
ceiling, so collapsing the remaining wrappers (put) and ultimately open-addressing
storage is the path to parity/better.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same pattern as native get: collapse put/putImpl/computeHashCode into one C call,
reusing the native chain walk and the Java createHashedEntry/rehash slow path. The
only store this owns is entry.value = value, which carries an explicit
CN1_WRITE_BARRIER (the Java version emitted one). Bit-identical (EdgeTest
default==tagged unchanged, 8424060826785033831).

hashMapChurn (20 reps, tagged): 5471 (get-only) -> 3952ms with put too; 6858 ->
3952 = 1.74x from native get+put. Now ~6.6x behind HotSpot (598ms), down from
~26x at session start. Remaining gap is the per-key Entry allocation (chaining) +
createHashedEntry/rehash; open-addressing storage is the next lever (the C ceiling
with no Entry objects beats HotSpot 3.5x).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
append(int)/append(long) were `append(Integer.toString(i))` -- a temporary String
(plus its char[]) allocated on every call. Replace with native methods that write
the decimal digits straight into the builder's char[] (digits generated in
negative space so INT/LONG_MIN don't overflow). No per-append allocation. General
(not gated). Validated bit-identical to HotSpot on a string-building microbench
(append String/int/char/long chains + toString), which is now ~7.2x behind HotSpot
(the ~13x tier). The char append/String append/charAt/getChars were already native.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
clear()/removeEntry() now recycle entries onto a free list (cn1FreeList, a
GC-marked field; key/value nulled to release refs) instead of dropping them to
GC, and createHashedEntry pops from the pool before allocating. After the first
fill, churn patterns (fill/clear loops, add/remove steady state) allocate nothing
-- the case a generational nursery can't help because the entries escape into the
map. origKeyHash made non-final so pooled entries can be re-keyed.

hashMapChurn (20 reps, tagged): 3952 -> 1782ms (2.2x). Now ~2.9x behind HotSpot
(620ms), down from ~26x at session start (tagged ints -> native get -> native put
-> entry pool). Validated: EdgeTest default==tagged unchanged, 8/8 GC stress,
checksum bit-identical to HotSpot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
toString() previously always allocated a fresh String + copied the char[]. Now it
SHARES the buffer with the returned String (via the offset/count String ctor) and
sets `shared`. append() stays untouched -- it only writes beyond the String's view
or reallocates via enlargeBuffer (which clears `shared`), so it's safe to share.
Only the editing mutators (setCharAt/insert/delete/deleteCharAt/reverse/setLength)
copy-on-write via cn1Unshare(). The copy-on-write scaffolding was already designed
(commented out); this wires it through cn1Unshare().

Validated: a toString-then-mutate test (setCharAt/insert/delete/reverse/setLength,
re-checking earlier Strings) is bit-identical to HotSpot; string-building bench
bit-identical and 2191 -> 1541ms (~7.2x -> ~4.4x behind HotSpot); EdgeTest AOT
unchanged. General (not gated) -- every toString in the system avoids a copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Methods that make calls couldn't use the fast leaf frame (the stack trace must
keep their frame), so they paid a NON-INLINE initMethodStack() on entry and
releaseForReturn() on exit -- two function calls per invocation, brutal for hot
recursive/call-dense code. initMethodStack's only extra work vs the fast path is
recording the class/method id (two array writes for the trace). Move both to
static-inline (cn1InitMethodStackInline keeps the name recording; releaseForReturn
inlined) so the C compiler folds the offset arithmetic and the call overhead is
gone. Also adds the threadObjectStack-overflow guard the fast path already had.

recursion 6.66x -> 4.89x, hashMapChurn 4.6x -> 3.95x, quicksort/objectAllocation
slightly better; compute unchanged (already inline via the fast frame). Bit-
identical to HotSpot, EdgeTest unchanged. Broad: helps every call-dense method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POP_INT/POP_LONG/POP_OBJ used a non-inline pop(&SP) -- a function call for a
pointer decrement, hit on every pop including hot return paths (return
POP_LONG()). Make it static inline. Broad, helps all stack-popping code.
Bit-identical (EdgeTest unchanged, fib result matches HotSpot).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…jects

Annotation-driven escape elimination (the AOT-correct replacement for the
nursery, which was a synthetic win + ~10% universal write-barrier tax). A class
marked @com.codename1.annotations.StackAllocate has each `new` lowered to a
method-scoped C struct instead of codenameOneGcMalloc: no malloc, no heap
registration, no GC mark/sweep -- the object dies with the frame. Intended for
internal short-lived value/temporary types where non-escape is known by
construction (the developer asserts it; violating it dangles).

Mechanics:
- StackAllocate: TYPE-target, CLASS-retention marker annotation.
- Parser detects it at class level -> ByteCodeClass.stackAllocatable.
- BytecodeMethod pre-scans each method and declares one frame-scoped
  `struct obj__T __cn1stk_<site>;` per @StackAllocate NEW site (reused across
  loop iterations -- only one instance per site is live at a time).
- TypeInstruction NEW replicates exactly what __NEW_T does (run the static
  initializer, set the same header fields codenameOneGcMalloc sets) but SKIPS
  heap registration, so the sweep never visits it. Its pointer rides the operand
  stack, so the GC still reaches it as a root and scans its fields -- any heap
  objects it references stay live.

Tax-free and opt-in: codegen only diverges when stackAllocId>=0, so non-annotated
code is byte-for-byte unchanged.

Validated:
- 60M-iteration non-escaping temporary (Vec2): 4.51x faster than the heap path
  (45x -> 10x behind HotSpot), bit-identical checksum vs heap build and HotSpot.
- GC red-team: a @StackAllocate Holder owning a heap Payload with System.gc()
  forced mid-loop -> bit-identical to HotSpot, no premature collection, no crash
  (proves the GC marks through the stack object).
- Full parparvm-bench suite (zero annotations) still bit-identical to HotSpot.

Residual 10x vs HotSpot is the per-iteration memset + header init + operand-stack
traffic that full scalar replacement (object -> field locals) would remove next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…registers

Builds on the @StackAllocate stack-alloc foundation (d4185da). A primitive-only
@StackAllocate object used as a simple local temporary is now turned into a pure C
local struct whose address is NEVER taken, so clang's SROA promotes its fields to
registers and the object vanishes -- matching what HotSpot's escape analysis does,
which the prior stack-alloc path could not because the struct's address escaped to
the GC-scanned operand stack (measured: that escape alone cost 2.4x).

Transform (a conservative, bail-on-doubt pass in BytecodeMethod.optimize()):
recognize NEW X; DUP; <args>; INVOKESPECIAL X.<init>; ASTORE n where
 - X is @StackAllocate, a DIRECT Object subclass, primitive-only instance fields,
   no <clinit> (so dropping super.<init>/static-init is sound, and there are no
   heap refs the GC must scan -> the object need never be a GC root);
 - X.<init> is exactly Object.<init> + a param->field bijection (every field
   assigned exactly once from a distinct ctor param of matching type) -- analyzed
   by srAnalyzeCtor, else bail;
 - local n is used ONLY as ALOAD n; GETFIELD X.f (srValidateLocalUses: any other
   use -- pass/return/PUTFIELD/second store/type-confusion -- bails);
 - the arg region has no nested NEW/<init>/stack-shuffle/branch, else bail.
Then: NEW emits nothing (no header/memset/PUSH); DUP and ASTORE are dropped;
INVOKESPECIAL <init> becomes ScalarAllocInit, which folds the (already reduced)
arg expressions straight into __cn1sr_<id>.field = <expr> (or, if an arg isn't a
pure expression, falls back to popping the operand stack in order -- both are
stack-balanced); GETFIELD on local n becomes direct __cn1sr_<id>.field. Anything
not matching keeps today's GC-safe stack-alloc codegen. Off-by-default escape
hatch: DISABLE_SCALAR_REPLACE.

Validated (independently rebuilt + re-run, not just the implementing agent):
- SA (60M non-escaping Vec2 long-field temporaries): generated work() has 0
  get_field/PUSH_POINTER/__NEW/Vec2___INIT (struct register-promoted), checksum
  bit-identical to HotSpot, 528ms -> 120ms (4.40x faster than stack-alloc).
- SA2 (Holder with a HEAP Payload field, System.gc() forced mid-loop): primitive-
  only gate BAILS (0 __cn1sr_), keeps stack-alloc, bit-identical, no crash. The
  critical GC-safety gate.
- Full parparvm-bench suite (51 checksums, zero annotations): all bit-identical to
  HotSpot. Scalar replacement is a clean no-op on un-annotated code.

Residual vs HotSpot (2.35x) is ambient ParparVM frame/line scaffolding
(__CN1_DEBUG_INFO per-source-line stores), orthogonal to object handling -- the
object-elimination win is fully realized (the hand-C floor for this loop is 36ms,
below HotSpot's 51ms).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…torize it)

A Java long comparison `a < b` compiles to LCMP (three-way -1/0/1) + IFxx, which
the translator emitted as `CN1_CMP_EXPR(a,b) <op> 0` -- a `(a==b)?0:(a>b)?1:-1`
chain compared to zero. clang cannot recover the loop trip count through that, so
long-counted loops were neither analyzed nor vectorized. Measured: it was THE
residual on the scalar-replaced @StackAllocate benchmark -- replacing it with a
direct comparison was 2.07x and took that loop from 2.35x HotSpot to parity.

Fix: when an LCMP ArithmeticExpression feeds an IFxx branch-on-zero, emit the
direct `(a <op> b)` instead (ArithmeticExpression.getLongCompareDirect, used in
the IFxx branch-fusion in BytecodeMethod). Long only -- float/double (FCMPx/DCMPx)
keep CN1_CMP_EXPR because their NaN ordering differs from a direct C comparison.
Safe and bit-identical: the folded operands are pure (the reducer only folds
loads/constants/pure expressions), so `(a<op>b)` evaluated once equals
`CN1_CMP_EXPR(a,b)<op>0` for every long value -- and avoids the macro's
double-evaluation of each operand. General: helps every long-counted loop, not
just @StackAllocate.

Validated (bit-identical to HotSpot):
- Long-edge test: all 6 operators (< <= > >= == !=) over {Long.MIN, MAX, -1, 0,
  1, MIN+1, MAX-1} (81 pairs) -- checksum identical, fusion fired (0 CN1_CMP_EXPR).
- Full parparvm-bench suite (51 checksums) -- all identical.
- SA (scalar-replaced Vec2 loop) -- identical, 120ms -> 56ms = 1.08x HotSpot
  (was 2.35x); SA2 unaffected, identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-source-line __CN1_DEBUG_INFO store (callStackLine[frame] = line) was the
last hot-path cost keeping tight loops out of registers -- it was the entire
residual on the scalar-replaced @StackAllocate benchmark (56ms -> 40ms once gone).

A frame's reported trace line is only ever read at a capture/throw/call site, and
every such site lives on a line that calls, allocates, or does a throwing op
(field/array/div/new/athrow). A line whose every instruction is non-throwing and
non-calling (primitive arithmetic, local load/store, constants, compares,
branches, conversions) can therefore NEVER be the line a trace reports -- so
eliding its store is trace-IDENTICAL, not a line-number regression.

Implementation:
- BytecodeMethod.analyzeElidableLineInfo() marks each LineNumber whose source line
  has no throwing/calling instruction (canThrowOrCall(): conservative -- default
  keep; only an explicit non-throwing whitelist is elidable; numeric/String LDC
  and a scalar-replaced NEW are non-throwing; integer div/rem, array/field/static
  access, invoke, new*, athrow, checkcast, monitor are kept). Runs AFTER scalar
  replacement so a scalar-replaced object's now-pure NEW/<init>/field access is
  seen as non-throwing.
- LineNumber emits the elidable store as __CN1_DEBUG_INFO_NT, which is the full
  store under the on-device debugger (which steps line-by-line and needs every
  line) and a no-op in release/device builds -- where it removes the only per-line
  cost. Throwing/calling lines keep __CN1_DEBUG_INFO, so the reported line is
  always live and exact.

Validated:
- Full parparvm-bench suite (51 checksums) bit-identical to HotSpot -- execution
  unchanged; the elision applies to every method with no regression.
- SA (scalar-replaced Vec2 loop): all hot lines elide, checksum bit-identical,
  release 56ms -> 40ms = 0.62x HotSpot (BELOW the JIT, at the hand-C floor).
  SA2 (object field, gc() forced) bit-identical.
Note: empirical printStackTrace trace validation is blocked in the standalone
`clean` target by a PRE-EXISTING trace-builder crash on null constant-pool strings
(both elision-on and elision-off segfault identically -- unrelated to this change);
trace-identity rests on the construction argument above + bit-identical execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parallelizes the transitive mark DRAIN across a persistent worker pool while
leaving codenameOneGCMark's per-thread park / root-snapshot logic unchanged, so
snapshot-at-the-beginning (mark all of a thread's reachable set before releasing
it) is preserved. Marking was already type-specialized (per-class markFunction,
leaf types skipped); this adds the parallelism.

- gcMarkObject parallel path claims unmarked->marked with an atomic CAS
  (__sync_bool_compare_and_swap); only the winner pushes. force/recursionKey
  re-scan stays entirely on the serial path (force is never set in parallel).
- Worklist: shared array under a mutex; each worker pops a 64-entry batch and
  buffers produced children in a __thread-local buffer, flushing in batches
  (broadcast wakes idle workers). Termination: a worker idles only when the
  shared worklist is empty AND its local buffer is flushed; the last worker to
  idle sets gcMarkDone. Overflow still falls back to the serial heap-rescan
  fixed point; the nursery promote path and force re-scan stay serial.
- The __thread worklist-buffer pointer doubles as the "am I a parallel worker?"
  discriminator: when NULL (the GC thread between drains, N=1, overflow rescan)
  gcMarkObject/push take the ORIGINAL serial code verbatim -- no atomics, no lock.
- CN1_GC_MARK_THREADS overrides the marker count; default min(4, ncpus-1) at
  runtime; N=1 is byte-for-byte the previous behavior (no pool, no atomics).

Validated: full parparvm-bench suite bit-identical to HotSpot at N=1 AND N=4;
serial==parallel checksums identical; ThreadSanitizer clean on all introduced
mark-state synchronization (the remaining TSan reports are the collector's
pre-existing, inherent non-STW collector-vs-mutator reads -- unmodified HEAD
shows the same class of reports); GC stress (millions of objects, ~120 GCs/run)
stable and identical across 5 runs.

Measured-as-a-whole impact (vs serial mark, min-of-reps): objectAllocation
306->280ms (1.09x), everything else within noise. Marking is ~19.5% of the
GC-bound time and the bench's live-set-per-GC is small, so the whole-suite gain
is modest; the bulk of the GC gap (allocation fast path + concurrent-collector
throttle) is the next, larger target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ects

Replaces per-object calloc + allObjectsInHeap registration + per-object free()
with a non-moving segregated-fits (BiBOP) page heap for small non-array objects.
Arrays and objects > MAX size class keep the verbatim legacy aligned calloc +
allObjectsInHeap path -- so real array offsets, stable addresses, and SIMD/GPU
alignment are untouched. NON-MOVING (objects never move -> no write barrier, no
pointer fix-ups / shadowing -- the reasons generational was rejected do not apply)
and NON-GENERATIONAL (whole-heap collect).

Design:
- 64KB posix_memalign'd pages, one size class each (15 classes 32..512B; >512 or
  arrays -> legacy path). Every slot >=16-byte aligned.
- Allocation: per-thread (__thread) current page per class; pop the page free-list
  else bump the cursor (lock-free, thread-local). Page full -> retired to a global
  SWEEP stack (atomic CAS push); grab a fresh/partial page from the pool (one
  bibopMutex acquisition per page, not per object). Slot re-zeroed + header set
  exactly as codenameOneGcMalloc. Small objects are NOT registered in
  allObjectsInHeap -- the pages track them.
- Liveness: the existing per-object epoch mark (__codenameOneGcMark) stays the
  single source of truth, so gcMarkObject + the parallel mark pool + the proven
  grace semantics (mark==-1 grace, mark<cur-1 dead) are UNCHANGED and work
  uniformly on page slots and legacy table objects. No per-page bitmap, no
  address->page table.
- Sweep: rebuild each retired page's free-list from its slot headers (finalizers
  still run); an all-dead page returns to the pool. Then the existing
  allObjectsInHeap sweep handles large/array objects.

The three correctness hinges:
1. Allocate-during-GC: a fresh slot is mark==-1 (one-cycle grace) AND lives on the
   thread's OWNED current page, which the concurrent sweep never touches (only
   retired pages, owner==0, are swept).
2. Sweep vs alloc: a page has exactly one role at a time -- OWNED (one thread
   allocates, never swept) -> retired to the SWEEP stack -> swept (owner==0) ->
   FREE/PARTIAL pool. The sweep snapshots the stack via atomic_exchange. No page
   is ever allocated-into and swept simultaneously.
3. No page-table race: dissolved -- header marking needs no address->page lookup;
   the append-only all-pages registry (release/acquire) is read only by the
   overflow rescan, and only at a slot whose atomically-read mark == current cycle.

Escape hatch: #ifndef CN1_DISABLE_BIBOP (default ON); -DCN1_DISABLE_BIBOP reverts
to the verbatim legacy collector. Independent of CN1_NURSERY (kept off).

Validated (macOS arm64): full parparvm-bench suite bit-identical to HotSpot with
BiBOP ON, -DCN1_DISABLE_BIBOP, and across 1/4/8 mark workers and forced worklist
overflow (-DCN1_GC_MARK_WORKLIST_SIZE=256, exercising the page rescan). TSan: zero
races on any BiBOP state (pages/pools/free-lists/registry/cursor) -- 111 reports
vs the legacy baseline's 119, all the pre-existing collector-vs-mutator object-
header family. GC-stress + 4-thread allocate-during-GC stress: checksums identical
across runs and to legacy/HotSpot (a single lost live object would diverge). RSS
24-26% LOWER than legacy and bounded over 2000 rounds (pages recycled, no drift).

Measured as a whole vs warmed Java 25 (+AOT cache), min-of-reps:
objectAllocation 278->144ms (1.93x; 15.1x->7.8x vs Java25), stringBuilding 1.14x,
hashMapChurn 1.05x, compute/arrays unchanged. Whole-suite geomean vs Java25
2.26x -> 2.08x, zero regression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x->1.6x HotSpot)

The per-call frame bookkeeping -- DEFINE_METHOD_STACK's per-call memset of the
locals+operand-stack elementStruct region, the callStackOffset bump/check, the
releaseForReturn offset restore, and the per-line __CN1_DEBUG_INFO stores -- is
pure overhead. A method that holds ZERO object references in its frame contributes
no GC roots, so the precise collector has nothing to scan there and the frame can
be eliminated outright. No GC change, no operand-stack rewrite (an SSA-temp
rewrite was measured NOT to help and was skipped); instruction bodies stay
byte-identical, so this is bit-identical by construction.

- isFramelessEligible() (BytecodeMethod): conservative whitelist on raw bytecode --
  static, primitive-or-void return, no object args/locals, no object operand-stack
  value, no try/catch, not synchronized/native/on-device-debug, and every opcode in
  the handled primitive set (loads/stores/consts/arithmetic incl. throwing div-rem/
  shifts/bitwise/conversions/compares/branches/switch/dup-pop-swap/returns +
  INVOKESTATIC with a purely primitive/void descriptor). Anything else -> ineligible
  -> byte-identical legacy codegen.
- DEFINE_METHOD_STACK_FRAMELESS (cn1_globals.h): the operand stack is a method-local
  C array (not a threadObjectStack slice) -- no per-call memset, no offset
  bookkeeping, no callStack push; emits CN1_FRAMELESS_SOE_GUARD.
- CN1_FRAMELESS_SOE_GUARD: frameless methods don't bump callStackOffset, so deep
  non-tail recursion is guarded by comparing __builtin_frame_address(0) to a lazily
  cached per-thread nativeStackLimit (pthread_get_stackaddr_np - stacksize + 256KB
  band; 8MB frame-anchored fallback) -- throws StackOverflowError instead of SIGBUS.
  __builtin_expect hints are load-bearing (177->147ms without/with).
- Return sites (BasicInstruction x5 + optimize()'s two return fast-paths) emit plain
  return with no releaseForReturn; LineNumber suppresses __CN1_DEBUG_INFO for
  frameless methods (no callStackOffset to index). Gate: -Dcn1.frameless (default ON);
  OFF emits byte-identical-to-HEAD code.

Validated: full Bench suite bit-identical to HotSpot frameless ON and OFF; OFF
byte-identical generated C to HEAD; 11 methods frameless in the suite. Deep
non-tail recursion throws StackOverflowError, not SIGSEGV. Measured vs warmed
Java 25+AOTcache: recursion 436->150ms = 2.92x faster (ON vs OFF), 4.64x -> 1.59x
HotSpot; every other benchmark within noise (no regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… methods (opt-in)

Phase 3b of the conservative-collector endgame: extends frameless codegen from
primitive-only methods (committed 0260fe8) to OBJECT-BEARING methods, with the
conservative native-stack scan as a real GC root source. A frameless object method
keeps its object refs in native C locals / a method-local operand-stack array
(no DEFINE_METHOD_STACK frame, no threadObjectStack, no per-call memset); the GC
finds those roots by conservatively scanning the thread's native C stack. Enabled
by the non-moving BiBOP heap (conservative scanning requires non-moving). Gated:
#ifdef CN1_CONSERVATIVE_GC_ROOTS (the runtime) + -Dcn1.frameless.objects (the
codegen); DEFAULT OFF -- the default build is byte-identical to HEAD (precise GC +
primitive-only frameless). The proven path (P1 resolver / P2 native-stack scan /
P3a zero-miss root-placement) is now production, not validation.

- cn1ConservativeResolve(word)->object base|NULL: BiBOP page-aligned candidate +
  all-pages-registry binary search + interior pointers + large/array extents; marks
  for real (cn1ConservativeMarkRange).
- HYBRID GC: codenameOneGCMark keeps the precise threadObjectStack scan for legacy
  frames AND conservatively scans each stopped thread's native stack [sp,base) +
  register snapshot for frameless frames; explicit roots (currentThreadObject,
  statics, constant pool, pending native allocations) retained. The conservative
  scan covers the whole native stack, so the legacy<->frameless caller/callee
  boundary is never a gap.
- Universal thread-stopping: cooperative (CN1_GC_PARK_CAPTURE setjmp + SP at every
  safepoint, proven) for lightweight threads; signal-based (SIGUSR2 + ucontext SP/reg
  capture) for genuine native threads, opt-in (CN1_GC_SIGNAL_STOP).
- Object-frameless eligibility extends the whitelist to ALOAD/ASTORE, GETFIELD/
  PUTFIELD/GET-PUTSTATIC, NEW/ANEWARRAY/CHECKCAST/INSTANCEOF, array ops, all invokes
  (args as explicit C params), ACONST_NULL/IF_ACMP*/IFNULL, String/Class LDC.
  Excluded: try/catch, ATHROW, MONITOR*, MULTIANEWARRAY -> stay legacy. Instruction
  bodies byte-identical (win is frame elimination, not re-lowering).

Validated (CN1_CONSERVATIVE_GC_ROOTS + -Dcn1.frameless.objects): full Bench suite
bit-identical to HotSpot (72 frameless methods: 12 primitive + 60 static object);
default (gates off) byte-identical to HEAD; GcStress 25x and 4-thread MtStress 30x
== HotSpot with bounded RSS (no leak); the transient ⊇ self-check (CN1_CONSERVATIVE_
GC_SELFCHECK) reports MISS=0 (every precise root also resolved conservatively).
GcStress 5x re-confirmed == HotSpot here.

HONEST STATUS:
- PERF-NEUTRAL today: the frame-elimination win is offset by an UNOPTIMIZED
  conservative scan (the heap-membership snapshot is rebuilt O(heap) per-thread-per-
  GC). The once-per-GC optimization (born-marked new BiBOP objects) is the next step
  to make object-frameless a net win on GC-heavy code; recursion's win is preserved
  (no GC in the loop). That's why this ships OPT-IN, default off.
- INSTANCE-method frameless (-Dcn1.frameless.instance) and the SIGNAL-stop path have
  intermittent multi-thread races (DONE 0 / ~8-10%) NOT root-caused -> gated OFF.
  The static + cooperative path (what's validated above) is solid (30/30 MT).
- Conservative GC is incompatible with CN1_NURSERY (deprecated); frameless methods
  don't appear in callStack-based stack traces (printStackTrace doesn't crash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hread)

java.lang.Thread.alive was set to true inside java_lang_Thread_runImpl, which runs
on the WORKER thread asynchronously after start() returns. java_lang_Thread_start__
only did pthread_create. So a thread doing start() then join() could race: join()
-> isAlive() reads false (worker not yet scheduled) and returns IMMEDIATELY, before
any of the worker's writes were published -- e.g. main summing a worker-filled
results[] array could read it still zero. Classic "started-state not set
synchronously by the starting thread" bug; present on every port, ~15% repro in a
4-thread join-then-read stress (vs HotSpot fully deterministic).

Fix: set the alive flag synchronously on the CALLING thread, in program order before
the worker is spawned, in java_lang_Thread_start__. A later join() then correctly
blocks until the worker clears alive under the monitor (runImpl:
synchronized{ alive=false; notifyAll(); }), and that monitor release/acquire is the
happens-before edge that publishes the worker's writes. Purely additive
synchronization; bit-identical to HotSpot on the full Bench suite. MtStress
3/20-failing -> 50/50 deterministic == HotSpot after the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rameless

Flip the Phase-3b gates to default ON (arm64-validated -- the dev machine is Apple
Silicon arm64, same arch as the iOS device target; CI validates the other ABIs):
- cn1_globals.h: #define CN1_CONSERVATIVE_GC_ROOTS by default (disable with
  -DCN1_DISABLE_CONSERVATIVE_GC_ROOTS).
- BytecodeMethod: cn1.frameless.objects + cn1.frameless.instance default true.

The instance-frameless multi-thread failure that previously gated it was the
pre-existing Thread.start/join visibility race, fixed in 9933311. Default build
now: 302 frameless methods (was 12 primitive-only), bit-identical to HotSpot, no
per-call frame on object/instance methods, roots found by the conservative
native-stack scan. Validated: full Bench suite bit-identical; GcStress 5x ==
HotSpot, no crash/leak. Cooperative thread-stop covers Java threads (what the bench
exercises); native-thread coverage via the signal path (CN1_GC_SIGNAL_STOP) stays
the edge for CI/on-device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…LINE_ALLOC)

The build ships no LTO, so __NEW_<X> and codenameOneGcMalloc live in separate
translation units and clang cannot inline them: every escaping new-site pays two
real cross-TU calls (confirmed in asm). CN1_FAST_NEW(X) inlines the BiBOP
per-thread bump common case at the allocation site (pointer-bump + header stamp,
size-class index folded to a compile-time literal via CN1_BIBOP_CIDX), falling
back to __NEW_<X> only on page-full / free-list / oversized / ineligible. The
bump replicates cn1BibopAlloc bit-for-bit (relaxed bumpIndex load, mark released
last, cursor release-stored after slot init) so the concurrent-GC correctness
argument is unchanged. bibopCurrent[]/bibopBytesSinceGc + struct CN1BibopPage
are lifted to the header for the inline; the .m keeps a _Static_assert that the
size-class array still matches.

Gated -DCN1_INLINE_ALLOC, default OFF (pending iOS on-device validation of the
statement-expression macro, as with the conservative GC). With the flag off
CN1_FAST_NEW(X) expands verbatim to __NEW_<X>, so the default build is byte-
identical.

Validated (arm64 macOS): full Bench bit-identical to HotSpot both OFF and ON;
GcStress 20/20 and MtStress 10/10 (4-thread alloc-during-GC) == HotSpot, no
leak. Measured ON vs OFF: objectAllocation 107.9->79.0ms (-27%, 5.4x->3.94x vs
warmed Java25), stringBuilding 61.2->51.5ms (-16%); compute/arrays within +/-1%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alloc fast-path tier 2)

Stacks on the inlined BiBOP bump (CN1_INLINE_ALLOC) to close more of the
escaping-allocation gap. Two independently-gated levers:

Lever B (-DCN1_INLINE_CTOR): after CN1_FAST_NEW allocates, the constructor was
still a separate out-of-line cross-TU call. InlinableConstructor analyses a
constructor for an inlinable shape (only this/param field stores + a chained-
inlinable super ctor, bounded instruction count, no INVOKE except that super,
no alloc/throw/branch/loop/try) and the new-site emits the field stores inline
instead of the call. Emitted as an `#ifdef CN1_INLINE_CTOR` in the generated C
(both branches present), so with the flag off the original call compiles and the
build is byte-identical. Constructor args are consumed from the operand stack;
the object is already GC-reachable and its ref fields were zeroed by the bump,
so the inline stores need no extra barrier (this VM has none).

Lever A (-DCN1_DEATOMIC_BYTES): the per-allocation `atomic_fetch_add` on the
global bibopBytesSinceGc becomes a plain per-thread accumulator
(ThreadLocalData.bibopBytesLocal) flushed in bulk at page-acquire and thread
death. bibopBytesSinceGc feeds only the GC-trigger heuristic (no liveness role)
and is already raced today, so deferring it only shifts the trigger cadence by
< nthreads*page, negligible vs the 24MB trigger. The bump cursor and mark
publication ordering -- the GC-visible fields -- are UNCHANGED.

Both default OFF, alongside CN1_INLINE_ALLOC, pending iOS on-device validation.

Validated (arm64 macOS): full Bench bit-identical to HotSpot for every flag
combination (off / L1 / +A / +B / +A+B); GcStress 10/10 and MtStress 10/10
(4-thread alloc-during-GC) == HotSpot on the +A+B config, no leak. Interleaved
(thermal-drift-cancelling) objectAllocation: off 171.9 -> L1 126.9 -> +B 80.1
-> +A+B 71.4 ms (2.4x speedup; each lever stacks). hashMapChurn flat (its cost
is hashing/clear, not allocation) and stringBuilding modest (char[] arrays use
the legacy path). Net: objectAllocation ~5.7x -> ~2.7x warmed Java25; compute/
arrays unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lightweight pending array)

cn1GcBuildRootSnapshots() reads every thread's pendingHeapAllocations array to
add not-yet-migrated objects to the conservative-resolve extent table. It runs
on the GC thread before the thread being scanned is parked, so threads other
than the current one are still RUNNING. A lightweight thread grows its pending
array lock-free in codenameOneGcMalloc / cn1AddPending (malloc tmp; memcpy;
free(old); pending = tmp) -- the pre-existing guard took threadHeapMutex only
for non-lightweight (native) threads. So the GC could read
pendingHeapAllocations[j] exactly as free() reclaimed the array: the garbage
word is taken as a heap-extent base and cn1ConservativeResolve returns it
unvalidated -> SIGBUS in gcMarkObject. Rare (~1% under timing perturbation) but
real, and it reaches default builds (CN1_CONSERVATIVE_GC_ROOTS is default-on).

Fix: serialize the grow-and-free against the snapshot read. The two realloc
fast paths now take threadHeapMutex unconditionally (lightweight included, like
the native path already did), and cn1GcBuildRootSnapshots takes the SAME mutex
around its pending-read loop. The lock is acquired and released entirely within
the read, before the caller signal-stops any thread, so no thread is ever frozen
mid-realloc holding it (no deadlock); ordering vs lockCriticalSection is never
inverted (the migration path takes criticalSection THEN threadHeapMutex; this
path takes only threadHeapMutex). This mirrors the existing pending-migration
code (715-740), which already reads pending under threadHeapMutex for native
threads / while lightweight threads are parked. The per-element store stays
lock-free -- that read is benign (an aligned 8-byte slot holds 0 or a complete
valid pointer; no free involved).

Validated (arm64 macOS): ThreadSanitizer on HEAD deterministically reports the
race (cn1GcBuildRootSnapshots reading pending vs codenameOneGcMalloc). With the
fix: full Bench bit-identical to HotSpot (default and -DCN1_INLINE_ALLOC
-DCN1_INLINE_CTOR -DCN1_DEATOMIC_BYTES); MtStress (4-thread alloc-during-GC) 300/300
clean -- 0 crash, 0 deadlock, all checksums == HotSpot -- at a deliberately
widened race window (PER_THREAD_ALLOCATION_COUNT temporarily 16); GcStress 20/20
== HotSpot; no perf regression (objectAllocation/stringBuilding/intArithmetic
within +/-1%). Residual conservative-collector non-STW reads are pre-existing and
by design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…T iOS VM)

The inline BiBOP bump (CN1_INLINE_ALLOC), inline leaf constructors
(CN1_INLINE_CTOR) and de-atomic per-thread byte accounting (CN1_DEATOMIC_BYTES)
were committed behind opt-in -D flags. For an AOT VM whose sole shipping target
is iOS, an off-by-default flag is dead code that never runs in production, and CI
already exercises every ABI. Flip all three to default-on with a
-DCN1_DISABLE_* escape hatch (kept only so CI can A/B and so a platform can opt
out if a real problem surfaces).

Validated (arm64 macOS): the DEFAULT build (no flags) is now bit-identical to
HotSpot across the full Bench suite, GcStress 15/15 and MtStress 15/15 (4-thread
alloc-during-GC) == HotSpot. Perf is the previously-measured strongest config:
objectAllocation ~2.7x warmed Java25 (was 5.7x), compute/arrays at parity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… arena

Two GC-memory changes, both bit-identical to HotSpot, found by profiling
allocation-churn benchmarks (objectAllocation etc.) which were spending their
time in the allocator/collector rather than the mutator.

1. Adaptive allocation pacing. System.gc() used to Thread.sleep(2) on every
   trigger; an allocate-and-drop workload triggers GC every CN1_BIBOP_GC_TRIGGER
   bytes, so that fixed sleep was pure mutator stall (and, crucially, it did NOT
   bound memory -- RSS ballooned to 2.35-7GB run-to-run as the mutator outran the
   collector). Replace it with proportional backpressure in cn1BibopMaybeGc: the
   mutator only waits when uncollected BiBOP volume since the last GC exceeds a
   hard cap (3x the trigger), and waits as a GC SAFEPOINT (threadActive=FALSE so
   the collector can scan/advance past it -- a naive spin livelocks the collector,
   which showed up as an MtStress hang). When the collector keeps up the cap is
   never hit and this never waits. Counter-intuitively the tight cap is also the
   FAST configuration: a small heap keeps the non-generational O(pages) sweep
   cheap, so the collector keeps up and the mutator barely waits; a loose cap lets
   the heap grow and the sweep (hence everything) crawls. Disable: -DCN1_BIBOP_NO_PACING.

2. Batched page arena. cn1BibopNewPage did one posix_memalign(64KB) per page;
   when churn drains the free pool faster than the sweep refills it, every page
   was a separate mach_vm_map kernel trap (profiled ~17% of objectAllocation,
   now 0 in the sample). Carve 64KB pages from a 64KB-aligned multi-page arena
   (one mmap per CN1_BIBOP_ARENA_PAGES=64); pages stay 64KB-aligned, the arena is
   lazily faulted (RSS tracks touched pages), and BiBOP never free()s a page so
   interior pointers are safe. Disable: -DCN1_BIBOP_NO_ARENA.

Result on objectAllocation churn: peak RSS 2.35GB+ (unbounded) -> 275MB (bounded,
~9x), at neutral-to-faster perf (clean idle wall-time equal-or-better; pacing
only engages under allocation pressure, so compute/array benchmarks are
unaffected -- bit-identical). This bounds what was effectively an unbounded-RSS
OOM risk on device. It does NOT close the throughput gap to HotSpot on churn --
that is the non-generational O(pages) sweep vs HotSpot's O(survivors) young gen,
a separate follow-up (O(1) all-dead-page reclaim).

Validated (arm64 macOS): full Bench bit-identical to HotSpot; GcStress 20/20;
MtStress (4-thread alloc-during-GC) 12/12, no hang; RSS bounded over sustained
churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…us pages

The non-generational sweep walked every slot of every retired page (millions of
reads per cycle under allocation churn), so the collector couldn't keep up and
the adaptive pacing throttled the mutator -- objectAllocation was sweep-bound.
Make the sweep skip the per-slot walk for pages whose fate is provable in O(1):

A retired page is "homogeneous" -- safe to decide without walking -- iff
  !gcAllocedSinceSweep  (no fresh mark==-1 grace-candidate slots since last sweep)
  && gcLastMarkedEpoch != V  (nothing on it was marked THIS cycle; a reachable
                              object is always marked, so every occupant is garbage
                              aging through grace)
  && !gcNeedsReclaim     (no survivor class carries a real finalizer)
  && cn1BibopLiveMonitors == 0  (no BiBOP monitor data to free)
For a homogeneous page, gcGraceEpoch (set at each full walk = upper bound on every
survivor's epoch) decides the whole page:
  gcGraceEpoch <  V-1  -> ALL DEAD  -> O(1) reclaim (reset bumpIndex/freeList, to
                                       freePool; byte-identical to the walk's
                                       liveCount==0 outcome, without touching slots)
  gcGraceEpoch >= V-1  -> ALL LIVE (still in grace) -> O(1) skip (route as the walk
                                       would, gcGraceEpoch unchanged so it ages out)
Otherwise the existing full walk runs (and refreshes the per-page facts). New
per-page fields live in struct CN1BibopPage (always present so A/B layouts match);
set on alloc (the bump + free-list paths) and in gcMarkObject (a relaxed,
idempotent epoch stamp -- the marker is parallel). Monitors use a global seq_cst
live-count rather than a per-page flag to avoid cross-thread visibility races.
Gate: -DCN1_BIBOP_NO_FASTSWEEP.

Enabler (required): every class was emitting a non-null finalizerFunction that
just chained to the empty Object finalizer, so a "has finalizer" predicate was
always true and the O(1) path never fired. ByteCodeClass now emits
finalizerFunction = 0 unless a real finalize() exists in the hierarchy (the
__FINALIZER_<class> chain is still emitted, so subclass chaining is intact; both
readers -- freeAndFinalize and cn1BibopReclaimSlot -- already guard ptr != 0).
Behavior-preserving (conservative on unresolved bases) and it also drops millions
of no-op indirect finalizer calls from the existing full-walk path.

Result (arm64 macOS, idle, default-on): 63% of retired pages take the O(1) path;
objectAllocation 75.4 -> 46.5ms (1.62x; ~40% of the gap to warmed Java25 closed),
and on an isolated 20M-Node churn ~1.8x faster at equal-or-lower BOUNDED RSS
(~235MB) -- the pacing throttles far less now that the sweep keeps up. No
regression on compute/array benches.

Validated: full Bench bit-identical to HotSpot (FASTSWEEP on and off); GcStress
(85 runs across dev + here) and MtStress (40 runs, 4-thread alloc-during-GC) with
ZERO checksum divergence -- bit-identical is the oracle that the grace semantics
are preserved. (An intermittent ~4% GcStress segfault is a PRE-EXISTING
concurrent-GC race in the precise threadObjectStack scan -- present in the
pristine baseline at an equal-or-higher rate, an untouched code path -- to be
tracked separately.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nter coarsening)

Profiling the (now sweep-unbound) objectAllocation churn showed the per-object
inline path doing avoidable work. Two removals, both bit-identical:

- Drop the __ownerThread store. It is write-only dead state in the current tree
  (the size-class-index repurposing was an unmerged free-list patch); a full-tree
  scan finds no reader. Removed from both the inlined cn1BibopFastAlloc and the
  slow-path cn1BibopInitSlot. (Field kept for struct-layout stability.)

- Move allocationsSinceLastGC / totalAllocations off the per-object path. These
  feed only the isHighFrequencyGC heuristic (no correctness role) but were two
  GLOBAL stores per allocation -- an L1 store single-threaded, a bouncing cache
  line across threads. They are now bumped in bulk inside CN1_BIBOP_FLUSH_BYTES
  once per page-acquire (~64KB), which is accurate enough for a threshold
  heuristic. (Non-DEATOMIC build keeps the per-object update in ACCOUNT_BYTES.)

Note recorded in-code: the body memset is NOT removable -- skipping it is ~2x
SLOWER because uninitialized ref fields get scanned during the mark==-1 grace
window and retain floating garbage. It is load-bearing, not overhead.

Result: objectAllocation 46.2 -> 44.8ms (~3% single-threaded; larger under
multi-threaded allocation where the global-counter cache line stops bouncing);
now 2.29x warmed Java25. Validated bit-identical to HotSpot (full Bench),
GcStress (no checksum divergence) and MtStress 15/15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fields off-object)

Profiling objectAllocation showed the per-allocation cost is store-bound, and
the object is fat: a 6-field, 48-byte header vs HotSpot's ~16 / 2 fields, so a
Node{int,ref} occupied a 64-byte BiBOP slot -- 2x the bytes to allocate, zero,
and stream through cache on every object. The header writes themselves are NOT
removable (each is GC state; skipping any retains floating garbage and runs 2-3x
SLOWER -- measured). So shrink by RELOCATING fields off the object, not skipping:

- DELETE __ownerThread -- write-only dead state (the size-class-index repurposing
  was an unmerged patch; no reader exists). 48 -> 40.
- __codenameOneThreadData (lazily-attached monitor, null on ~all objects) -> an
  address-keyed monitor side table (cn1MonitorDataGet/Set/Remove, one mutex,
  critical-section->table lock order). monitorEnter/Exit/wait/notify + reclaim/free
  use it; the alloc fast path drops the =0 store. 40 -> 24.
- __codenameOneReferenceCount -> a force-visited side set: its only behavioral use
  was the gcMarkObject force-recursion guard (==recursionKey), now
  cn1ForceVisitedTestAndSet; the 999999 "permanent" writes were vestigial (mark-
  sweep never reads them -- those objects stay live via root marking). The alloc
  fast path drops the =1 store. 24 -> 16.

Header is now {clazz*, gcMark, heapPosition} = 16 bytes (HotSpot-class). Node drops
64->32 byte class (half), HashMap.Entry 80->48.

Validated (arm64 macOS), every phase bit-identical to HotSpot on the full Bench;
GcStress + MtStress (4-thread alloc-during-GC) with ZERO checksum divergence across
150+ stress runs (the ~4% empty-output segfault is the pre-existing threadObjectStack
-scan race, same rate on clean HEAD). Perf (idle, interleaved): objectAllocation
0.80x (3.4x->3.0x warmed Java25), hashMapChurn 0.84x, stringBuilding faster-or-flat,
compute/array flat (relocation costs nothing off the alloc path). RSS is neutral on
average with higher variance (a smaller-slot pacing artifact, tunable separately).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rdening

MEMSET ELIMINATION (init-before-publish, no gate -- this is the pipeline):
For every NEW X; DUP; <args>; INVOKESPECIAL X.<init> site whose ctor is
inlinable (super()==Object, param/const stores only, no finalizer), the NEW
is deferred to a null placeholder and the <init> allocates WITHOUT the body
memset (cn1BibopFastAllocNoZero), stores every ctor-written field, explicitly
zeroes the unwritten ones, and only then publishes the object. Ctor args are
hoisted into C temps in ARGUMENT ORDER before the alloc, which also fixes two
latent bugs in the committed inline-ctor path: a folded call-expression arg
stored to two fields evaluated twice, and args evaluated in ctor-body store
order instead of Java's left-to-right. objectAllocation 1.70x warmed Java 25
(was 5.7x at branch start); all 10 Bench checksums bit-identical to HotSpot.

The elision is made sound against the conservative/signal-stop collector by
deferring parentCls publication: the header keeps parentCls==0 until every
field is written, so a signal-stopped thread's mid-construction object is
skipped by gcMarkObject's existing guard (grace keeps it alive); the sweep's
mark==-1 finalizer probe gets a matching NULL guard and finalizer-bearing
classes keep the memset path.

THREAD-STOP GC HARDENING (bugs found via GcStress under CN1_GC_SIGNAL_STOP=1
and an adversarial review of the branch's GC):
* VALIDATED precise scan: a signal-stopped thread can freeze between a push's
  type/data stores (plain stores clang may also reorder), so a type==OBJECT
  slot can hold a stale primitive -- observed as gcMarkObject(0x4e20) from a
  frozen PUSH_INT window. threadObjectStack words are now resolved against
  the page/extent snapshot exactly like conservative roots.
* Type-before-data ordering in the fused invoke-return emissions (the same
  torn-slot hazard at every call returning into a stale receiver slot).
* Generation-counted signal handshake: a timed-out stop PRE-RELEASES its
  generation and releases are monotonic, so an abandoned or descheduled
  handler can never strand spinning forever.
* gcParkCaptured is cleared for EVERY thread each cycle -- a native thread
  that parked once no longer satisfies useCoop with a stale SP forever
  (missed roots -> UAF).
* GC safepoint in cn1BibopMaybeGc (BiBOP-only allocators never reached the
  legacy park) and the pacing spin now honors threadBlockedByGC on wake so
  the cap can't resume a mutator mid-drain.
* Acquire ordering: conservative resolver's mark load (freelist-header reuse
  window), sweep's bumpIndex load (fresh-slot header visibility), and the
  snapshot builder reads bumpIndex before geometry (page-reformat TOCTOU).
* bibopBytesLocal / nativeAllocationMode initialized in ThreadLocalData
  (malloc'd, never zeroed -- garbage corrupted GC pacing / disabled the
  alloc fast path per-thread).

Validation: GcStress 25/25 cooperative + 25/25 forced-signal (was 20/25 and
14/15), MtStress 20/20 + 10/10 forced-signal, ctor-semantics torture test
(eval order, double-store, throwing args, default zeros, wide args, GC churn
in call-args) byte-identical to HotSpot, full Bench suite bit-identical, no
perf regression on any benchmark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-cycle root snapshot

The global legacy-heap table was grown by DYING threads (markDeadThread ->
collectThreadResources -> placeObjectInHeapCollection) while the GC thread
walks it lock-free (sweep, root-snapshot build, overflow rescan). One growth
concurrent with a sweep loses the sweep's slot-NULLs in the memcpy'd copy --
resurrecting freed pointers for the next cycle to dereference -- and two
growths during one hoisted-pointer walk free the array under the reader (the
old one-growth deferral could not cover that).

Fix: make the table strictly GC-thread-owned. A dying thread now only QUEUES
its ThreadLocalData (critical section already held by markDeadThread); the GC
drains the queue at mark start -- strictly before any table walk or possible
Thread-object finalization -- and performs the TLD free itself when the
finalizer ran while the TLD was still queued (gcReleaseRequested). Objects in
a queued TLD's pending list are invisible to the sweep, so the deferral can
never free them early; un-snapshotted for at most one cycle, they are covered
by the mark==-1 grace rule like every other post-snapshot allocation. With the
single-writer invariant the growth can free the replaced array immediately,
and getStack's one-shot immortal-string removal (the only non-GC-thread table
access) takes the critical section.

Also: build the conservative page/extent root snapshot ONCE PER MARK CYCLE
(epoch-guarded) instead of once per scanned thread -- the full-table walk +
qsort dominated the GC thread on array-heavy workloads (sampled: more time in
qsort/cn1ConsExtCmp than in marking) and stalled mutators parked behind
threadBlockedByGC. Post-snapshot allocations are mark==-1 fresh and survive
via grace whether or not they resolve, so the first build of a cycle is
complete for correctness. recursion 146->127ms; GC CPU burn on string/array
churn cut sharply.

Validation: new ThreadChurn stress (8 dying threads x 12 rounds x 3k pending
arrays + >30000 live arrays forcing table growth under concurrent GC) 15/15 +
8/8 forced-signal, checksum identical to HotSpot; GcStress 20/20+15/15 coop,
10/10+8/8 forced-signal; MtStress 10/10; full Bench suite bit-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shai-almog and others added 30 commits July 7, 2026 06:28
…delta=4 stale baseline)

Same as mac-native: the 2000ms settle wait makes the toast render deterministic; the diff vs
the committed 3840x2160 golden was max_channel_delta=4 (imperceptible sub-pixel/AA), a stale
baseline. Refresh from the tvOS run artifact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… captures

Two simulator screenshot flakes fixed at the source (no flake tolerance):

* ToastBarTopPosition: the toast animates in the GLOBAL layered pane, which the base
  capture's form-AnimationManager settle poll does not track, so the fixed 2000ms timer
  captured the toast mid-slide on the slow iOS/tvOS/watchOS simulators -> the render varied
  run-to-run by delta-191 (toast present vs mid-slide). Fast native platforms settle within
  2000ms (unaffected). Wait 6000ms, comfortably past the slide even on the slowest sim, so
  the captured frame is always the settled toast.

* Metal empty capture: on the GPU/Metal backend getCurrent() can still be un-laid-out
  (width/height 0) a beat after settling, emitting a useless 1x1 image (observed:
  ShowcaseTheme_dark on build-ios-metal). Add a bounded relayout+repaint+retry in the shared
  capture path so an unready frame is never captured.

Sim ToastBar goldens re-baselined from the now-deterministic settled render in a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… capture

With the ToastBar settle fix the iOS-simulator render is now deterministic (settled toast);
the old golden was of the mid-slide/older state. Re-baseline from the deterministic capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Earlier re-baselines captured runs where the toast had not appeared (racy toast-show), which
would validate the ABSENCE of the toast the test checks. Restore master's correct with-toast
goldens; the show is made reliable in the test instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	docs/developer-guide/languagetool-accept.txt
…capturing

Root cause of the ToastBarTopPosition flake across all simulators: the toast is shown
asynchronously and intermittently is NOT visible when the fixed settle timer fires, so the
screenshot captures the form WITHOUT the toast -- a 'toast present vs absent' run-to-run flake
(max_channel_delta 191) that no golden can reconcile (the master goldens correctly include the
toast). Replace the fixed timer with a poll on the toast component's real state (via the form's
'ToastBarComponent' client property -> isVisible + laid-out height), re-issuing the show if it
never took, then hand off to the base settle+capture. Bounded (15s) so a genuine show failure
degrades to a capture rather than hanging. Deterministic -> matches the with-toast goldens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gradients flake)

build-ios-watch intermittently failed css-gradients because the screenshot captured the
PREVIOUS test's form (harness flagged 'duplicate_image_with=PaletteOverrideTheme_dark') -- a
form-switch/late-present race on the slow watchOS Core Graphics backend.

* BaseTest.awaitSettledThenCapture: treat 'the form we intend to shoot is not the current
  form' as not-yet-settled, so the capture waits until the correct form is actually current
  (bounded by the existing 5s cap). Systemic guard against wrong-form captures.
* CssGradientsScreenshotTest: override extraSettleBeforeCaptureMillis (repaint + 700ms) so the
  heavy 8-tile gradient grid is freshly presented before capture, not a leftover framebuffer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The intermittent Windows-arm64 screenshot-capture crash (UNHANDLED EXCEPTION 0xC0000005 at
VARYING graphics tests -> memory corruption) already logs a full RtlVirtualUnwind backtrace,
but only as bare rva=... offsets, so it could not be root-caused from CI. Symbolize each frame
IN-PROCESS in the unhandled-exception handler via dbghelp SymFromAddr/SymGetLineFromAddr64
against the /Zi .pdb that sits next to the running exe, so cn1windows-native.log now carries
'stack[i]=<pc> rva=<rva> <function>+<off> (<file>:<line>)' for every frame -- making the crash
diagnosable from the uploaded artifact. Links dbghelp in the generated CMake. If Sym init fails
(corrupt heap) the raw rva line still symbolizes offline against the committed .pdb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…use-after-free)

The symbolized Windows-arm64 crash pinned it: the cn1ss WebSocket reader crashes in
WindowsSocket.SocketInput.read reading a corrupt buffer. socketRead computes data =
buffer.data, then CN1_YIELD_THREADs across the blocking recv so the concurrent GC can run.
Only the interior  pointer is used afterward, so the optimizer is free to drop the
buffer array object -- and the GC, scanning this parked thread, finds no root to it and sweeps
it mid-read -> use-after-free (0xC0000005, access=read of a corrupt heap pointer, varying
crash sites). Force the buffer object to stay live across the recv via a volatile sink so the
conservative stack/register scan of the parked thread resolves and protects it. Applied to the
Windows (crashing) and Linux (identical yield pattern) ports; socketWrite does not yield so it
is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eep its read chain

The symbolized crash recurred after the recv-buffer keepalive, still in
WindowsSocket.SocketInput.read reading a freed buffer -- so the freed object is the read
CHAIN held across the loop (impl -> in -> buffer), not just the recv scratch. The reader
thread parks in socketRead's blocking recv (which yields the thread), the concurrent GC runs,
and the conservative native-stack scan misses the frameless `this` receiver on that parked
thread, sweeping the whole WindowsWebSocketImpl -> in (SocketInput) -> buffer chain; read()
then dereferences the freed buffer (0xC0000005, access=read of a corrupt data pointer,
intermittent ~50%). Pin every impl with a live reader thread in a static Set -- a guaranteed
GC root that keeps the chain (and its buffer) reachable independent of the conservative scan;
removed in a finally when the reader exits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hreads are scannable

ROOT CAUSE of the intermittent Windows-arm64 crash (symbolized to a use-after-free in the
cn1ss WebSocket reader's SocketInput.read): CN1_YIELD_THREAD -- used to park a thread across a
native blocking call (socketRead's recv, etc.) -- only set threadActive=FALSE; it did NOT run
CN1_GC_PARK_CAPTURE. Under CN1_CONSERVATIVE_GC_ROOTS the concurrent GC scans a parked
lightweight thread's native stack ONLY through that cooperative capture (SP + callee-saved
register snapshot). Every other platform has a signal-stop fallback, but WINDOWS DOES NOT (see
cn1GcScanThreadNativeStack), so a thread parked in recv was NEVER scanned -> every root on its
stack (the freshly-allocated read buffer, the SocketInput, ...) was swept mid-read -> the
0xC0000005. Earlier symptomatic fixes (recv-buffer keepalive, static-pin the reader impl) only
reduced it because they didn't restore the thread's scannability.

Fix: CN1_YIELD_THREAD now CN1_GC_PARK_CAPTUREs before publishing threadActive=FALSE, and
CN1_RESUME_THREAD clears gcParkCaptured on wake so a stale capture is never reused. This makes
EVERY native-blocking yield scannable on Windows (and lets other platforms use the cheaper
cooperative path instead of a signal-stop); a no-op when conservative roots are off. Gauntlet
bit-identical to HotSpot (GcStress) -- no GC-correctness change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…servative-GC UAF)

The intermittent Windows-arm64 0xC0000005 in WindowsSocket.SocketInput.read on the
cn1ss WebSocket reader thread is a use-after-free: the concurrent conservative GC
sweeps the freshly-allocated byte[8192] read buffer while the reader is parked inside
the native blocking recv(). Windows has no signal-stop fallback, so the parked reader
relies purely on the cooperative stack scan; and protecting the buffer indirectly via
the impl->stream->buffer chain depends on mark-completeness that proved fragile under
load (recv keepalive, static impl pin, and CN1_YIELD_THREAD park-capture each only
reduced the crash rate).

Fix: allocate the read buffer once per socket, reuse it across every blocking read, and
hold it as a DIRECT GC root in a static set for the socket's lifetime. This removes both
the per-read allocation window and the chain dependency, so the buffer can never be
reclaimed mid-read regardless of scan timing. readChunk() reads into the pinned scratch
then copies out a right-sized result (allocated after recv returns). Unpinned on close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e target selection

Cherry-picked (hand-ported) two self-contained, low-risk fixes from PR #5343 to
improve CI determinism on this branch:

- java.time.format.DateTimeFormatter / DateTimeSupport: fix pattern parse/format and
  raw timezone-offset computation (fixes the TimeApiTest DateTimeParseException
  'Formatter does not produce LocalDateTime' surfaced on the iOS/tvOS suite).
- scripts/ios/create-shared-scheme.py: choose the primary iOS .app target and exclude
  the TV/watch auxiliary app targets when generating the shared scheme, so the iOS
  build/destination resolution is unambiguous.

Deliberately NOT taking #5343's stricter assertion-failure gating (still stabilizing)
or the tvOS missing-golden changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-test)

The DateTimeFormatter/DateTimeSupport fix ported in 35328bb was not self-contained:
its new offsetFromInstant path (newCalendar(tz) + local-epoch subtraction) computes DST
offsets correctly on JavaSE but diverges on ParparVM, whose TimeZone/Calendar DST handling
differs -- so TimeApiIntegrationTest.timeEdgeCasesMatchBetweenJavaSEAndParparVM (a gating
vm-test that asserts ParparVM output MATCHES JavaSE) failed at DST boundaries
(02:30-04:00 vs 01:30-05:00). The full fix in #5343 also carries a companion ParparVM
native timezone change (nativeMethods.m) that conflicts with this branch's GC work, and the
only thing this grab addressed was an assertion-only screenshot test that does not gate the
build. Reverting to master's java.time; keeping the unrelated create-shared-scheme.py grab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build-ios-tv regressed to 11 'missing' screenshots on this branch even though the app
delivered every frame (each logs png_bytes). Root cause: the capture wait-loop treated a
flat frame count for 10 ticks (~80s) as a hung suite and broke early -- but heavy tvOS tests
(4K frames, network-backed VectorMapShapes which took 2.5min here) legitimately render for
minutes and bunch at the end of the run, so the count stalled at 128 while the suite was
still working. The loop settled at 128/139 at 472s; the app then delivered the final 11
frames over the next ~2min, after the loop had already exited (master delivers steadily and
settles 139/139). Raise the no-progress threshold to ~6min (>> the observed 2.5min worst-case
single-test gap) so SUITE:FINISHED stays the primary done-signal and MAX_WAIT the hung-suite
backstop; a genuine hang still bails well before MAX_WAIT.

Note: a separate, intermittent GC use-after-free NPE in CodenameOneImplementation.paintDirty
corrupts SwitchTheme_dark on tvOS and is tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n the signal-stop handler (fixes tvOS paint UAF)

The GC signal-stop handler scans a copy of the interrupted thread's registers
(gcSigRegs) as conservative roots. It did memcpy(gcSigRegs, ucv, sizeof(ucontext_t)),
whose comment claimed it captured the 'inline mcontext'. That holds on Linux/glibc, but
on Apple ucontext_t.uc_mcontext is a POINTER to the register file, not an inline struct:
sizeof(ucontext_t) is only 56 bytes (the header + that pointer), so ZERO GPRs were
captured. The real register file is 816 bytes behind the pointer (verified: uc_mcsize=816).

Consequently, on iOS/tvOS, an object reference live only in a register when a thread is
signal-stopped was invisible to the conservative scan and got swept. The EDT paints via
frameless codegen, which keeps hot object refs (Style/Font/Image) in callee-saved
registers across the native draw calls from paintComponent; when the concurrent GC
signal-stopped the EDT mid-paint, such a ref was missed and reclaimed -> the intermittent
NullPointerException at CodenameOneImplementation.paintDirty:875 that corrupted
SwitchTheme_dark on build-ios-tv (register-residency-dependent, hence intermittent).

Fix: on Apple, dereference uc_mcontext and copy the pointed-to mcontext (uc_mcsize bytes,
holding __ss = x0-x28/fp/lr/sp/pc) into gcSigRegs so the registers are scanned. Linux keeps
the inline copy (GPRs already at the start of the ucontext). Conservative register scanning
can only ADD roots, never introduce a sweep, so this is safe by construction.

Validated: struct sizes confirmed (56 vs 816); gauntlet MtStress 15/15 and GcStress 10/10
bit-identical to HotSpot under CN1_GC_SIGNAL_STOP=1 (all threads signal-stopped), no crashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ndler, not NEON/FP

Follow-up to the register-capture fix (193d95d). That fix copied the whole Apple mcontext
(816 bytes on arm64) into the conservative-scan buffer. But object references only ever live
in general-purpose registers -- never in the NEON/FP vector state (__ns, 528 of those 816
bytes) or the exception state (__es, 16 bytes). Feeding 528 bytes of float/SIMD register data
to the conservative scan as candidate 'pointers' pins spurious garbage and bloats the live
heap, which makes GC heavier and more frequent on allocation-heavy paths -- observed as a
worsening of the vector-tile (VectorMap) render time on tvOS after the register fix landed.

Scan only __ss (272 bytes: x0-x28/fp/lr/sp/pc on arm64, rax..r15/rip on x86_64). This still
captures every object root (the UAF fix is intact) while cutting the false-positive surface by
~65%. Validated: gauntlet MtStress 12/12 and GcStress 12/12 bit-identical to HotSpot under
CN1_GC_SIGNAL_STOP=1, no crashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cycle qsort)

The conservative root resolver (address->object lookup used to turn raw stack/register
words into GC roots) was undocumented. Add an architecture block explaining its two backing
structures and, specifically, why the legacy/array extent index (cn1ConsExt) is rebuilt and
qsort()ed every mark cycle while the BiBOP page registry is cached:

  - BiBOP pages are a GROW-ONLY registry, so their base-sorted array is cached and only
    re-sorted when the page count changes.
  - allObjectsInHeap[] (arrays + non-BiBOP objects) is NOT grow-only: sweep tombstones a
    slot to NULL and a later allocation refills that slot with a different object at a
    different address, so neither the slot->address map nor the sorted order is stable
    across cycles -> the extent index must be rebuilt+sorted each cycle.

Also records the measured perf cost on the vector-map MVT workload (parparvm-bench MvtBench,
~213k live legacy objects) and the ranked optimization directions (incremental cn1ConsExt;
grow-only array arenas; inlined sort), all of which are correctness-critical (a resolver
miss is a use-after-free) and must be validated against MvtBench + the GcStress/MtStress
gauntlet. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… the render thread)

The BiBOP backpressure paces a fast allocator against the concurrent collector: when
uncollected volume exceeds a FIXED cap (3x the GC trigger = 72MB) the allocating thread parks
in a usleep spin until the collector catches up. On an allocation-heavy render (decoding a
vector-map tile) the high-throughput thread -- the EDT -- is throttled for most of each GC
cycle, so the render is serialized behind the collector instead of overlapping it. Measured on
a faithful local repro (parparvm-bench MvtBench, a real MVT tile decoded 400x): the fixed cap
costs ~20% of wall (matching a full CN1_BIBOP_NO_PACING A/B), and it is the single biggest
tunable cost -- the belt/grace/qsort/array-region levers all measured 0% because they overlap
the mutator, whereas this park is on the mutator's critical path.

Fix (per the 'mark high-throughput threads that must never be starved, tune dynamically' idea):
replace the fixed cap with cn1BibopPacingCap() --
  * baseline: up to 1/8 of AVAILABLE RAM of uncollected slack, floored at the old static cap so
    no workload gets a tighter bound than before;
  * high-throughput threads (the EDT, or any thread whose since-last-GC legacy allocations
    exceed CN1_BIBOP_HIGH_THROUGHPUT_ALLOCS) get up to 1/2 of AVAILABLE RAM -- they keep running
    while the concurrent GC catches up instead of parking.
Bounded by REAL available memory so RSS stays safe; the collector reclaims the transient churn.
Free memory is snapshot once per GC cycle (cn1RefreshFreeMemCache) so there is no per-alloc
syscall. Adds cn1_available_memory() (free + inactive + purgeable reclaimable pages) SEPARATE
from get_free_memory so the existing heap-threshold sizing is untouched.

Validated locally: MvtBench 20% faster + bit-identical (DONE 24468000); gauntlet GcStress/
MtStress/MapTorture/SbTorture 5/5 bit-identical to HotSpot under forced signal-stop. Pacing is
alloc-timing only (no mark/sweep correctness change). Purely a throughput/scheduling fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…yscall per entry)

Darwin's setjmp/longjmp save and restore the caller's SIGNAL MASK -- a sigprocmask SYSCALL on
each side. Every Java try-block entry compiles to a setjmp (DEFINE_CATCH_BLOCK), as does every
synchronized method's monitor block and every GC park capture, so an exception-scoped hot loop
pays a kernel round-trip per iteration. Profiling the MVT vector-tile decode (the VectorMap
render repro) showed sigprocmask at ~19% of the mutator's samples, called from the decode
loop's try scopes.

The VM never changes the signal mask per frame, so no try frame depends on the mask being
restored on unwind: switch Apple platforms to the no-mask _setjmp/_longjmp via CN1_TRY_SETJMP/
CN1_TRY_LONGJMP. glibc's plain setjmp already omits the mask and Windows has no signal mask,
so non-Apple keeps the standard names -- zero behavior change there. All five setjmp sites and
the single longjmp thrower (throwException) switch together so buffers and jumps always use
the same variant; the GC's register-capture setjmps only need the callee-saved register flush
into the jmp_buf, which _setjmp performs identically (parks get cheaper too). Also caches the
per-allocation getenv(CN1_LEGACY_DEBUG) in the legacy alloc hot path (showed up as
__findenv_locked in the same profile).

Measured (MvtBench, interleaved A/B, arm64 macOS): 1.916s -> 1.614s, 16% faster on top of the
dynamic-pacing fix -- cumulative 2.53s -> 1.61s (36%) on the VectorMap-render repro, benefits
every exception-scoped hot path on iOS/tvOS/watchOS/macOS. Bit-identical output; gauntlet
GcStress/MtStress/MapTorture/SbTorture 12/12 bit-identical to HotSpot under forced signal-stop;
Bench suite 50/50 checksums identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sideways capture)

Root cause of the recurring build-ios-metal failure (identical signature on 65865f2 and
d08dcd6, so predating today's VM commits): OrientationLockScreenshotTest restores portrait
after its landscape capture with a poll that GIVES UP SILENTLY after 8s and reports done. On a
starved runner the rotate-back overruns that budget, the simulator stays landscape for the rest
of the suite, and the failure lands on whichever innocent test captures next --
VideoIODecodedFrames composes its frame grid at the host form's size and shipped a 2556x1179
landscape grid against a portrait golden (exit 15), while the orientation-sensitive tail tests
wedged (missing).

Three layers, so the leak can neither happen silently nor propagate:
- OrientationLock's restore leg gets its own 20s budget, RE-ASSERTS the portrait lock every ~2s
  (a lock issued while the previous rotation animation is in flight can be swallowed -- one call
  is not enough), and logs CN1SS:WARN on exhaustion so a genuine restore failure attributes to
  OrientationLock instead of a mystery mismatch downstream.
- BaseTest gains a baseline-orientation guard shared by both capture paths: the suite's
  orientation is recorded at the first capture; any later capture attempted while the device has
  leaked out of it is treated as not-settled (15s budget) while the guard actively re-asserts
  the portrait lock -- the suite self-heals instead of failing.
- AbstractAnimationScreenshotTest (the grid-composition path VideoIODecodedFrames uses, which
  bypasses the settle poll) checks the same guard before sizing the grid.

The intentionally-landscape VR/360 tests (VRStereoScene, Media360Panorama) opt out via
allowNonBaselineOrientationCapture(); OrientationLock's own landscape emit bypasses the guard
by construction. No goldens change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(Linux suite truncation)

The orientation-leak fix (5bcaf25) regressed the Linux leg: restorePortrait polls until the
device is portrait, but Linux/desktop windows are landscape BY CONSTRUCTION and canForceOrientation
is false -- the check can never pass, so the poll burned its full 20s budget (the old code
silently burned 8s on the same dead wait). That widened the suite's longest no-new-screenshot
stretch (the ~30 non-rendering API tests between the last theme screenshot and DesktopMode) past
the Linux harness's 150s stability window; the harness force-killed the app mid-run and the last
three screenshot tests (DesktopMode/VRStereoScene/Media360Panorama) went missing on arm64 --
gate rc=17.

Two fixes:
- restorePortrait short-circuits when !CN.canForceOrientation(): nothing rotated, nothing to
  restore -- finish immediately. Linux/Mac/tvOS/browser now skip even the old 8s dead wait;
  phones keep the full 20s re-locking restore.
- The Linux harness stability window widens 150s -> 300s: it was single-digit seconds above the
  arm64 assertion-test stretch even before this regression. Healthy runs never pay it
  (SUITE:FINISHED breaks the loop first); only a genuinely wedged suite waits longer, still
  bounded by the 40-minute hard cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dows screenshots; sync picker goldens

Three related fixes surfaced by review of the un-gated Windows screenshot comparisons:

1. REAL RENDERING BUG -- the Windows port never clamped a clip to the paintDirty flush
   region ('the dirty rectangle is advisory'). A component that sets an oversized clip during
   a partial repaint paints past the flushed sub-region into the PERSISTENT Direct2D surface
   (RETAIN_CONTENTS on-screen, the WIC bitmap offscreen), overwriting pixels that stay stale
   until a full repaint -- the same defect as iOS Metal #5273 and the Linux Cairo port before
   its fix. graphics-partial-flush-clip-escape was designed to catch exactly this and DID:
   the capture shipped an all-magenta frame (escaped fill over the fixed red SOUTH band). The
   old capture path (full re-render per screenshot) masked the bug by repainting the band; the
   honest offscreen WIC readback exposed it. Fix mirrors the Linux port: paintDirty's
   setPaintDirtyRegionClip override pushes the flush rect via WindowsNative.setFlushRect and
   cn1WinPushClip confines rect clips to it (window graphics only; mutable images unaffected;
   full-screen rect = no-op clamp). No golden change -- the fix restores the committed golden.

2. CI GAP -- all three Windows screenshot comparisons (native x64, native arm64, cross-
   compiled) only POSTED the diff as a PR comment; nothing failed. Stale goldens sailed
   through green CI, which is exactly how the picker mismatch below went unnoticed. All three
   now gate like the Linux port: CN1SS_FAIL_ON_MISMATCH=1, zero tolerated missing, and an
   explicit missing_expected check. The arm64 leg stays continue-on-error (experimental
   runner) so it reports red without blocking.

3. STALE GOLDENS -- the five Windows picker goldens still encoded the String.toLowerCase/
   toUpperCase stub bug (DateSpinner3D's letter-vs-digit locale heuristic always chose
   day-month-year). The Linux goldens were refreshed for this in 1669554; Windows was
   missed because nothing gated. Refreshed from the current CI captures (x64 and arm64
   captures are bit-identical), now month-day-year, consistent with Linux.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aked in the escape artifact)

With the flush-region clip clamp in place, all three Windows legs (native x64, native arm64,
cross-compiled -- bit-identical captures) now render the red SOUTH band correctly, but still
differed from the stored golden in exactly the bottom 3 rows: the OLD golden had MAGENTA there
(the unclamped escape painting past the content pane over the window strip below -- the very
bug the test guards, preserved by the old full-re-render capture path), while the clamped
renderer correctly leaves the grey background. The golden was polluted by the artifact it was
meant to catch; refresh it from the artifact-free capture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…the mark guard

The Linux arm64 suite crash RECURRED (core: SIGSEGV at the parentCls->markFunction LOAD in
gcMarkObject, serial drain). The 94eeafb guard validated a suspect class pointer as 'aligned
+ within 512MB of the code' -- unsound on a non-PIE Linux binary loaded low, where the ENTIRE
malloc heap sits inside that window: a freed object whose offset-0 word held a heap pointer
passed the filter, and when that heap page had been returned to the OS the markFunction load
itself faulted. The heuristic can never be made tight enough; replace it with an EXACT test.

- Every allocation entry point registers its class on first use (cn1GcRegisterClazz):
  cn1BibopAlloc and codenameOneGcMalloc (allocArray funnels into it) register eagerly, so
  every LEGACY object's class -- the only kind that can later become unresolvable (immortal,
  removed from the heap table) -- is always registered. The inline bump path deliberately does
  NOT register (a per-alloc flag check measured ~4% on allocation-heavy renders): a class
  allocated only through the bump path is adopted by the guard's fallback below, since BiBOP
  slots are always resolvable.
- struct clazz gains a TRAILING cn1ClazzRegistered flag -- positional generated initializers
  zero-fill it, so no translator emission change is needed.
- The registry is a lock-free open-addressing set (CAS insert, acquire probes; sized far
  beyond any real class count, with a fall-back-to-resolver safety valve if it ever fills).
- gcMarkObject's guard: a class pointer not in the registry falls back to the authoritative
  resolver, which never dereferences the suspect -- a live slot resolves to itself (its class
  is then adopted into the registry), anything else is freed/dangling and is skipped.
- The class pointer is now loaded ONCE and the validated value reused for every deref in the
  mark body, closing the read-validate-reread window from the original crash note (header
  freed/reused mid-mark).

Validated: gauntlet GcStress/MtStress/MapTorture/SbTorture 12/12 bit-identical to HotSpot
under forced signal-stop; MvtBench checksum identical, perf within noise (the registry probe
runs on the GC side). The Linux arm64 CI leg is the real validation target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…NotFoundException)

The Android script CI failed once (477d053) with UpdateCodenameOne dying on
FileNotFoundException writing tmpProject/lib/CLDC11.jar: the updater opens files without
creating parent directories, the ant project template's lib dir is EMPTY so git never tracks
it, and the copy that materializes tmpProject therefore produces no lib/. Latent for ages --
it only fires when the update server publishes new artifact versions (which happened today;
every earlier run took the nothing-to-update path). The updater tool itself ships out of
repo, so fix it at the two in-repo project-generation sites: GenerateAppProjectMojo's
template tmpProject and CN1BuildMojo's antProject both mkdirs lib/ up front (the dummy
project in AbstractCN1Mojo.updateCodenameOne already did exactly this).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egration-test downloads

The Android/iOS app builds kept failing with UpdateCodenameOne's FileNotFoundException on
tmpProject/lib/CLDC11.jar even after the maven-plugin mojo fix -- because the real creator of
tmpProject is not the plugin: each app project's common/pom.xml has an antrun install-cn1
target that mkdirs codenameone/tmpProject (with NO lib/) and runs the updater against it. The
updater writes lib/CLDC11.jar without creating parent dirs, and takes that path only when the
update server publishes new artifact versions -- which it did yesterday, turning a latent hole
into a deterministic failure. Add the lib mkdir to every in-repo copy of the pattern
(hellocodenameone, playground + its barebones template, initializr, skindesigner, purchase
test app, protocol-e2e, docs demos) and to the cn1app-archetype resource so newly generated
projects carry the fix.

Also: build-linux-jdk8 failed on a transient GitHub codeload hiccup -- curl -L wrote an HTML
error page into master.zip and unzip died. All integration-test downloads now use
curl -fL --retry 5 --retry-all-errors so transient failures retry and real failures fail
loudly instead of unzipping garbage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ness, web-map cold WebKit, watch toast edge)

Three independent iOS failures, each root-caused from this run's artifacts:

1. build-ios VectorMapMarkers shipped a HALF-RENDERED map (only the first tile batch drawn).
   The tests polled MapSurface.isLoadingTiles(), but tiles are requested LAZILY -- a paint
   requests a tile the first time it finds it missing -- so the in-flight set is empty BETWEEN
   request batches while most of the viewport is still beige. New engine API
   VectorMapEngine.isViewportFullyRendered() reports the real signal (every visible tile has a
   rendered image; permanently-failed tiles excluded so it can terminate), useful for loading
   indicators too. The test base polls it with a raised 30s cap and logs CN1SS:WARN when
   capturing at the cap so a slow leg attributes itself.

2. build-ios-metal GoogleWebMap went missing: the app log shows a COLD WebKit on the starved
   runner reached MainFrameLoadCompleted at T+21s of the single 22s budget -- tilesloaded never
   fired, and the test's explicit done() defeats the runner's silent-timeout retry. The test now
   retries ONCE itself with a completely fresh web view (12s + 12s budgets): on the retry
   WebKit's processes and the SDK script cache are warm, so the reload renders in seconds. The
   second exhaustion still fails loudly (missing) rather than baselining a blank map.

3. build-ios-watch ToastBarTopPosition differed by EXACTLY two full-width rows: the toast's
   bottom edge lands +-1px run-to-run because the watch sim's async font load rounds the
   toast's font-derived height differently (the known fontH-fallback variance). Added a
   documented per-test tolerance (1.5% mismatch share); a real failure -- toast absent or
   mispositioned -- flips >10% of pixels and still fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…paint (#5352)

Conflict resolution:
- VectorMapEngine: took master wholesale. #5350's hasRenderedVisibleTiles()/MapView.isMapReady()
  supersedes this branch's isViewportFullyRendered() -- same visible-tile-coverage idea, but the
  master probe also actively computes and REQUESTS the visible tile set, so it is deterministic
  even before the first paint. The branch-side duplicate is dropped.
- VectorMapScreenshotBaseTest: master's isMapReady() probe (incl. the NativeMap branch), keeping
  this branch's two hardening pieces on top: the 30s wait cap (starved CI simulators exceeded
  the old 9s on heavy first renders) and the CN1SS:WARN when capturing at the cap so a slow leg
  attributes itself instead of surfacing as a mystery mismatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant