Skip to content

feat(stability): add memory-leak skill - #81

Open
MajorLift wants to merge 5 commits into
mainfrom
jongsun/add/memory-leak-hunt-skill-v2
Open

feat(stability): add memory-leak skill#81
MajorLift wants to merge 5 commits into
mainfrom
jongsun/add/memory-leak-hunt-skill-v2

Conversation

@MajorLift

@MajorLift MajorLift commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Adds memory-leak to the stability domain — a two-phase retention review for JavaScript/TypeScript.

A memory leak is a retention path: something acquires a reference (a listener, a timer, a map entry, a subscription) and never releases it at the boundary where it should. The skill's core move is to pair every acquire with its release.

Phase 1 — static, from the diff (the lead)

Enumerate the retention primitives the change introduces and, for each, name the holder → held set → outlived boundary triple, then pair the acquire against its release:

Primitive Acquire Release to pair it with
Event listener .on / addEventListener off / removeEventListenersame handler reference
Timer setInterval / recurring setTimeout clearInterval / clearTimeout
Pending registry map.set(id, {resolve}) map.delete(id) on every completion path
Subscription .subscribe() / messenger.subscribe the returned unsubscribe, at teardown
Module singleton assignment to module/this scope reset on replacement
Growing collection push / set / add a drain or bounded eviction policy

Findings are scoped to what the diff adds versus what pre-exists — a pre-existing unpaired primitive is not this PR's finding.

Phase 2 — runtime, only on escalation

DevTools/CDP heap snapshots over N cycles, the retainer graph, detached-node count, and a falsifying lifecycle test — reached for only where the static read cannot pair a primitive.

Why this ordering. A heap snapshot is expensive and slow to interpret; the decisive cheap step is the read a reviewer already performs. Most leak claims are settled without ever taking a snapshot, and the ones that aren't have a specific unpaired primitive to point the profiler at.

Files

domains/stability/skills/memory-leak/skill.md, references/heap-investigation.md (snapshot workflow and retainer-graph reading), scripts/retention-scan.py (acquire/release pairing over a diff), scripts/heap-over-cycles.example.ts.

Shipped experimental; no repos/ overlay, so it applies to extension, mobile, and core alike. Commits GPG-signed.

stability is a new domain — a leak's failure mode is the tab dying or the service worker being killed, which is availability rather than throughput, so it sits better here than under coding (the catch-all) or performance. The PR registers it in .github/CODEOWNERS, defaulted to the platform teams.

Note for reviewers

The description is 1,027 characters, within the 1,536 budget in #47. It is written for skill-discovery matching — the trigger cues are what route a request here rather than to a sibling skill, so length there buys selection accuracy.

Showcase — what it produces on two real PRs

Both are in metamask-extension, both merged, and they are deliberately opposite cases: one where the honest answer is no leak, one where there is.

#40684 — extract patch-store substream — proving absence. Phase 1 enumerates the retention primitives the diff introduces and pairs every acquire with its release. When all pair, the finding is that the change adds no unreleased retention — stated as a bounded claim about the diff, not as "no leak anywhere". No heap snapshot needed, so the cost is a careful read.

#44352 — Firefox detached-window leak — a real one. Phase 1 finds nothing to pair, because the leak is a native object's lifecycle rather than a listener or timer the diff adds: reading win.documentPictureInPicture.requestWindow lazily instantiates a per-window object whose preserved-wrapper cycle Firefox's collector cannot break. The evidence is Phase 2:

  • Slope, not a single reading — figures reported in #44352: retained heap climbing ~105 MB per popup open/close, linearly, with 30 cycles reaching 3.56 GB and surviving a forced GC. A single reading gives occupancy; the slope across cycles is what distinguishes a leak from a working set.
  • Retainer graph — names the holder and the boundary where the collector should reclaim it.
  • Intervention test — the fix reads the constructor prototype instead of the instance getter, so no per-window instance is created. Changing only the accessor the graph named, and watching the growth flatten, is what makes the causal claim rather than a correlational one.

The pair is the point: the same discipline that proves absence in #40684 localises a real leak in #44352, and neither answer is reached by looking harder at a heap dump.


Validation runs

Trial runs of this PR's skills against merged metamask-extension PRs nobody flagged. Every claim was re-verified against the real diff before posting. Clean results are included on purpose — a skill that only ever reports problems cannot be calibrated.

PR Skill Verdict Finding
#42823 memory-leak Gap listener registered, never removed; latent, charged as such
#45035 memory-leak Clean 2 primitives introduced, both paired

Each comment carries a trial-run disclaimer and links back here for feedback.

Two-phase retention review for JavaScript/TypeScript.

Phase 1 is a static read of a diff: enumerate the retention primitives the change
introduces — listeners, timers, pending-request registries, subscriptions, module
singletons, growing collections — and pair every acquire with its release site.
A primitive with a teardown is safe; one without is the finding.

Phase 2 escalates to DevTools/CDP heap snapshots only for a primitive the read
cannot pair. Leading with the read rather than the instrument settles most leak
claims without ever taking a snapshot.
@MajorLift MajorLift changed the title feat(coding): add memory-leak-hunt skill feat(coding): add memory-leak-hunt skill Jul 30, 2026
@MajorLift
MajorLift marked this pull request as draft July 30, 2026 14:03
The skill covers runtime retention behaviour, not code authoring, and `coding`
reads as language- and style-level guidance. Registers `/domains/stability/` in
CODEOWNERS alongside the other platform-owned domains.
@MajorLift MajorLift changed the title feat(coding): add memory-leak-hunt skill feat(stability): add memory-leak-hunt skill Jul 30, 2026
@MajorLift
MajorLift marked this pull request as ready for review July 30, 2026 18:15
@MajorLift

MajorLift commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Context budget

What this PR costs an agent, measured from an install rather than read from the diff. Three tiers, and only the first is unavoidable.

Skill Frontmatter Selected + refs & knowledge
memory-leak 992 chars ~2,701 tok ~5,225 tok

Frontmatter is the only tier paid unconditionally — every agent loads it on every run once the skill is installed, used or not, because it is what the agent reads to decide relevance. The 28 skills across the eleven open skill PRs sit at a median of ~1,716 tokens selected and ~1,860 with references followed. All are within the 1,536-character description budget.

Selected is paid only when the agent picks the skill. + refs & knowledge is the ceiling if every bundled reference is then read; it is a worst case, not an expectation.

Method

tools/install --repo metamask-extension --maturity experimental against this branch at affac8f1f, measured per installed skill directory. Repo overlays are merged into the emitted SKILL.md, so they land in the selected tier rather than being missed by a source-byte count. Token figures are bytes/4 — a proxy for scale, not accounting.

These figures are pinned to the commit above and drift on every push; #96 tracks automating them.

`hunt` disambiguated nothing. No sibling skill targets memory leaks, and none
would: this one already covers both halves — the static retention read from the
diff and the heap investigation when the read cannot settle it — so there is no
detection/diagnosis split for the suffix to mark.

Installed as `mms-memory-leak`.
@MajorLift MajorLift changed the title feat(stability): add memory-leak-hunt skill feat(stability): add memory-leak skill Jul 31, 2026
Missed when the rename swept the other branches: the description, the section
heading, and two prose references all still named `pr-validate`. The evidence
category is now linked to the catalog rather than named bare.
The listener pass matched only `.on('event', handler)` and
`.addListener('event', handler)` — a method named exactly `on` or `addListener`
with a quoted event name. Any form carrying the event in the method name was
invisible.

Running it on extension#42823 returned "no retention path INTRODUCED" for a file
containing `background.onNotification(routeMessengerEventNotification)` with zero
`removeOnNotification` call sites anywhere in `ui/`. A clean verdict over a real
unpaired listener is the worst output this script can produce, because it reports
what the pattern can see as though it were what is there.

Adds `onXxx(handler)`, `subscribe(handler)`, `addEventListener`, and
`addXxxListener` forms, each paired against its corresponding release
(`removeOnXxx`/`offXxx`, `unsubscribe`, `removeEventListener`, `removeXxx`).
The same file now reports the primitive as NEW and OPEN.

Verified no regression: `client.on('connected', connected)` and its siblings in
qr-sync-controller.ts are still detected by the quoted-event pass.

Also drops a hardcoded `PR #40684` from the header, which printed on every run
whatever was scanned, and a re-run hint naming a script that does not exist.
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