Skip to content

fix(profiler): scan the object graph breadth-first - #25

Merged
tommy-gun merged 1 commit into
mainfrom
codex/fix-collection-module-profiling
Jul 26, 2026
Merged

tommy-gun merged 1 commit into
mainfrom
codex/fix-collection-module-profiling

Conversation

@tommy-gun

Copy link
Copy Markdown
Contributor

Follow-up to #24. That PR fixed collection discovery for issue #23, but introduced a regression that showed up on the first live capture.

The problem

Discovery walked the object graph depth-first against a global visit budget (maxVisitedObjects: 2000). Hitting the budget set #discoveryStopped, which ended the entire scan — not just that branch. So the first object-valued own property of stateObj could consume the whole budget and starve every sibling enumerated after it.

#24 removed the guard that had been shielding the old implementation from this:

if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return; }

The pre-#24 walk never entered arrays, so bulk data never cost anything. Once arrays and Map/Set values became traversable — which is exactly what issue #23 needed — nothing replaced that protection.

Observed on rss-feed (_store holds a Map of 27 feed sources, each with an array of parsed items): only _store.* got instrumented. _indicator.*, _poller.* and _repository.* were never reached, and because the calling functions were missing, every event collapsed to depth: 0 — the call tree, and with it the Self-vs-Total distinction, was gone.

Reproduced synthetically at realistic proportions (27 sources × 40 items, default limits):

patchedFunctions _store _indicator _poller _repository
depth-first 26 ✅ ❌ ❌ ❌
breadth-first 37 ✅ ✅ ✅ ✅

The fix

Walk in level order. Methods sit near the root while bulk data (cached items, parsed entries) sits deeper, so breadth-first spends the budget on shallow objects first and truncates the deep data instead. truncated is still true in both rows above — the difference is what gets cut.

  • #patchTree is replaced by #patchGraph (level-order queue) + #visitObject (single-node work). The four walk methods no longer recurse; they receive an enqueue callback.
  • Objects are claimed when queued, not when processed, so an object referenced by several parents is never scheduled twice.
  • Exhausting maxVisitedObjects now only ends the loop and marks the scan truncated. The hard #discoveryStopped flag stays reserved for maxPatchedFunctions, where a global stop is the intended behaviour.

No changes to #patchObject or the restore path — wrapper installation and descriptor restoration are untouched.

Notes for the reviewer

  • testDataHeavySiblingDoesNotStarvePeers mirrors the rss-feed shape with maxVisitedObjects: 30 so the budget genuinely runs out. I verified it fails against the depth-first implementation — otherwise it would not be catching anything.
  • One thing from the pre-Fix collection module profiling #24 captures will not come back, deliberately: _store._sources.values. The old code shadow-patched Map.prototype.values onto the instance, which measures the GJS runtime rather than the extension.
  • Full suite green: stop hook exit 0 (ruff, eslint, mypy, syntax, WSL headless, pytest 59 passed), GJS bridge suite 9/9, inspector suite OK.
  • Still needs live verification on GNOME. The proof here is synthetic — a fresh rss-feed capture should now contain _indicator.*, _poller.*, _repository.* and, importantly, non-zero depth values.

🤖 Generated with Claude Code

Discovery used a depth-first walk against a global visit budget, so the
first object-valued own property of stateObj could consume the whole
budget and stop the scan for every sibling after it. Extensions that hold
cached data in the first sibling -- rss-feed keeps a Map of feed sources,
each with an array of parsed items -- ended up with only that subtree
instrumented; the objects carrying the extension's actual behaviour were
never reached, and the call tree collapsed to depth 0.

The array guard that shielded the previous implementation was removed to
fix collection discovery (issue #23) without anything replacing it.

Walk in level order instead. Methods sit near the root while bulk data
sits deeper, so breadth-first spends the budget on shallow objects first
and truncates the deep data. Objects are claimed when queued so shared
references are not scheduled twice, and exhausting maxVisitedObjects now
only ends the scan and marks it truncated rather than tripping the hard
discoveryStopped flag, which stays reserved for maxPatchedFunctions.

Add a regression test covering a data-heavy first sibling; it fails
against the depth-first implementation.

Co-Authored-By: Claude Opus 5 <[email protected]>
@tommy-gun
tommy-gun merged commit 5ffa9c9 into main Jul 26, 2026
8 checks passed
@tommy-gun
tommy-gun deleted the codex/fix-collection-module-profiling branch July 27, 2026 18:54
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