Investigate why crc census and score load the target loader in process #1390

Closed
opened 2026-10-25 21:29:39 +00:00 by jared · 4 comments
Owner

Problem

A measurement tool loads the code of its target project into its own process. Loaders pile up across runs, and the loads force every os-sdlc bin test into a fork.

Observed

os-sdlc crc census and score load the target project's Zeitwerk loader file into the caller's process (ExpectedConstants#loaded). Loaders pile up across in-process runs.
Tests isolate it by forking Boot.run (tests/support/in_process_bin.rb).
Every os-sdlc bin test now runs in a fork, so the #1327 journey tracer cannot see os-sdlc bin calls.

Reproduce

n/a: this is a design question, not a failure with a command.

Approach

Answer these questions and state a decision. Why does a measurement tool load the target's code into its own process? Can it run in a child process, or read the constants without loading? Record the journey tracer gap for #1327.

Origin

  • Trigger: in-process test conversion of crc bin tests (finding-a) in fast-batch-15
  • Improvised this session: tests fork Boot.run (tests/support/in_process_bin.rb)
  • Chain: loaders pile up ← ExpectedConstants#loaded loads the target loader file ← IMPROVISATION (fork in tests/support/in_process_bin.rb)
  • Root candidate: none found; searched ExpectedConstants, census, Zeitwerk loader, in_process_bin, fork
  • VERDICT: CREATE
  • Where: os-sdlc crc census and score, ExpectedConstants#loaded
  • Session: 01c29715-c945-43b4-86fc-6c5fc6c4dd3f
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/01c29715-c945-43b4-86fc-6c5fc6c4dd3f.jsonl
## Problem A measurement tool loads the code of its target project into its own process. Loaders pile up across runs, and the loads force every os-sdlc bin test into a fork. ## Observed os-sdlc crc census and score load the target project's Zeitwerk loader file into the caller's process (ExpectedConstants#loaded). Loaders pile up across in-process runs. Tests isolate it by forking Boot.run (tests/support/in_process_bin.rb). Every os-sdlc bin test now runs in a fork, so the #1327 journey tracer cannot see os-sdlc bin calls. ## Reproduce n/a: this is a design question, not a failure with a command. ## Approach Answer these questions and state a decision. Why does a measurement tool load the target's code into its own process? Can it run in a child process, or read the constants without loading? Record the journey tracer gap for #1327. ## Origin - Trigger: in-process test conversion of crc bin tests (finding-a) in fast-batch-15 - Improvised this session: tests fork Boot.run (tests/support/in_process_bin.rb) - Chain: loaders pile up ← ExpectedConstants#loaded loads the target loader file ← IMPROVISATION (fork in tests/support/in_process_bin.rb) - Root candidate: none found; searched ExpectedConstants, census, Zeitwerk loader, in_process_bin, fork - VERDICT: CREATE - Where: os-sdlc crc census and score, ExpectedConstants#loaded - Session: 01c29715-c945-43b4-86fc-6c5fc6c4dd3f - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/01c29715-c945-43b4-86fc-6c5fc6c4dd3f.jsonl
Author
Owner

Occurrence: Stop crc census and score from loading the target Zeitwerk loader into the caller
Seen: plugins/os-sdlc/tests/support/in_process_bin.rb; os-sdlc crc census and score
Run: Session 6c6b6eae-4ebd-4ac4-9b32-535875a6b39b, fast-batch-15 ruling R1
Adds: fast-batch-15 ruling R1 (predicted, 62%) keeps the forked InProcessBin for now and depends on this ticket. Each forked test hides the os-sdlc bin calls from the #1327 use_case/journey tracer. A fix here lets InProcessBin drop the fork. Evidence: docs/crc/os-sdlc/fast-batch-15/rulings.tsv row R1.

Occurrence: Stop crc census and score from loading the target Zeitwerk loader into the caller Seen: plugins/os-sdlc/tests/support/in_process_bin.rb; os-sdlc crc census and score Run: Session 6c6b6eae-4ebd-4ac4-9b32-535875a6b39b, fast-batch-15 ruling R1 Adds: fast-batch-15 ruling R1 (predicted, 62%) keeps the forked InProcessBin for now and depends on this ticket. Each forked test hides the os-sdlc bin calls from the #1327 use_case/journey tracer. A fix here lets InProcessBin drop the fork. Evidence: docs/crc/os-sdlc/fast-batch-15/rulings.tsv row R1.
Author
Owner

Findings (2026-10-27, takedown track; read-only investigation, not built).

Mechanism: plugins/os-sdlc/lib/os_sdlc/expected_constants.rb:46 (loaded) runs load loader_path on the target's loader file, then :47 collects every Zeitwerk::Loader from ObjectSpace whose dirs sit under lib_root. Callers: CRC census crc/census/settings.rb:47, score crc/score/run.rb:26, and the detectors detector/{cycle_edges,error_lineage,holders}/signals.rb (same leak, not named in this ticket). Each run leaves a new loader registered in Zeitwerk::Registry, autoload hooks on Object and the target namespaces, and whatever the loader file requires; a second run re-executes the file and ObjectSpace returns the stale loaders too.

Why it loads: it needs all_expected_cpaths (path -> constant) and dirs(namespaces: true) (root constant). Only the loader config (inflections, collapse, ignore, push_dir) produces those faithfully; a Prism parse would be a heuristic. It never calls eager_load.

Smallest fix (proposed): in ExpectedConstants#loaded, read cpaths and the root constant, then unload and unregister each loader it created (unregister is @experimental in zeitwerk 2.8.3, loader.rb:365). Alternative B: run the loader file in a child and emit JSON (isolates fully, keeps a fork per caller). Open risk: when the target is os-sdlc itself, OsSdlc.loader may already be set.

Proposed tickets:
A. "ExpectedConstants leaves no loader behind" — 1. After ExpectedConstants.of, the Zeitwerk::Registry loader count is unchanged. 2. Two runs in one process return the same cpaths. 3. The root constant is unchanged. (Fixture project test, red before the fix.)
B. "InProcessBin runs CRC bins without a fork" (after A) — 1. InProcessBin#forked (in_process_bin.rb:58) is gone. 2. tests/crc/*_bin_test.rb pass. 3. Suite time is not worse.

Findings (2026-10-27, takedown track; read-only investigation, not built). Mechanism: `plugins/os-sdlc/lib/os_sdlc/expected_constants.rb:46` (`loaded`) runs `load loader_path` on the target's loader file, then :47 collects every `Zeitwerk::Loader` from `ObjectSpace` whose dirs sit under `lib_root`. Callers: CRC census `crc/census/settings.rb:47`, score `crc/score/run.rb:26`, and the detectors `detector/{cycle_edges,error_lineage,holders}/signals.rb` (same leak, not named in this ticket). Each run leaves a new loader registered in `Zeitwerk::Registry`, autoload hooks on `Object` and the target namespaces, and whatever the loader file requires; a second run re-executes the file and `ObjectSpace` returns the stale loaders too. Why it loads: it needs `all_expected_cpaths` (path -> constant) and `dirs(namespaces: true)` (root constant). Only the loader config (inflections, collapse, ignore, push_dir) produces those faithfully; a Prism parse would be a heuristic. It never calls `eager_load`. Smallest fix (proposed): in `ExpectedConstants#loaded`, read cpaths and the root constant, then `unload` and `unregister` each loader it created (`unregister` is `@experimental` in zeitwerk 2.8.3, loader.rb:365). Alternative B: run the loader file in a child and emit JSON (isolates fully, keeps a fork per caller). Open risk: when the target is os-sdlc itself, `OsSdlc.loader` may already be set. Proposed tickets: A. "ExpectedConstants leaves no loader behind" — 1. After `ExpectedConstants.of`, the `Zeitwerk::Registry` loader count is unchanged. 2. Two runs in one process return the same cpaths. 3. The root constant is unchanged. (Fixture project test, red before the fix.) B. "InProcessBin runs CRC bins without a fork" (after A) — 1. `InProcessBin#forked` (`in_process_bin.rb:58`) is gone. 2. `tests/crc/*_bin_test.rb` pass. 3. Suite time is not worse.
Author
Owner

Filed from this ticket: #1420 (ExpectedConstants unregisters the loaders it creates) and #1421 (InProcessBin runs CRC bins without a fork; blocked by #1420).

Filed from this ticket: #1420 (ExpectedConstants unregisters the loaders it creates) and #1421 (InProcessBin runs CRC bins without a fork; blocked by #1420).
Author
Owner

Resolution

Done: Investigated (comment 2026-10-27): ExpectedConstants loads the target loader because only the loader config yields cpaths and the root constant faithfully. Fixed by #1420 (dispose the loaders it creates) and #1421 (InProcessBin no longer forks).

Evidence: #1420 closed, main 9e1f144f; #1421 closed, main bb9f1f88. With no fork, the #1327 journey tracer can see os-sdlc bin calls in tests.

Follow-ups: none

## Resolution **Done:** Investigated (comment 2026-10-27): ExpectedConstants loads the target loader because only the loader config yields cpaths and the root constant faithfully. Fixed by #1420 (dispose the loaders it creates) and #1421 (InProcessBin no longer forks). **Evidence:** #1420 closed, main 9e1f144f; #1421 closed, main bb9f1f88. With no fork, the #1327 journey tracer can see os-sdlc bin calls in tests. **Follow-ups:** none
jared closed this issue 2026-10-27 16:57:08 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1390
No description provided.