Skip to content

gc: the PYPY_GC_DEBUG nurseries, a debug_check_consistency with a body, gc.get_count's second element, and the comments that outran the code - #1516

Open
youknowone wants to merge 14 commits into
mainfrom
gc-decouple
Open

gc: the PYPY_GC_DEBUG nurseries, a debug_check_consistency with a body, gc.get_count's second element, and the comments that outran the code#1516
youknowone wants to merge 14 commits into
mainfrom
gc-decouple

Conversation

@youknowone

Copy link
Copy Markdown
Owner

A pass over the GC layer's open items. Each one is either implemented, or filed as a deviation with the measurement that decided it — and several turned out to be comments that had outrun the code, which is its own class of defect: a reader who trusts them reasons about a collector that does not exist.

Behaviour

PYPY_GC_DEBUG rotating nurseries and PYPY_GC_NURSERY_DEBUG. Nursery allocates its arena page-aligned and page-rounded so it can be handed to mprotect, and gains install_debug_rotating_nurseries / debug_rotate for post_setup and debug_rotate_nursery: six spare arenas, each inaccessible until it reaches the front of the ring. HAS_PROTECT is false on wasm32, where the install returns without allocating.

debug_check_consistency gets a body. It was one debug_assert_eq! gated at its call sites. Upstream opens with if self.DEBUG: and asserts for real, so the gate moved inside and the debug_assert! went: the checks survive a release build, which is what PYPY_GC_DEBUG arms them for, and a run that does not set it pays one load. Adds the two list invariants and the heap half from GCBase.debug_check_consistency — enumerate every root, trace the reachable graph, run debug_check_object on each object once.

gc.get_count() answers its second element. It returned the constant (0, 0, 0). Element 1 is gen-0 collections since gen-1 was collected, which the collector does maintain — minor_collections minus its value at the last major cycle's end. Sampling that is the whole change, and the subtle part is where: finish_incremental_cycle is a sweep-to-finalize seam, not the end, and do_collect_full runs a minor before every remaining step, so a snapshot taken there reads 1 immediately after gc.collect(). The sample belongs at the FINALIZING -> SCANNING transition. Elements 0 and 2 stay 0 with the reason recorded at the site.

Measured on pyre-dynasm: (0, 0, 0) at start, (0, 2, 0) after 200k tuple appends, (0, 0, 0) after gc.collect().

pin_root's normalized return, bound at six sites. pin_root returns the normalized live word and is #[must_use] with a message that sanctions let _ = for liveness-only pins. Six functions spelled it that way and then read the pre-pin local, so the value handed to the comparison was the one normalize_published_slot replaced. fileio_writebuf's pin also had no push_roots bracket, so its slot stayed on the shadow stack after the call returned.

Dead root machinery removed. CompiledCodeRegistry, CompiledCodeRegion, SafepointMap, SafepointEntry, scan_frame, find_region and GcMap were never populated and never consulted outside collector.rs's own test module; the GcAllocator::jit_free trait method that retained over the always-empty vec goes with them. set_active_extra_root_walker had no caller repo-wide, so ACTIVE_EXTRA_ROOT_WALKER was never set and its two walk_active_extra_roots calls could not yield a root — the multi-registrar shadow_stack::walk_extra_roots already runs at both sites. Three of the removed types documented a role no backend performs.

Deviations, filed rather than closed

gc.set_threshold / gc.get_debug / gc.freeze. All three are 3.14-only surface with no PyPy side and no grader — moduledef.py binds none of these names, and test_gc.py is skipped on both the lib-python testmap and the cpython_tests baseline. set_threshold is a unit mismatch, not a missing wire: every driver the collector exposes is in bytes. The other two were prototyped and abandoned on measurement: inside the single collection test_saveall brackets expecting one object, pyre's sweep enumerates 317 dying objects across 12 type ids, stable over three runs, because without refcounting "died" means "died since the last major" and no callback filter recovers 3.14's cyclic-only subset.

No automatic shadow-stack transform. Upstream's transformer inserts push_roots / pop_roots around the operations that can collect, across the whole translated graph. pyre has no such pass over Rust code, so every bracket is hand-written; the module doc now carries the census with the command that re-derives it — 1376 scopes, 2604 pins, 3571 read-backs across 163 files — and names what the missing pass gates: born-old interpreter allocation and the non-moving safepoint major.

wasm runs no part of the GC rewrite. The omission covers every op, not some allocation shapes. Two of the pass's zeroing duties go with it: the clear_gc_fields NULL stores, and the clear half of NewArrayClear, which wasm lowers exactly like NewArray. The nursery zero-fill is what makes both hold — the comment credited only the first. Retiring it takes the whole pass (which additionally needs a ZeroArray lowering and a descr-carrying GC_LOAD/GC_STORE lowering, the arm that panics today) or explicit initialization at four named sites.

GCREFTRACER is not in the object graph. The registry doc said the table drops when the loop token is freed. On cranelift a bridge's table is pinned a second time by every BridgeData that can dispatch to the bridge, so it outlives the CLT it was registered against; only dynasm has the CLT as sole holder. Nothing dispatches Backend::free_loop at all — the release is Arc drop at memmgr eviction. Also records why no clear_gcref_tracer analog is owed: upstream zeroes array_length because its slot array is reserved inside the code block and freed with it, while here the slots are the table's own Box.

Comment corrections

Each of these named something the code does not do.

  • at_outermost_activation tests EVAL_NESTING <= 2, so module level and one called function's loop both collect; two comments called it outermost-only.
  • get_possibly_forwarded_header called its nursery case latent because every finalizer-queue registrant is stable-allocated. list_descr_new takes its header from the collecting nursery arm and registers a finalizer.
  • rescan_major_stack_roots_black_and_drain said a JitFrame lives in the old generation. alloc_off_gc_jitframe returns memory outside the GC heap, and the other jitframe paths are nursery bumps.
  • register_gc_alloc_collecting_hook named only the elidable bigint payload helpers as its callers.
  • register_mutator's doc armed unregistration from a GcMutatorRegistration thread-local that does not exist; the pairing is RuntimeThread, and the ordering is load-bearing.
  • pop_to's TODO cited a JitDriver Drop that does not exist. The try_with is a standing precaution: no live path pops from a thread-local destructor.
  • TypeInfoLayout claimed its reserved word makes the row match rffi.sizeof(GCData.TYPE_INFO). Upstream's row is four words; the reserved word is there so TypeEntry's stride stays a power of two — deleting it gives 24.
  • MAX_TYPES said "a dozen or so types" are used. Measured: 867 of 1024.
  • writebarrier_before_move has no caller outside tests, and its CARDS_SET precondition has no production setter; the doc now names the three W_ListObject sites that will owe the call when one appears.
  • Four majit-gc modules opened with a /// block, which documents the use that follows rather than the module. Each of those imports is private, so the text reached no rendered page.

Verification

cargo fmt --all -- --check clean; scripts/check-majit-boundary.py and scripts/check-new-line-citations.py --base origin/main clean (934 added Rust lines scanned, no new line-number citations). majit-gc unit suite 306 passed, including the new minors_accumulate_until_a_major_finishes, which is what caught the wrong snapshot point. cargo check clean on the cranelift and wasm backends; release pyre-dynasm builds; all 27 gc_* / stdlib_gc snippets pass, plus the new gc_get_count_reports_minors_since_the_major.py.

The full check.py matrix has not been run locally — CI owns it.

🤖 Generated with Claude Code

https://claude.ai/code/session_013SXZZhZ24w8JtnFUaEGzjP

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 70d0455c-d0f0-4f59-8973-d94950f4bb88

📥 Commits

Reviewing files that changed from the base of the PR and between bd0908b and c80562e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • majit/gate-triage.md
  • majit/majit-backend-cranelift/src/compiler.rs
  • majit/majit-backend-dynasm/src/runner.rs
  • majit/majit-backend-wasm/src/lib.rs
  • majit/majit-gc/Cargo.toml
  • majit/majit-gc/src/collector.rs
  • majit/majit-gc/src/gcreftracer.rs
  • majit/majit-gc/src/lib.rs
  • majit/majit-gc/src/nursery.rs
  • majit/majit-gc/src/oldgen.rs
  • majit/majit-gc/src/shadow_stack.rs
  • majit/majit-gc/src/trace.rs
  • pyre/extra_tests/snippets/gc_get_count_reports_minors_since_the_major.py
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/eval.rs
  • pyre/pyre-interpreter/src/module/gc/mod.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-jit/src/eval.rs
  • pyre/pyre-object/src/dictmultiobject.rs
  • pyre/pyre-object/src/gc_hook.rs
  • pyre/pyre-object/src/gc_interp.rs
  • pyre/pyre-object/src/gc_roots.rs
  • pyre/pyre-object/src/setobject.rs
  • pyre/pyrex/src/lib.rs
 __________________________________________________________________
< Fine-tuning the Enterprise's warp drive to avoid BSOD at warp 9. >
 ------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ 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 gc-decouple

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.

@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: 65391244a6

ℹ️ 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 +7933 to +7934
if self.config.gc_nursery_debug && barriers.is_empty() {
self.nursery.debug_rotate();

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 Refresh published nursery bounds after rotating

When both PYPY_GC_DEBUG and PYPY_GC_NURSERY_DEBUG are enabled, this replaces Nursery::start, but the singleton fast path in gc_is_nursery_object continues using the bounds published only by store_singleton. It consequently classifies the protected retired arena as young and the active arena as old; for example, mapdict::note_young_owner then omits active-nursery owners from its reconciliation set, leaving stale address-keyed instance-dict/weakref entries after the next minor collection. Republish or dynamically update the singleton bounds whenever the rotation succeeds.

Useful? React with 👍 / 👎.

Comment on lines +5949 to 5953
assert!(
!hdr.has_flag(flags::PINNED),
"GCFLAG_PINNED outside the nursery after collection at {obj_addr:#x}"
);
}

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 Port the state-specific consistency checks

With PYPY_GC_DEBUG enabled during MARKING or SWEEPING, this function returns after only the common nursery flags and omits incminimark's _debug_check_object_marking, _debug_check_object_sweeping, _debug_check_object_finalizing, and _debug_check_object_scanning dispatch. As a result, black-to-white edges, missing TRACK_YOUNG_PTRS, and uncleared finalization/card flags—the incremental-collector corruptions this mode is intended to catch—are reported as a consistent heap. Please port the state-dependent checks rather than presenting this truncated body as debug_check_object parity.

AGENTS.md reference: AGENTS.md:L223-L226

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit b6632ea).
Updated: 2026-08-27T02:19:26.451Z

Files in the reviewed diff
Cargo.lock
majit/gate-triage.md
majit/majit-backend-cranelift/src/compiler.rs
majit/majit-backend-dynasm/src/runner.rs
majit/majit-backend-wasm/src/lib.rs
majit/majit-gc/Cargo.toml
majit/majit-gc/src/collector.rs
majit/majit-gc/src/gcreftracer.rs
majit/majit-gc/src/lib.rs
majit/majit-gc/src/nursery.rs
majit/majit-gc/src/oldgen.rs
majit/majit-gc/src/shadow_stack.rs
majit/majit-gc/src/trace.rs
pyre/extra_tests/snippets/gc_get_count_reports_minors_since_the_major.py
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/eval.rs
pyre/pyre-interpreter/src/module/gc/mod.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-object/src/dictmultiobject.rs
pyre/pyre-object/src/gc_hook.rs
pyre/pyre-object/src/gc_interp.rs
pyre/pyre-object/src/gc_roots.rs
pyre/pyre-object/src/setobject.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • majit/majit-gc/src/collector.rs:5904 ↔ rpython/memory/gc/incminimark.py:2729: the new debug reachability walk starts from only enumerate_root_walker_values(). Upstream enumerate_all_roots() also includes prebuilt_root_objects and both live/pending finalizer lists. With PYPY_GC_DEBUG, pyre can miss a stale pointer reachable solely through those roots.

  • majit/majit-gc/src/collector.rs:5895 ↔ rpython/memory/gc/base.py:451: the port uses std::collections::HashSet and Vec for the debug walk, where upstream owns an AddressDict and GC AddressStack. This is an introduced storage-shape divergence, not a required Rust-language limitation.

  • pyre/pyre-interpreter/src/module/gc/mod.rs:1712 ↔ pypy/module/gc/interp_gc.py:8: get_count() describes generation 0 as a nursery collection, but gc.collect(0) still ignores the generation and performs a full collection at mod.rs:1423-1438. This fails CPython’s generation-count contract: after gc.collect(0), CPython expects count slots 1/2 to be (1, 0); after gc.collect(1), (0, 1) (lib-python/3/test/test_gc.py:401-414). Pyre reports (0, 0) after either full collection.

  • majit/majit-gc/src/nursery.rs:62 ↔ rpython/rtyper/lltypesystem/llarena.py:487: HAS_PROTECT is true for every non-wasm architecture, while upstream enables protection only on POSIX and Windows and explicitly disables it elsewhere. Non-wasm non-POSIX/non-Windows targets incorrectly allocate/rotate protected nurseries.

3. Pre-existing mismatches (already present before this patch)

None.

4. Structural adaptations

  • pyre/pyre-interpreter/src/module/gc/mod.rs:1712 ↔ pypy/module/gc/moduledef.py:5: adding gc.get_count is a CPython-surface adaptation; PyPy’s gc module exposes no such binding. The API exists in the pinned CPython tests (lib-python/3/test/test_gc.py:382). Its currently incorrect generation behavior is reported in section 2.

  • majit/majit-gc/src/nursery.rs:95 ↔ rpython/memory/gc/incminimark.py:597: page-aligned Rust allocations plus region::protect replace PyPy’s llarena rotating-nursery implementation. This is an implementation-language adaptation on supported POSIX/Windows targets.

@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: b6632eac95

ℹ️ 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 +5899 to +5901
if seen.insert(addr) {
self.debug_check_object(addr);
pending.push(addr);

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 Reject headerless roots before reading GC headers

When PYPY_GC_DEBUG is enabled and an extra root walker yields one of pyre's off-GC/headerless values, record immediately calls debug_check_object, which reads header_of(addr); the later pending traversal does the same. The normal major-root path explicitly rejects such values with is_managed_heap_object, because interpreting the preceding bytes as a GcHeader can spuriously panic, trace an arbitrary type, or fault. Apply the same validity/ownership handling here before queuing a root.

AGENTS.md reference: AGENTS.md:L223-L226

Useful? React with 👍 / 👎.

Comment on lines +5904 to +5906
for root in self.enumerate_root_walker_values() {
if !root.is_null() {
record(root.0, &mut seen, &mut pending);

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 Include finalizer and prebuilt roots in debug traversal

With PYPY_GC_DEBUG enabled, this starts the consistency walk from enumerate_root_walker_values, but the collector's enumerate_all_root_values additionally includes prebuilt_root_objects, registered finalizer objects, and pending finalizer queues. An object reachable only through one of those GC-owned structures is therefore never passed to debug_check_object, so stale pointers or invalid flags in that population are silently missed; port GCBase.debug_check_consistency's enumerate_all_roots root set instead.

AGENTS.md reference: AGENTS.md:L223-L226

Useful? React with 👍 / 👎.

`Nursery` allocates its arena page-aligned and page-rounded so it can be
handed to `mprotect`, and gains `install_debug_rotating_nurseries` /
`debug_rotate` for incminimark.py's `post_setup` and
`debug_rotate_nursery`: six spare arenas, each inaccessible until it
reaches the front of the ring.

`GcConfig` gains `debug` (`PYPY_GC_DEBUG`) and `gc_nursery_debug`
(`PYPY_GC_NURSERY_DEBUG`), both in `GC_ENV_NAMES`. `with_config` installs
the ring when `debug` is non-zero and turns the garbage fill on for
`gc_nursery_debug`; `_minor_collection`'s barrier rebuild rotates on the
no-pinned-objects arm, and `debug_check_consistency` runs after every
minor collection when `debug >= 2`.

`region` becomes a dependency of the non-wasm32 build; `HAS_PROTECT` is
false on wasm32, where `install_debug_rotating_nurseries` returns without
allocating.

Fills in the `MAJIT_GC_NURSERY_POISON` ledger entry, whose nursery half
is the same fill.

Assisted-by: Claude
`pin_root` returns the normalized live word and is `#[must_use]` with a
message that sanctions `let _ =` for liveness-only pins. Six functions
spelled it that way and then read the pre-pin local, so the value handed
to the comparison was the one `normalize_published_slot` replaced:

  scan_dict_key_reentrant        key.obj
  scan_set_key_reentrant         key.obj
  w_set_contains_key_for_update  stored.obj, key.obj
  w_set_remove_key_for_update    stored.obj, key.obj
  error_is_exception             err.exc_object
  fileio_writebuf                view

At the four walk sites the read-back that repairs it already ran, one or
two statements below the comparison it was needed for. `err` is a shared
reference, so that one takes a local instead of writing back.

`fileio_writebuf`'s pin also had no `push_roots` bracket, so its slot
stayed on the shadow stack after the call returned.

Assisted-by: Claude
… walker

`CompiledCodeRegistry`, `CompiledCodeRegion`, `SafepointMap`,
`SafepointEntry`, `scan_frame` and `find_region` were never populated and
never consulted outside `collector.rs`'s own test module. Their only
non-test toucher, `MiniMarkGC::jit_free`, retained over a vec that is
always empty and has no production caller, so the `GcAllocator::jit_free`
trait method and its `GcHandle` forward go with it. `GcMap` had no
consumer besides `SafepointEntry` and follows.

`set_active_extra_root_walker` has no caller repo-wide, so
`ACTIVE_EXTRA_ROOT_WALKER` was never set and the two
`walk_active_extra_roots` calls in the minor drag-out and the labelled
root enumeration could not yield a root. The multi-registrar
`shadow_stack::walk_extra_roots` runs at both sites already.

Three of the removed types documented a role no backend performs:
`SafepointMap` said the Cranelift backend builds them during
compilation, `GcMap` said the backend records one at each guard.

Assisted-by: Claude
Comments only; no behaviour changes.

`at_outermost_activation` tests `EVAL_NESTING <= 2`, so module level and
one called function's loop both collect and refusal starts at depth 3.
`gc_interp::safepoint`'s doc and the `EvalActivationGuard::enter` comment
both described it as firing at the outermost activation only.

`get_possibly_forwarded_header` called its nursery case latent on the
grounds that every finalizer-queue registrant is stable-allocated.
`list_descr_new` takes its header from `w_list_new`, the collecting
nursery arm, and registers a finalizer for a builtin-layout subclass.

`rescan_major_stack_roots_black_and_drain` said a JitFrame lives in the
old generation. `alloc_off_gc_jitframe` returns `alloc_zeroed` memory
outside the GC heap, and it is reached only from the dynasm entry frames
and `dynasm_realloc_frame`; the other jitframe paths are nursery bumps.

`register_gc_alloc_collecting_hook` named the elidable bigint payload
helpers as its callers. The rooted sibling also carries every list
header, `w_weakref_new`, and builtin `str()`.

Assisted-by: Claude
The check was one `debug_assert_eq!` on `raw_malloc_might_sweep`, gated
at its call sites. Upstream opens the body with `if self.DEBUG:` and
asserts for real, so move the gate inside and drop the `debug_assert!`:
the checks now survive a release build, which is what `PYPY_GC_DEBUG`
arms them for, and a run that does not set it pays one load.

Adds the two list invariants the body opens with — no young
raw-malloced objects and no young objects with weakrefs — and the heap
half from `GCBase.debug_check_consistency`: enumerate every root, trace
the reachable graph, and run `debug_check_object` on each object once.
`debug_check_object` asserts that a pinned object is in the nursery, and
that any other is not and carries neither `GCFLAG_VISITED_RMY` nor
`GCFLAG_PINNED` out of the collection.

`OldGen::young_rawmalloced_is_empty` is the accessor the first of those
invariants reads.

`rawmalloc_sweep_candidates_require_sweeping_state` now arms the level
and loses its `#[cfg(debug_assertions)]`, since the assertion it expects
is no longer compiled out of a release build.

Assisted-by: Claude
`register_mutator`'s doc said unregistration was armed by a
`GcMutatorRegistration` thread-local in `init_gc_subsystem`. No such type
exists. The pairing is `pyre_interpreter::module::thread`'s `RuntimeThread`,
armed by `enter_runtime_thread` touching `RUNTIME_THREAD` right after this
call, and that order is load-bearing: `register_mutator` is where the thread
first touches all five root structures, so their destructors are registered
before `RuntimeThread`'s and run after it.

The two `TODO:` markers, on `pop_to` and `depth()`, described the `try_with`
both already use, and cited a `Drop` impl for `JitDriver` that does not exist.
State them as the rationale they are, and record that no caller is TLS-owned
today.

`try_pop_to`'s doc claimed it differs from `pop_to` by tolerating a
torn-down thread-local. Both reach it through `try_with`; the difference is
the balance assertion. Its callers are `Drop` impls, while
`ExportedState::release_roots` and `Trace::release_roots` are `Drop` paths
that keep the assert, so say it is a choice per site.

Assisted-by: Claude
… barrier

`TypeInfoLayout`'s doc said the reserved word makes the size match
`rffi.sizeof(GCData.TYPE_INFO) = 16`. `GCData.TYPE_INFO` is four words —
infobits, customdata, fixedsize, ofstoptrs — so 32 bytes on 64-bit, and
`VARSIZE_TYPE_INFO` extends it to eight; `get_type_id` allocates the narrow
struct for a fixed-size type and the wide one for a varsize type, so upstream's
per-entry size is not uniform. The row here is smaller, not equal, and the
reserved word is there to keep `TypeEntry`'s stride a power of two. The same
doc already said 32 bytes two sentences later.

`MAX_TYPES`'s doc called 1024 generous headroom above "the dozen-or-so types
pyre currently registers". A stock interpreter registers 867. Record the count
and the command that re-derives it.

`writebarrier_before_move` has no non-test caller and its guard rejects every
object pyre can build, because the only non-test setter of `HAS_CARDS`,
`alloc_in_oldgen_with_cards`, has no production caller either. Record which
sites owe the call when that changes: `W_ListObject`'s `object_insert`,
`object_remove` and `object_drain` shift items with a bare `ptr::copy`, where
upstream reaches the barrier through `rgc.ll_arraymove`.

Assisted-by: Claude
`moduledef.py` binds no `get_count`, `set_threshold`/`get_threshold`,
`set_debug`/`get_debug` or `freeze`/`unfreeze`/`get_freeze_count`, so these
answer to 3.14 alone and have no implementation to follow. Say so in the module
doc, and record that nothing grades them: `lib-python/conftest.py`'s testmap
skips `test_gc` as an implementation detail and `cpython_tests/run.py` carries
that skip forward, so the assertions live in `extra_tests/snippets/`.

`GC_THRESHOLD`'s doc said the collector has no generational allocation counters
to drive. It has counters; none shares `threshold0`'s unit. What schedules a
collection is `get_total_memory_used` against
`next_major_collection_threshold`, a byte reading, and the one knob retunable
after construction, `set_max_heap_size`, is a byte ceiling; an old-gen live
object count does exist as the arena collection's `live_objects`.

`GC_DEBUG`'s doc named no flag. `DEBUG_SAVEALL` is the one that shows why the
word cannot drive anything: with no refcount to have reclaimed acyclic garbage
first, the population reaching `OldGen::sweep_arenas_step`'s free-or-keep
callback is everything that died since the last major — 317 objects over 12
type ids inside the single collection `test_saveall` brackets expecting one.

`freeze`'s doc did not say why rooting the live set is not the same operation:
a frozen object in 3.14 is skipped by the cyclic collector but still reclaimed
by refcount, while a rooted one is immortal until `unfreeze` with its `__del__`
deferred.

Assisted-by: Claude
…st major

Two of the three elements 3.14 reports are collection counts, not object
counts: element 1 is the generation-0 collections run since generation 1 was
collected, element 2 the generation-1 collections since generation 2 was. Under
the generation mapping `NUM_GENERATIONS` already publishes, a minor is the
generation-0 collection and a major collects both older generations, so
element 1 is the minors since the last major and element 2 is zero because
nothing collects generation 1 alone.

Add `minor_collections_at_major_end` and `GcAllocator::minor_collections_since_major`,
installed by all three backends through
`set_active_minor_collections_since_major`.

The sample is taken at the FINALIZING -> SCANNING transition, not in
`finish_incremental_cycle`. That function is the sweep-to-finalize seam, and
`do_collect_full` runs a minor before every remaining step, so one more minor
still runs after it; sampling there left `gc.collect()` reporting a minor the
collection had not finished running. `minors_accumulate_until_a_major_finishes`
covers both the accumulation and the reset.

Element 0 stays zero and now says why. The allocation seam is keyed by a majit
type id, and the tracked predicate is not a function of that key —
`cpython_object_is_gc` reaches the object's type and, for a type object, the
object itself, so one type id covers a tracked heap type and an untracked
static one. A decidable bit would still undercount: every backend emits the
nursery bump inline and merges several objects into one, and a virtualized
allocation is removed outright.

Measured on `pyre-dynasm`: `(0, 0, 0)` at startup, `(0, 2, 0)` after 200000
appends, `(0, 0, 0)` after `gc.collect()`.

Assisted-by: Claude
collector.rs, nursery.rs, shadow_stack.rs and trace.rs opened with a
`///` block describing the module, which documents the `use` statement
that follows it rather than the module. Each of those imports is
private, so the text reached no rendered page. The other eleven
majit-gc modules already use `//!`.

Assisted-by: Claude
The `Nursery::reset` comment named `clear_gc_fields` as the only store
the zero-fill covers. `NewArrayClear` is the second: the wasm codegen
lowers it exactly like `NewArray`, and `wasm_jit_alloc_array` stamps the
length and nothing else, so the clear half comes from the recycled bytes
being zero. The `ZeroArray` the pass emits never reaches wasm, and the
codegen declines a trace carrying one rather than rely on the allocator.

Also record that the omission covers every op rather than some
allocation shapes, that `remove_ref_constants` does run on wasm, and
that a request routed to old-gen outright is cleared by
`alloc_in_oldgen_clear` on every target, so only the nursery-overflow
spill needs `clear_nursery_substitute`.

Assisted-by: Claude
…upstream duty

The registry doc said the table drops when the loop token is freed. On
cranelift a bridge's table is pinned a second time by every `BridgeData`
that can dispatch to the bridge, so it outlives the CLT it was registered
against while a fail descr in another token holds one; only dynasm has
the CLT as sole holder. Nothing dispatches `Backend::free_loop` either —
the release is `Arc` drop at memmgr eviction.

Also record why no `clear_gcref_tracer` analog is owed: upstream zeroes
`array_length` because its slot array is reserved inside the code block
and freed with it, while here the slots are the table's own `Box`.

Assisted-by: Claude
The module doc ended in a phase plan whose last entry read "this commit",
and it never said the decisive fact: upstream's transformer inserts
push_roots/pop_roots automatically around the operations that can
collect, while every bracket here is hand-written. Replace the plan with
the current state, the deviation, and the census of hand-written sites
with the command that re-derives it — 1376 scopes, 2604 pins and 3571
read-backs across 163 files.

Also name what the missing pass gates: the born-old interpreter
allocation in gc_interp and the non-moving safepoint major.

Assisted-by: Claude
`gc.collect(n)` bounded its generation against `NUM_GENERATIONS` and then
discarded it, so every generation ran a full collection and `gc.get_count`'s
second element could only move through allocation.  Route the generation to
`MiniMarkGC::do_collect` -- the `incminimark.py collect(gen)` port, which had
no callers -- through a new `GcAllocator::collect_generation` and the
active-backend hook, the path `get_objects` already carries a generation over.
`gc_sync`'s singleton is not the GC cranelift and wasm own, so the removed
`majit_gc::gc_collect_gen` could not have served this.

The declared default generation was 0 and is now the oldest, so a bare
`gc.collect()` keeps running a full collection.

`gc.collect(0)` now moves `gc.get_count()[1]` the way 3.14 moves it --
(0,0,0) -> (0,1,0) -> (0,2,0) -> (0,0,0) on CPython, dynasm and cranelift
alike.  Rewrite the snippet around that rather than around appending 200000
tuples until a minor happens to land.

Assisted-by: Claude
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