Skip to content

fix(gc): make per-object layout gate authoritative - #7894

Merged
proggeramlug merged 2 commits into
mainfrom
fix/7873-layout-latch
Aug 11, 2026
Merged

fix(gc): make per-object layout gate authoritative#7894
proggeramlug merged 2 commits into
mainfrom
fix/7873-layout-latch

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the racy count-plus-byte mirror with one exported AtomicU32 armed-thread count
  • disarm that count from the owning PerObjectLayoutHint TLS destructor when a worker exits armed
  • make generated allocation gates read the authoritative count with an LLVM monotonic atomic load
  • retain the recycled-address cleanup path and add deterministic race, worker-exit, and address-reuse witnesses

Reproduction

The synchronization-hook regression forces the reported ordering: A decrements the last arm, B re-arms in the former publication window, then A resumes. Before the fix it ended with count=1, gate=0 and failed. The isolated worker test also failed before the fix with a leaked count of 1 after join().

Performance

Quiet M1 mini, same base commit and runtime/compiler pairs, 5 warmups plus 25 alternating measured pairs of the 50,000,000-allocation pointer-free typed-shape probe:

  • base median: 289.646 ms
  • fix median: 289.754 ms
  • delta: +0.037% (noise-level)
  • paired median difference: +0.252 ms

A seq-cst generated load was measured and rejected before this final version: 286.584 ms to 299.293 ms (+4.43%). The final monotonic load remains atomic and authoritative but avoids that ARM acquire-load tax; the counter transitions remain seq-cst.

Validation

  • cargo test -p perry-runtime: 2,145 passed, 0 failed, 4 ignored; doc tests clean
  • concurrent per-object layout suite after rebase: 15 passed
  • cargo test -p perry-codegen --lib: 897 passed
  • production cargo check -p perry-runtime -p perry-codegen
  • file-size, test-registration, TLS-budget, address-class, formatting, and whitespace gates

Fixes #7873

Summary by CodeRabbit

  • Bug Fixes

    • Improved garbage collection layout tracking during concurrent operations.
    • Fixed stale layout records that could remain after memory addresses were reused.
    • Ensured thread cleanup correctly updates layout state when workers exit.
    • Strengthened allocation and object-layout cleanup reliability under concurrent workloads.
  • Tests

    • Added coverage for concurrent layout transitions, thread-exit cleanup, and recycled-address handling.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dbea4ff1-449b-44a3-bc1d-2da01b878482

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The runtime replaces the split layout-latch state with one atomic armed-thread count. Thread-local teardown now disarms the count. Code generation reads the 32-bit count with an aligned monotonic atomic load. Tests cover races, thread exit, and address reuse.

Changes

Per-object layout latch

Layer / File(s) Summary
Authoritative runtime latch and validation
crates/perry-runtime/src/gc/layout_tables.rs, crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs
The runtime uses one checked AtomicU32 count, disarms it from PerObjectLayoutHint::Drop, and loads it with sequential consistency. Tests cover concurrent rearming, thread exit, and recycled addresses.
Atomic code generation gate
crates/perry-codegen/src/inst.rs, crates/perry-codegen/src/block.rs, crates/perry-codegen/src/dialect/mod.rs, crates/perry-codegen/src/runtime_decls/objects.rs, crates/perry-codegen/src/lower_call/typed_shape_init.rs, crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs
The code generator supports aligned monotonic atomic loads and uses the 32-bit global count before calling layout cleanup. Tests require the generated atomic load.
Changelog entry
changelog.d/7894-layout-latch.md
The changelog records the latch transition and thread-exit cleanup changes.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Worker
  participant PerObjectLayoutHint
  participant typed_shape_init
  participant layout_forget_object

  Worker->>PerObjectLayoutHint: create armed thread-local layout state
  PerObjectLayoutHint->>PerObjectLayoutHint: increment atomic armed-thread count
  typed_shape_init->>typed_shape_init: atomically load global count
  typed_shape_init->>layout_forget_object: call cleanup when count is nonzero
  Worker->>PerObjectLayoutHint: exit with live layout records
  PerObjectLayoutHint->>PerObjectLayoutHint: Drop decrements the global count
Loading

Possibly related PRs

  • PerryTS/perry#7525: Introduced the per-object layout-table fast path extended by this latch change.
  • PerryTS/perry#7809: Introduced layout tracking logic updated by this thread-exit cleanup.
  • PerryTS/perry#7834: Introduced the global layout cleanup mechanism updated to use the atomic count.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #7873 by fixing the authoritative gate, TLS-exit disarming, deterministic race coverage, worker-exit coverage, and recycled-address cleanup.
Out of Scope Changes check ✅ Passed All changes support issue #7873, including runtime and codegen fixes, focused tests, and the related changelog entry.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly and concisely describes the main change: making the per-object layout gate authoritative.
Description check ✅ Passed The description explains the changes, linked issue, reproduction, performance impact, and validation results, with only minor template sections omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7873-layout-latch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug marked this pull request as ready for review August 11, 2026 23:05
@proggeramlug

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.d/7894-layout-latch.md`:
- Around line 1-3: Add a concise “Validation:” line to the changelog fragment
after the behavior summary, recording the passed runtime tests with
RUST_TEST_THREADS=1, codegen checks, concurrent-layout checks, production
checks, formatting checks, and file-size checks.

In `@crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs`:
- Around line 164-170: Update the setup around previous_tenant and child so
child is allocated before retaining previous_tenant, or root previous_tenant and
reload its potentially updated pointer after child allocation. Ensure
layout_note_slot receives the current valid previous_tenant address while
preserving the existing slot and pointer-tag values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9858038d-b12d-479b-b66a-c7067a567e68

📥 Commits

Reviewing files that changed from the base of the PR and between d831231 and 5c6b9bb.

📒 Files selected for processing (9)
  • changelog.d/7894-layout-latch.md
  • crates/perry-codegen/src/block.rs
  • crates/perry-codegen/src/dialect/mod.rs
  • crates/perry-codegen/src/inst.rs
  • crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs
  • crates/perry-codegen/src/lower_call/typed_shape_init.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-runtime/src/gc/layout_tables.rs
  • crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs

Comment on lines +1 to +3
Fixed the process-wide per-object GC layout gate so concurrent thread arm/disarm
transitions cannot publish a false zero, and workers that exit with live layout
records no longer leave the allocation fast path permanently armed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add validation notes to the changelog fragment.

The fragment describes the behavior change but does not record validation. Add a concise Validation: line with the runtime, codegen, concurrent-layout, production, formatting, and file-size checks that passed. Record RUST_TEST_THREADS=1 for runtime tests.

Proposed change
 Fixed the process-wide per-object GC layout gate so concurrent thread arm/disarm
 transitions cannot publish a false zero, and workers that exit with live layout
 records no longer leave the allocation fast path permanently armed.
+Validation: Runtime tests with RUST_TEST_THREADS=1, codegen tests, concurrent-layout
+tests, production checks, and formatting/file-size checks.

As per coding guidelines, changelog fragments must include detailed history and validation notes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Fixed the process-wide per-object GC layout gate so concurrent thread arm/disarm
transitions cannot publish a false zero, and workers that exit with live layout
records no longer leave the allocation fast path permanently armed.
Fixed the process-wide per-object GC layout gate so concurrent thread arm/disarm
transitions cannot publish a false zero, and workers that exit with live layout
records no longer leave the allocation fast path permanently armed.
Validation: Runtime tests with RUST_TEST_THREADS=1, codegen tests, concurrent-layout
tests, production checks, and formatting/file-size checks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/7894-layout-latch.md` around lines 1 - 3, Add a concise
“Validation:” line to the changelog fragment after the behavior summary,
recording the passed runtime tests with RUST_TEST_THREADS=1, codegen checks,
concurrent-layout checks, production checks, formatting checks, and file-size
checks.

Source: Coding guidelines

Comment on lines +164 to +170
let previous_tenant = crate::object::js_object_alloc(0, 2);
let child = crate::object::js_object_alloc(0, 0);
crate::gc::layout_note_slot(
previous_tenant as usize,
1,
POINTER_TAG | (child as u64 & POINTER_MASK),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the layout_trace test module disables moving collection, and how sibling tests root allocations.
set -euo pipefail

fd -t f 'per_object_tables.rs' | xargs -r rg -n -C 5 'ManualGcScanGuard|ScopedRootScannerRegistryGuard|RuntimeHandleScope|force_full_scan|clear_marks'

# How other layout_trace tests hold an allocation across a later allocation.
fd -t d 'layout_trace' | xargs -r rg -n -C 4 'js_object_alloc'

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs"
printf '%s\n' '--- target test and module setup ---'
sed -n '1,220p' "$file"

printf '%s\n' '--- allocation and GC-trigger definitions ---'
rg -n -C 8 'pub\s+unsafe\s+extern.*js_object_alloc|fn\s+js_object_alloc|js_object_alloc\(|CopyingNurseryTestGuard|GcTriggerThresholdTestGuard|ManualGcScanGuard|ScopedRoot' crates/perry-runtime/src crates/perry-runtime/tests 2>/dev/null | head -n 500

printf '%s\n' '--- explicit collection and relocation helpers in the target module ---'
rg -n -C 8 'collect|relocat|move|forget|layout_transfer|force|trigger|assert_flag_sound' "$file"

Repository: PerryTS/perry

Length of output: 49245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant runtime files ---'
fd -t f . crates/perry-runtime | rg '/(object|gc|tests|mod)\.rs$|gc/tests'

printf '%s\n' '--- exact allocation definition and callers ---'
rg -n -C 12 'pub extern "C" fn js_object_alloc|pub fn js_object_alloc|unsafe fn js_object_alloc|fn js_object_alloc' crates/perry-runtime/src

printf '%s\n' '--- test guards and collection helpers ---'
rg -n -C 10 'struct (CopyingNurseryTestGuard|GcTriggerThresholdTestGuard)|impl (CopyingNurseryTestGuard|GcTriggerThresholdTestGuard)|ManualGcScanGuard|ScopedRootScannerRegistryGuard|RuntimeHandleScope|fn clear_marks|fn clear_mark_seeds|collect_garbage|gc_collect|force_full_scan' crates/perry-runtime/src/gc

printf '%s\n' '--- layout_trace module declaration ---'
rg -n -C 10 'mod layout_trace|per_object_tables' crates/perry-runtime/src

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate object files ---'
rg --files crates/perry-runtime/src | rg '(^|/)(object|allocation|arena|gc)([^/]*)\.rs$'

printf '%s\n' '--- exact js_object_alloc definitions ---'
rg -n -C 15 'fn js_object_alloc\b|js_object_alloc\s*=' crates/perry-runtime/src --glob '*.rs' | head -n 250

printf '%s\n' '--- guard definitions only ---'
rg -n -C 12 'struct CopyingNurseryTestGuard|struct GcTriggerThresholdTestGuard|impl CopyingNurseryTestGuard|impl GcTriggerThresholdTestGuard' crates/perry-runtime/src/gc --glob '*.rs' | head -n 300

printf '%s\n' '--- target test module ancestry ---'
rg -n -C 15 'per_object_tables' crates/perry-runtime/src/gc/tests --glob '*.rs' | head -n 200

Repository: PerryTS/perry

Length of output: 30434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- object allocation path ---'
sed -n '1,180p' crates/perry-runtime/src/object/alloc.rs
rg -n -C 20 'js_object_alloc_with_parent|alloc_object|gc_alloc|arena_alloc|maybe.*gc|trigger' crates/perry-runtime/src/object/alloc.rs crates/perry-runtime/src/gc --glob '*.rs' | head -n 500

printf '%s\n' '--- test support guard behavior ---'
sed -n '349,470p' crates/perry-runtime/src/gc/tests/support.rs
rg -n -C 8 'fn clear_marks|fn clear_mark_seeds|pub.*gc_collect_minor|fn gc_collect_minor|automatic.*trigger' crates/perry-runtime/src/gc/tests/support.rs crates/perry-runtime/src/gc --glob '*.rs' | head -n 300

printf '%s\n' '--- guards used by layout_trace tests ---'
rg -n 'CopyingNurseryTestGuard|GcTriggerThresholdTestGuard|js_shadow_slot' crates/perry-runtime/src/gc/tests/layout_trace --glob '*.rs'

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- arena_alloc_gc definition ---'
rg -n -C 25 'fn arena_alloc_gc\b|pub.*arena_alloc_gc\b|arena_alloc_gc\s*\(' crates/perry-runtime/src --glob '*.rs' | head -n 350

printf '%s\n' '--- automatic trigger conditions ---'
rg -n -C 15 'GC_NEXT_TRIGGER_BYTES|GC_NEXT_MALLOC_TRIGGER|maybe_trigger|trigger.*gc|gc_collect_minor|gc_collect_full' crates/perry-runtime/src/arena crates/perry-runtime/src/gc crates/perry-runtime/src --glob '*.rs' | head -n 500

printf '%s\n' '--- all setup calls in layout_trace files ---'
for f in crates/perry-runtime/src/gc/tests/layout_trace/*.rs; do
  printf '%s\n' "--- $f ---"
  rg -n 'CopyingNurseryTestGuard|GcTriggerThresholdTestGuard|suppress_automatic_triggers|js_shadow_frame_push|gc_collect' "$f" || true
done

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- arena_alloc_gc slow/fast path ---'
sed -n '250,430p' crates/perry-runtime/src/arena/allocators.rs

printf '%s\n' '--- layout_trace test setup calls ---'
for f in crates/perry-runtime/src/gc/tests/layout_trace/*.rs; do
  matches=$(rg -n 'CopyingNurseryTestGuard|GcTriggerThresholdTestGuard|suppress_automatic_triggers|js_shadow_frame_push|gc_collect' "$f" || true)
  if [ -n "$matches" ]; then
    printf '%s\n' "--- $f ---"
    printf '%s\n' "$matches"
  fi
done

printf '%s\n' '--- trigger check call sites in allocator ---'
rg -n -C 12 'gc_check_trigger|GC_NEXT_TRIGGER_BYTES|GC_NEXT_MALLOC_TRIGGER' crates/perry-runtime/src/arena/allocators.rs crates/perry-runtime/src/arena/block.rs crates/perry-runtime/src/gc --glob '*.rs' | head -n 400

Repository: PerryTS/perry

Length of output: 44364


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- arena allocation trigger boundary ---'
sed -n '748,825p' crates/perry-runtime/src/arena/block.rs

printf '%s\n' '--- root-scan policy for raw stack locals ---'
rg -n -C 18 'conservative_stack_scan_decision|mark_stack_roots|stack.*scan|C-stack|native_stack_fallback|ScanDisabled|SkipDisabled' crates/perry-runtime/src/gc --glob '*.rs' | head -n 500

printf '%s\n' '--- shadow-root API ---'
rg -n -C 12 'js_shadow_slot_set|RuntimeHandleScope|root_raw_mut_ptr|root_nanbox|shadow.*slot' crates/perry-runtime/src/gc crates/perry-runtime/src --glob '*.rs' | head -n 500

printf '%s\n' '--- free-list reuse and object death behavior ---'
rg -n -C 15 'free_list|layout_forget_object|layout_clear_for_ptr|arena_reset_empty_blocks|reclaim.*dead' crates/perry-runtime/src/arena crates/perry-runtime/src/gc --glob '*.rs' | head -n 500

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- conservative scan decision implementation ---'
rg -n -C 25 'pub\(super\)? fn conservative_stack_scan_decision|fn conservative_stack_scan_decision|ConservativeStackScanDecision::SkipDisabled|ManualGcScanGuard' crates/perry-runtime/src/gc/roots.rs crates/perry-runtime/src/gc --glob '*.rs' | head -n 450

printf '%s\n' '--- gc_check_trigger moving-collection path ---'
rg -n -C 30 'pub.*gc_check_trigger|fn gc_check_trigger|gc_collect_minor_copying|gc_scavenge_enabled|moving_loop_polls' crates/perry-runtime/src/gc --glob '*.rs' | head -n 500

printf '%s\n' '--- clear_marks and test initialization ---'
rg -n -C 15 'fn clear_marks|fn clear_mark_seeds|ensure_gc_initialized|gc_init\(\)' crates/perry-runtime/src/gc/tests crates/perry-runtime/src/gc --glob '*.rs' | head -n 350

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- scan mode implementation ---'
sed -n '1,280p' crates/perry-runtime/src/gc/roots/scan_mode.rs 2>/dev/null || true
rg --files crates/perry-runtime/src/gc | rg 'scan_mode'
file=$(rg --files crates/perry-runtime/src/gc | rg 'scan_mode' | head -n 1)
if [ -n "${file:-}" ]; then sed -n '1,300p' "$file"; fi

printf '%s\n' '--- gc_check_trigger definition ---'
rg -n -C 20 'fn gc_check_trigger\b|pub extern "C" fn js_gc_check_trigger|gc_check_trigger\(' crates/perry-runtime/src/gc --glob '*.rs' | head -n 250

printf '%s\n' '--- direct trigger and scan guard call sites ---'
rg -n -C 25 'force_full_scan|ManualGcScanGuard::new|ManualGcScanGuard|GcTriggerKind::Direct|GcTriggerKind::Automatic' crates/perry-runtime/src/gc --glob '*.rs' | head -n 500

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files defining gc_check_trigger ---'
rg -l 'gc_check_trigger' crates/perry-runtime/src/gc --glob '*.rs' | sort

printf '%s\n' '--- gc_check_trigger body candidates ---'
for f in $(rg -l 'gc_check_trigger' crates/perry-runtime/src/gc --glob '*.rs' | sort); do
  line=$(rg -n '(^|[[:space:]])(pub\(crate\) )?(unsafe )?fn gc_check_trigger|gc_check_trigger\s*=' "$f" | cut -d: -f1 | head -n1 || true)
  if [ -n "$line" ]; then
    start=$((line-20)); [ "$start" -lt 1 ] && start=1
    end=$((line+180))
    printf '%s\n' "--- $f:$line ---"
    sed -n "${start},${end}p" "$f"
  fi
done

Repository: PerryTS/perry

Length of output: 13578


Avoid the allocation while holding previous_tenant.

js_object_alloc(0, 0) can enter the allocation-point GC path when the arena block is full. Allocate child first, or root previous_tenant and reload its updated pointer before layout_note_slot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs` around
lines 164 - 170, Update the setup around previous_tenant and child so child is
allocated before retaining previous_tenant, or root previous_tenant and reload
its potentially updated pointer after child allocation. Ensure layout_note_slot
receives the current valid previous_tenant address while preserving the existing
slot and pointer-tag values.

Source: Coding guidelines

@proggeramlug
proggeramlug merged commit 508cf92 into main Aug 11, 2026
1 of 19 checks passed
@proggeramlug
proggeramlug deleted the fix/7873-layout-latch branch August 11, 2026 23:46
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.

gc: per-object layout global latch can publish false zero and leaks armed thread exits

1 participant