Skip to content

Stop one compute-only thread from freezing the whole VM (issue #5537) - #5631

Merged
shai-almog merged 7 commits into
masterfrom
gc-uncooperative-thread-5537
Aug 31, 2026
Merged

Stop one compute-only thread from freezing the whole VM (issue #5537)#5631
shai-almog merged 7 commits into
masterfrom
gc-uncooperative-thread-5537

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Follow-up to #5609 on issue #5537, and a different root cause: the reporter's
last comment ("the GC is waiting for this thread. This thread is compute bound
and will never yield") is correct.

What was wrong

The mark phase stops each lightweight thread cooperatively -- it raises
threadBlockedByGC and spins on while(t->threadActive) -- and the translator
emits no safepoint polls in generated code, neither on method entry nor on loop
back-edges. Every safepoint lives inside a runtime function, and
cn1BibopMaybeGc is reached once per 64KB page, not per object.

A Java loop that allocates nothing new and enters no contended monitor therefore
reaches no safepoint at all, and that spin never ends. It is not a slow GC, it is
a whole-VM freeze: every other thread parks at its next allocation waiting for a
cycle that can never start. The reporter's debugger caught the collector at
totalwait = 609491500 microseconds -- 10 minutes 9 seconds -- while a game-tree
search ran a compute-only evaluation loop.

The diagnostic meant to catch exactly this was dead for its whole life: time(0)
is in seconds, and the code compared the elapsed value against 10000 and printed
it divided by 1000, so it first became eligible after 2.8 hours and would have
understated by 1000x. That freeze printed nothing.

What this does

Bounds the spin at CN1_GC_SAFEPOINT_WAIT_MAX_US (250ms) and past it freezes the
thread with the same SIGUSR2 stop the collector already uses for genuine native
threads. Windows keeps the unbounded spin -- no POSIX signals, and scanning a
running thread and then sweeping under it is worse than a hang.

The reasoning for each constraint a signal freeze imposes is in comments at the
site it applies to, plus a new section in vm/CLAUDE.md.

Measured

GcUncooperativeThreadIntegrationTest, one 6s compute-only spin against a
churning allocator, idle host. Thresholds are ratios of the workload's own two
measurements, not wall-clock constants.

arm spin max mutator stall share
this build 6052ms 383ms 0.06
-DCN1_GC_NO_FORCE_STOP (ablation) 6151ms 6140ms 1.00

The gate requires the ablation arm to reproduce the wedge, so it cannot go inert.

Not in this change

  • monitorEnter's first-creation branch locks with threadActive still TRUE
    where the contended branch parks first -- a genuine three-way deadlock that
    this escalation rescues on POSIX and not on Windows. Different bug, hot path,
    wants its own change and gate. Documented in vm/CLAUDE.md.
  • A back-edge safepoint poll in the translator, which is the proper long-term
    answer and costs throughput in every loop the VM ever runs.

Note on local testing

GcSteadyStateIntegrationTest's 768MB-ceiling scenario fails on a high-core-count
developer machine and passes in CI (2837s there). A/B'd on an idle host: master
859.9s, this branch 883.5s -- both fail identically, so it is pre-existing and
unrelated. That scenario's dynamics depend on the mutator/collector core ratio,
as its own comments note.

🤖 Generated with Claude Code

The mark phase stops each lightweight thread cooperatively -- it raises
threadBlockedByGC and spins on while(t->threadActive) -- and the translator
emits no safepoint polls in generated code, neither on method entry nor on
loop back-edges. Every safepoint lives inside a runtime function: the
allocator handshakes, contended monitorEnter, Thread.sleep/Object.wait and
the native-call bracket. cn1BibopMaybeGc is reached once per 64KB PAGE, not
per object.

So a Java loop that allocates nothing new and enters no contended monitor
reaches no safepoint at all, and that spin never ends. It is not a slow GC,
it is a whole-VM freeze: every other thread parks at its next allocation
waiting for a cycle that can never start. The reporter's debugger caught the
collector at totalwait = 609491500 microseconds -- 10 minutes 9 seconds --
while a game-tree search ran a compute-only evaluation loop.

Bound the spin at CN1_GC_SAFEPOINT_WAIT_MAX_US (250ms) and past it freeze the
thread with the same SIGUSR2 stop the collector already uses for genuine
native threads, retried on the same cadence because that stop can time out on
a descheduled handler. Windows keeps the unbounded spin: it has no POSIX
signals, and scanning a running thread and then sweeping under it is worse
than a hang.

A thread frozen wherever it happened to be, rather than at a point it chose,
constrains what the rest of the iteration may do, and each constraint is
argued at its site: nothing may allocate while the freeze is held (the root
snapshot is built before it and skipped at both later call sites), the
pending-allocation table is not migrated (the pending[size]=o; size++ window
would orphan an object and then hand its slot back out), the freeze is
released as soon as roots are captured rather than after the mark drain
because a signal-frozen thread busy-spins where a parked one sleeps, and a
thread already frozen must not be signalled again.

Also fix the diagnostic that was supposed to catch this and never could:
time(0) is in SECONDS, and the code compared the elapsed value against 10000
and printed it divided by 1000, so the warning first became eligible after
2.8 hours and would have understated by a factor of 1000. The ten-minute
freeze above printed nothing. totalwait was an int as well, which is signed
overflow at about 36 minutes of waiting -- reachable only by the wedge the
counter exists to report.

gcMarkForcedStop is initialized explicitly because ThreadLocalData is
malloc'd and never zeroed; garbage there would have told the scanner a
running thread was frozen, scanned it from a garbage SP, and never stopped
it.

GcUncooperativeThreadIntegrationTest gates both halves and rebuilds the same
translated project with -DCN1_GC_NO_FORCE_STOP to prove it can fail. One 6s
compute-only spin against a churning allocator, idle host: the ablation arm
stalls a mutator 6140ms of a 6151ms spin, this build 383ms of 6052ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T01:53:27.315906Z 25d569e New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c147c0c1c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vm/ByteCodeTranslator/src/cn1_globals.m Outdated
Comment on lines +2338 to +2343
#if defined(__OBJC__)
NSLog(@"[GC] force-stopped thread %d after %lldus at a safepoint it never reached (%ld so far)",
(int)t->threadId, totalwait, cn1GcForcedStops);
#else
fprintf(stderr, "[GC] force-stopped thread %d after %lldus at a safepoint it never reached (%ld so far)\n",
(int)t->threadId, totalwait, cn1GcForcedStops);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Defer logging until after releasing the forced stop

When the escalation catches a thread inside malloc, NSLog, or a write to stderr, the stopped thread may own allocator or logging locks. Calling NSLog/fprintf here while that thread remains frozen can wait for the same lock and permanently wedge the collector—the failure this change is intended to prevent. Record the diagnostic data while stopped, but emit it only after cn1GcMarkReleaseForced().

Useful? React with 👍 / 👎.

Comment thread vm/ByteCodeTranslator/src/cn1_globals.m Outdated
@@ -2283,7 +2371,19 @@ void codenameOneGCMark() {
long long __mg0 = cn1GcNowNs();
#endif
lockCriticalSection();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid taking the global mutex while the target is frozen

A force-stop can arrive at any instruction, including after the target enters markDeadThread() or another runtime path holding criticalSection; there is also a race between the last threadActive read and signal delivery. In that case this blocking lock acquisition waits on a mutex whose owner cannot run until cn1GcMarkReleaseForced(), but release is reached only after this section, so GC deadlocks. The forced path must not acquire this mutex while the target is suspended (or must release and revalidate the target before doing so).

Useful? React with 👍 / 👎.

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Windows port (x64 / Intel-AMD): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, SSE2 SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 60ms / native 4ms = 15.0x speedup
SIMD float-mul (64K x300) java 66ms / native 4ms = 16.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 190.000 ms
Base64 CN1 decode 128.000 ms
Base64 SIMD encode 100.000 ms
Base64 encode ratio (SIMD/CN1) 0.526x (47.4% faster)
Base64 SIMD decode 92.000 ms
Base64 decode ratio (SIMD/CN1) 0.719x (28.1% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 20.000 ms
Image createMask (SIMD on) 14.000 ms
Image createMask ratio (SIMD on/off) 0.700x (30.0% faster)
Image applyMask (SIMD off) 39.000 ms
Image applyMask (SIMD on) 52.000 ms
Image applyMask ratio (SIMD on/off) 1.333x (33.3% slower)
Image modifyAlpha (SIMD off) 26.000 ms
Image modifyAlpha (SIMD on) 53.000 ms
Image modifyAlpha ratio (SIMD on/off) 2.038x (103.8% slower)
Image modifyAlpha removeColor (SIMD off) 29.000 ms
Image modifyAlpha removeColor (SIMD on) 48.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.655x (65.5% slower)

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Windows port, REAL shipping pipeline: the hellocodenameone screenshot suite rendered by a binary CROSS-COMPILED on Linux (clang-cl + xwin, WebView2 linked) and RUN on a Windows x64 runner. Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 59ms / native 3ms = 19.6x speedup
SIMD float-mul (64K x300) java 64ms / native 5ms = 12.8x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 191.000 ms
Base64 CN1 decode 126.000 ms
Base64 SIMD encode 100.000 ms
Base64 encode ratio (SIMD/CN1) 0.524x (47.6% faster)
Base64 SIMD decode 93.000 ms
Base64 decode ratio (SIMD/CN1) 0.738x (26.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 43.000 ms
Image createMask (SIMD on) 15.000 ms
Image createMask ratio (SIMD on/off) 0.349x (65.1% faster)
Image applyMask (SIMD off) 40.000 ms
Image applyMask (SIMD on) 31.000 ms
Image applyMask ratio (SIMD on/off) 0.775x (22.5% faster)
Image modifyAlpha (SIMD off) 60.000 ms
Image modifyAlpha (SIMD on) 22.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.367x (63.3% faster)
Image modifyAlpha removeColor (SIMD off) 31.000 ms
Image modifyAlpha removeColor (SIMD on) 24.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.774x (22.6% faster)

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Linux port (x64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub x64 runner. Baseline: scripts/linux/screenshots.

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Linux port (arm64), GTK3/Cairo/Pango, ParparVM bytecode-to-C (no JVM): the hellocodenameone screenshot suite rendered by a native ELF built + run on the GitHub arm64 runner. Baseline: scripts/linux/screenshots-arm.

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Windows port (arm64 / Apple Silicon - Arm): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, NEON SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 57ms / native 3ms = 19.0x speedup
SIMD float-mul (64K x300) java 53ms / native 4ms = 13.2x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 267.000 ms
Base64 CN1 decode 154.000 ms
Base64 SIMD encode 65.000 ms
Base64 encode ratio (SIMD/CN1) 0.243x (75.7% faster)
Base64 SIMD decode 61.000 ms
Base64 decode ratio (SIMD/CN1) 0.396x (60.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 36.000 ms
Image createMask (SIMD on) 8.000 ms
Image createMask ratio (SIMD on/off) 0.222x (77.8% faster)
Image applyMask (SIMD off) 25.000 ms
Image applyMask (SIMD on) 19.000 ms
Image applyMask ratio (SIMD on/off) 0.760x (24.0% faster)
Image modifyAlpha (SIMD off) 17.000 ms
Image modifyAlpha (SIMD on) 13.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.765x (23.5% faster)
Image modifyAlpha removeColor (SIMD off) 22.000 ms
Image modifyAlpha removeColor (SIMD on) 36.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.636x (63.6% slower)

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

✅ ByteCodeTranslator Quality Report

Test & Coverage

  • Tests: 530 total, 0 failed, 54 skipped

Benchmark Results

  • Execution Time: 10526 ms

  • Hotspots (Top 20 sampled methods):

    • 22.11% java.util.ArrayList.indexOf (310 samples)
    • 6.85% com.codename1.tools.translator.BytecodeMethod.equals (96 samples)
    • 6.35% com.codename1.tools.translator.BytecodeMethod.addToConstantPool (89 samples)
    • 4.64% com.codename1.tools.translator.BytecodeMethod.optimize (65 samples)
    • 3.85% com.codename1.tools.translator.ByteCodeClass.findDeclaredMethod (54 samples)
    • 2.78% java.lang.StringBuilder.append (39 samples)
    • 2.71% java.lang.System.identityHashCode (38 samples)
    • 2.35% java.lang.Object.hashCode (33 samples)
    • 2.28% com.codename1.tools.translator.Parser.cn1EnsureSubclassIndex (32 samples)
    • 1.85% com.codename1.tools.translator.ByteCodeClass.hasDeclaredMethod (26 samples)
    • 1.71% com.codename1.tools.translator.ByteCodeClass.fillVirtualMethodTable (24 samples)
    • 1.64% com.codename1.tools.translator.Parser.generateClassAndMethodIndexHeader (23 samples)
    • 1.57% org.objectweb.asm.tree.analysis.Analyzer.analyze (22 samples)
    • 1.50% java.util.HashMap.hash (21 samples)
    • 1.14% org.objectweb.asm.tree.analysis.Analyzer.findSubroutine (16 samples)
    • 1.14% com.codename1.tools.translator.BytecodeMethod.appendCMethodPrefix (16 samples)
    • 1.14% java.lang.StringCoding.encode (16 samples)
    • 1.07% com.codename1.tools.translator.NativeSymbolIndex.<init> (15 samples)
    • 1.07% com.codename1.tools.translator.bytecodes.Invoke.resolveDirectTarget (15 samples)
    • 1.00% java.lang.String.equals (14 samples)
  • ⚠️ Coverage report not generated.

Static Analysis

  • ✅ SpotBugs: no findings (report was not generated by the build).
  • ⚠️ PMD report not generated.
  • ⚠️ Checkstyle report not generated.

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 366 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 62ms / native 3ms = 20.6x speedup
SIMD float-mul (64K x300) java 53ms / native 3ms = 17.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 223.000 ms
Base64 CN1 decode 134.000 ms
Base64 native encode 677.000 ms
Base64 encode ratio (CN1/native) 0.329x (67.1% faster)
Base64 native decode 345.000 ms
Base64 decode ratio (CN1/native) 0.388x (61.2% faster)
Base64 SIMD encode 71.000 ms
Base64 encode ratio (SIMD/CN1) 0.318x (68.2% faster)
Base64 SIMD decode 50.000 ms
Base64 decode ratio (SIMD/CN1) 0.373x (62.7% faster)
Base64 encode ratio (SIMD/native) 0.105x (89.5% faster)
Base64 decode ratio (SIMD/native) 0.145x (85.5% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.250x (75.0% faster)
Image applyMask (SIMD off) 79.000 ms
Image applyMask (SIMD on) 53.000 ms
Image applyMask ratio (SIMD on/off) 0.671x (32.9% faster)
Image modifyAlpha (SIMD off) 50.000 ms
Image modifyAlpha (SIMD on) 46.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.920x (8.0% faster)
Image modifyAlpha removeColor (SIMD off) 48.000 ms
Image modifyAlpha removeColor (SIMD on) 44.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.917x (8.3% faster)

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1466 seconds

Build and Run Timing

Metric Duration
Simulator Boot 64000 ms
Simulator Boot (Run) 0 ms
App Install 15000 ms
App Launch 6000 ms
Test Execution 503000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 88ms / native 2ms = 44.0x speedup
SIMD float-mul (64K x300) java 72ms / native 3ms = 24.0x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 208.000 ms
Base64 CN1 decode 94.000 ms
Base64 native encode 381.000 ms
Base64 encode ratio (CN1/native) 0.546x (45.4% faster)
Base64 native decode 255.000 ms
Base64 decode ratio (CN1/native) 0.369x (63.1% faster)
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.236x (76.4% faster)
Base64 SIMD decode 50.000 ms
Base64 decode ratio (SIMD/CN1) 0.532x (46.8% faster)
Base64 encode ratio (SIMD/native) 0.129x (87.1% faster)
Base64 decode ratio (SIMD/native) 0.196x (80.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.286x (71.4% faster)
Image applyMask (SIMD off) 58.000 ms
Image applyMask (SIMD on) 41.000 ms
Image applyMask ratio (SIMD on/off) 0.707x (29.3% faster)
Image modifyAlpha (SIMD off) 38.000 ms
Image modifyAlpha (SIMD on) 33.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.868x (13.2% faster)
Image modifyAlpha removeColor (SIMD off) 45.000 ms
Image modifyAlpha removeColor (SIMD on) 35.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.778x (22.2% faster)

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1683 seconds

Build and Run Timing

Metric Duration
Simulator Boot 94000 ms
Simulator Boot (Run) 1000 ms
App Install 15000 ms
App Launch 1000 ms
Test Execution 556000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 305ms / native 9ms = 33.8x speedup
SIMD float-mul (64K x300) java 261ms / native 11ms = 23.7x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 260.000 ms
Base64 CN1 decode 263.000 ms
Base64 native encode 1710.000 ms
Base64 encode ratio (CN1/native) 0.152x (84.8% faster)
Base64 native decode 1985.000 ms
Base64 decode ratio (CN1/native) 0.132x (86.8% faster)
Base64 SIMD encode 164.000 ms
Base64 encode ratio (SIMD/CN1) 0.631x (36.9% faster)
Base64 SIMD decode 116.000 ms
Base64 decode ratio (SIMD/CN1) 0.441x (55.9% faster)
Base64 encode ratio (SIMD/native) 0.096x (90.4% faster)
Base64 decode ratio (SIMD/native) 0.058x (94.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 9.000 ms
Image createMask (SIMD on) 7.000 ms
Image createMask ratio (SIMD on/off) 0.778x (22.2% faster)
Image applyMask (SIMD off) 49.000 ms
Image applyMask (SIMD on) 45.000 ms
Image applyMask ratio (SIMD on/off) 0.918x (8.2% faster)
Image modifyAlpha (SIMD off) 213.000 ms
Image modifyAlpha (SIMD on) 252.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.183x (18.3% slower)
Image modifyAlpha removeColor (SIMD off) 76.000 ms
Image modifyAlpha removeColor (SIMD on) 345.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 4.539x (353.9% slower)

Two P1 review findings on the escalation, both correct, plus a third instance
of the same class that the review did not flag. All three are the rule the
change states for cn1GcBuildRootSnapshots -- nothing may block on a lock the
stopped thread might own -- applied inconsistently: the rule was written and
then broken three lines below it.

A thread force-stopped by signal halts at an arbitrary instruction, so it can
own the libc allocator's lock, stdio's, os_log's, or criticalSection. Blocking
on any of those before cn1GcMarkReleaseForced makes the collector wait on a
thread that cannot run until the collector releases it: the same permanent
wedge this change exists to remove, only rarer and harder to place. "It is
only held briefly" is not a defence -- the target can enter that code in the
window between the last threadActive read and the signal landing.

- The escalation's own NSLog/fprintf ran under the freeze. The counter values
  are captured there now and printed after the release.
- The pending-migration block took criticalSection unconditionally and guarded
  only its body, which deadlocks just as thoroughly. The forced path skips the
  lock as well as the body.
- The aggressive-allocator hold calls get_free_memory() and NSLog, and the EDT
  is an ordinary candidate for the escalation -- a long computation on the
  event thread is the shape issue #5537 reported. Skipped on the forced path;
  its allocSize input describes a table this cycle deliberately did not
  migrate anyway.

Every step between freeze and release is now enumerated and classified at the
top of the per-thread block, so the next addition there has to answer the
question rather than rediscover it.

GcUncooperativeThreadIntegrationTest unchanged: 336ms of a 5955ms spin (0.06)
against 6575ms of 6571ms (1.00) for the -DCN1_GC_NO_FORCE_STOP ablation.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb71d94c6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if(base == 0 || ssz == 0) return;

#ifdef CN1_GC_CAN_FORCE_STOP
if(t->gcMarkForcedStop) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve stack bounds before freezing the mutator

On Linux this forced-stop branch is reached only after the target is suspended, but cn1GcScanThreadNativeStack() first calls cn1GcStackBase(), whose pthread_getattr_np() implementation uses internal locks and performs realloc/free. If the signal stopped the target inside malloc or while it held the relevant pthread lock, the collector can deadlock here before reaching cn1GcMarkReleaseForced(), recreating the VM freeze this escalation is intended to prevent. Cache the stack bounds before cn1GcMarkForceStopUncooperative() and reuse them in this branch.

Useful? React with 👍 / 👎.

Third finding of the same class, and the most instructive one: I classified
cn1GcStackBase as safe to call under a held freeze after reading only its
Apple branch, which is pthread_get_stackaddr_np plus pthread_get_stacksize_np
-- two plain accessors. The Linux branch of the same function is
pthread_getattr_np, which mallocs (and reads /proc/self/maps for the initial
thread) and frees again through pthread_attr_destroy. Calling it while the
target is frozen can block on the allocator lock the target owns, which is the
deadlock the previous commit removed from three other sites.

A cross-platform helper has to be classified on its WORST platform. Checking
one spelling and letting the conclusion cover the rest is how this reached the
"SAFE, lock-free" line of the enumeration; the enumeration now says so.

cn1GcMarkForceStopUncooperative resolves the bounds while the thread is still
running and stashes them on the TLD -- they cannot change for a live pthread
-- and cn1GcScanThreadNativeStack's forced branch moves ABOVE the
cn1GcStackBase call so the frozen path never reaches it. Unresolvable bounds
now decline the freeze rather than take one: without them the conservative
scan could not read the thread's native stack even once stopped, so freezing
would skip its roots for nothing.

gcSigStackBase existed but was never read; it is now the pre-freeze base, with
gcSigStackSize alongside it. Both are explicitly initialized, because
ThreadLocalData is malloc'd and never zeroed.

Note CI runs parparvm-tests on ubuntu-latest and passed twice with the defect
present -- it is a race, so a green Linux run was never evidence of absence.

GcUncooperativeThreadIntegrationTest unchanged: 365ms of a 6193ms spin (0.06)
against 6092ms of 6081ms (1.00) for the ablation.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a8ddbfbdf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2355 to +2356
cn1GcBuildRootSnapshots();
forcedStop = cn1GcMarkForceStopUncooperative(t);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep adoption allocation out of the forced-stop window

When the forced stop catches a mutator inside malloc, the subsequent precise/native root scans call gcMarkObject, and with the default CN1_ADOPT_POLICY == 1 a surviving non-leaf BiBOP root reaches cn1MatureObject, whose adoption-buffer growth calls realloc. In particular, the first adoption uses this path because the buffer capacity starts at zero, so the collector can block on the allocator lock held by the frozen mutator and never reach cn1GcMarkReleaseForced(). Preallocate/defer adoption or otherwise ensure root marking cannot allocate while this freeze is held.

Useful? React with 👍 / 👎.

Fourth finding of the same class, and it refutes the claim the previous two
commits leaned on. I asserted the root scans are malloc-free having checked
gcMarkWorklistPush (fixed array) and cn1ForceVisitedTestAndSet (force path
only, which no root scan takes) and stopped there. gcMarkObject has a third
allocating edge: with the default CN1_ADOPT_POLICY == 1 a surviving non-leaf
BiBOP root reaches cn1MatureObject, whose adoption buffer grows by realloc --
and gcAdoptCap starts at zero, so the first adoption of the process always
takes it. Checking two of three paths and generalising is the same mistake as
reading one platform's branch of cn1GcStackBase.

This one is NOT introduced here. Master already marks between
cn1GcSignalStopOne and cn1GcSignalReleaseOne in cn1GcScanThreadNativeStack, so
the collector could already hang against a frozen NATIVE thread; the
escalation widens the exposure a long way, because lightweight threads now
take that path, far more often, and with the precise object stack scanned
under the freeze as well. Both sites are fixed.

cn1GcFreezeHeld is raised for the duration of either freeze, and cn1MatureObject
declines BEFORE its claim CAS when the flag is up and the buffer would have to
grow. Declining before the CAS is the whole point: claiming and then bailing is
what the existing OOM path does, and it leaves the object flagged -4 and
unregistered forever, which is a leak, because the CAS can never fire again. A
declined object stays -3, is still marked and traced this cycle (the worklist
push below is unconditional), and simply graduates in a later one. The buffer
is also given headroom before each freeze, while allocating is still legal, so
the decline should stay rare.

GcUncooperativeThreadIntegrationTest 348ms of a 6015ms spin (0.06) against
5847ms of 5865ms (1.00) for the ablation; GcHeapIntegrity and GcOverflowSpiral
green, which is where an adoption mistake would show.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ded343848

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2625 to +2627
if(forcedStop) {
cn1GcMarkReleaseForced(t);
forcedStop = JAVA_FALSE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the forced mutator stopped when SATB is disabled

When the documented -DCN1_DISABLE_SATB fallback is used, this release lets the mutator resume before its grey root worklist is drained even though both insertion and deletion barriers compile to no-ops. The resumed thread can read a child from a captured root into a local and clear the root's field; the drain then misses the child, while the pre-release stack snapshot cannot contain the new local, allowing sweep to reclaim an object the thread is still using. Either retain the freeze through gcMarkDrainParallel() in this configuration or exclude force-stop support when SATB is disabled.

Useful? React with 👍 / 👎.

Comment on lines +193 to +196
assertTrue(fixed.output.contains(FORCE_STOP_MARKER),
"The collector never reported a forced stop, so the spinner parked on its own"
+ " and this run did not exercise the escalation at all. Output: "
+ tail(fixed.output));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip the force-stop gate on Windows

On Windows, CN1_GC_CAN_FORCE_STOP is explicitly not defined and the runtime intentionally retains the unbounded cooperative wait, so the fixed arm can never emit FORCE_STOP_MARKER; this assertion therefore deterministically fails whenever the benchmark group is run on Windows, regardless of the implementation's correctness on supported POSIX targets. Guard the test with a Windows/platform-capability assumption or restrict it to platforms where signal stopping is compiled.

Useful? React with 👍 / 👎.

…Windows (review)

Two P2 review findings, both correct.

The early release is justified by the SATB deletion barrier -- that is what lets
a released mutator stay sound before the drain, and it is already the only thing
keeping genuine native threads honest, since they are never blocked at all. The
argument was stated and then not guarded: under the documented
-DCN1_DISABLE_SATB ablation both barriers compile to no-ops and it evaporates.
The resumed thread can read a child out of a captured root into a local the
pre-release stack snapshot cannot contain, clear the field, and have the sweep
reclaim an object it is still using. That build now keeps the freeze until after
gcMarkDrainParallel, which costs the busy spin the early release exists to avoid
-- the right trade in an ablation build, and none at all in a shipping one.
The deferred log moves to the later point so it stays after the release in both.

The gate asserted the force-stop marker unconditionally. CN1_GC_CAN_FORCE_STOP
is deliberately undefined on Windows -- no POSIX signals, so the runtime keeps
the unbounded cooperative wait there -- which made the assertion fail on Windows
for a reason that says nothing about the code under test. Skipped by assumption
rather than weakened: the assertions are what make it a gate, and the platform
that can satisfy them is the platform the feature exists on.

Compiles clean on -DCN1_DISABLE_SATB along with the other five arms. Gate
unchanged: 367ms of a 5967ms spin (0.06) against 6274ms of 6256ms (1.00).

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4174c6557f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// worklist is a fixed-size array; the force-visited side table
// is only touched on the force path, which no root scan takes).
cn1GcBuildRootSnapshots();
forcedStop = cn1GcMarkForceStopUncooperative(t);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not force-stop a nursery minor collection

When CN1_NURSERY is enabled and a minor collection lasts past the 250 ms escalation threshold, cn1NurseryMinorCollect() deliberately leaves threadActive true while setting t->nurseryPromoting. This line can therefore freeze it mid-collection, after which the precise/native root scans call gcMarkObject(t, ...); because t->nurseryPromoting is true, gcMarkObject() takes its minor-GC branch and returns without globally marking any non-nursery object. The following global sweep can then reclaim mature objects that are live only from this thread. Avoid escalating while the target is performing its minor collection, or ensure forced root marking uses collector rather than target nursery state.

Useful? React with 👍 / 👎.

The previous commit wrapped the early release in #if !defined(CN1_DISABLE_SATB)
and left the old unconditional-release comment stranded above the new one, so
the block carried two descriptions of itself and the stale one no longer
matched the code under it. Verified comment-only: preprocessing both revisions
with -fpreprocessed -dD -E -P and diffing them is empty.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f47025f23e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2633 to +2635
if(forcedStop) {
cn1GcMarkReleaseForced(t);
forcedStop = JAVA_FALSE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep allocation-capable marking outside the forced freeze

When built with the documented -DCN1_DISABLE_SATB fallback, this conditional omits the release and leaves the mutator frozen across markStatics(d) and gcMarkDrainParallel(d). Those paths are not allocation-free: generated markStatics() calls gcMarkObject(..., JAVA_TRUE), whose force-visited table can call malloc, and the first parallel drain lazily creates worker threads with pthread_create. If the signal caught the target while it held the allocator or pthread-internal lock, the collector can block here and never reach the later cn1GcMarkReleaseForced(), recreating a permanent VM wedge. Reserve/initialize these resources before freezing or otherwise keep the no-SATB frozen interval allocation-free.

Useful? React with 👍 / 👎.

…SATB is off (review)

Two more findings. The first is a use-after-free this change introduced; the
second is one my previous fix introduced while fixing something else.

NURSERY. The root scans mark through gcMarkObject(t, ...) -- the TARGET's
thread state, not the collector's. cn1NurseryWriteBarrier raises
nurseryPromoting and deliberately leaves threadActive TRUE for the whole minor
collection, which makes such a thread a prime candidate for a 250ms
escalation; and under that flag gcMarkObject's first act is to promote-or-
return WITHOUT marking. Freezing there hands the sweep a thread whose roots
were every one of them silently skipped, and mature objects live only from it
are reclaimed. A cooperatively parked thread never has the flag set, which is
why this could not happen before. cn1GcMarkForceStopUncooperative now declines
such a thread -- checked AFTER the stop, because a read taken while the thread
still runs can be raised in the window before the signal lands, whereas a
frozen thread's flag cannot change.

SATB. The previous commit answered "the early release is only sound because of
the barrier" by holding the freeze through the drain when the barrier is
compiled out. That is worse: it drags markStatics -- which force-marks, and so
reaches the force-visited table's malloc -- and gcMarkDrainParallel's lazy
pthread_create inside a window where the frozen thread may own the allocator
or pthread lock. A wedge in the middle of the fix for a wedge. The frozen
window has to stay small and enumerable and a full parallel drain is neither,
so -DCN1_DISABLE_SATB now simply does not get the escalation and keeps
master's unbounded cooperative wait, which is the behaviour that ablation
exists to measure against. The release and the deferred log go back to one
site each, and the dependency is enforced in the CN1_GC_CAN_FORCE_STOP guard
instead of being asserted in a comment.

Clean on eight compile arms including -DCN1_NURSERY and
-DCN1_NURSERY -DCN1_DISABLE_SATB. Gate 355ms of a 5843ms spin (0.06) against
7046ms of 7025ms (1.00); GcHeapIntegrity green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almog merged commit e44d84d into master Aug 31, 2026
59 of 63 checks passed
@shai-almog
shai-almog deleted the gc-uncooperative-thread-5537 branch August 31, 2026 04:11
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