[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M699AQXNCBYPHN74F4E7NJBF":3,"skill-same:01M699AQXNCBYPHN74F4E7NJBF":98},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":49,"ownerHtmlUrl":50,"ownerBlogUrl":51,"ownerDescription":52,"updatedAt":53,"install":54,"resources":67},"01M699AQXNCBYPHN74F4E7NJBF","heap-snapshot-analysis",null,"Analyze V8 heap snapshots to investigate memory leaks and retention issues. Use when given .heapsnapshot files, asked to compare before\u002Fafter snapshots, asked to find what retains objects, or investigating why objects survive GC. Provides snapshot parsing, comparison, retainer-path helpers, and scratchpad scripts.","microsoft\u002Fvscode","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M6998NK2WED5M549DX06VBCH",195270,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"heap snapshot analysis","analysis","heap","snapshot","helpers","memory","when","path","comparison","retention","scripts","objects","files","snapshots","gc","use","analyze","why","parsing","issues","2026-11-10 10:12:30",false,"---\nname: heap-snapshot-analysis\ndescription: \"Analyze V8 heap snapshots to investigate memory leaks and retention issues. Use when given .heapsnapshot files, asked to compare before\u002Fafter snapshots, asked to find what retains objects, or investigating why objects survive GC. Provides snapshot parsing, comparison, retainer-path helpers, and scratchpad scripts.\"\n---\n\n# Heap Snapshot Analysis\n\nInvestigate memory leaks from V8 heap snapshots (`.heapsnapshot` files). This skill starts when snapshots already exist: either the user provided them, DevTools exported them, or another workflow produced them. Use the helpers here to compare snapshots, group object deltas, and trace retainer paths.\n\n## IGNORE Prior Investigations\n\n**Start every investigation fresh.** Do NOT read, consult, or be influenced by prior investigations found in:\n\n- `\u002Fmemories\u002F` (user, session, or repo memory)\n- `.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fscratchpad\u002F` (previous dated subfolders and their `findings.md` files)\n- Any other notes from earlier sessions\n\nPrevious findings can bias the analysis toward suspects that are no longer relevant, or cause the agent to skip steps and jump to conclusions. Let the current snapshots speak for themselves. Only reference prior work if the user explicitly asks you to.\n\n## When to Use\n\n- User provides `.heapsnapshot` files (before\u002Fafter a workflow)\n- User has heap snapshots captured by another skill or script\n- Need to find what retains disposed objects (retainer path analysis)\n- Comparing object counts\u002Fsizes between two snapshots\n- Investigating why particular objects survive GC\n\n## Workflow\n\nIf the user needs the agent to launch VS Code, drive a scenario, and capture snapshots first, use the VS Code performance workflow skill before returning here for low-level snapshot analysis.\n\n### 1. Parse Snapshots\n\nUse the helpers in [parseSnapshot.ts](.\u002Fhelpers\u002FparseSnapshot.ts) to load snapshots. The files are often >500MB and too large for `JSON.parse` as a string — the helpers use Buffer-based extraction. In scratchpad scripts, import helpers from `..\u002Fhelpers\u002F*.ts`.\n\nFor very large snapshots, the helper may still be too eager. Node cannot create a Buffer larger than roughly 2 GiB, so snapshots above that size can fail with `ERR_FS_FILE_TOO_LARGE` even before parsing. In that case, do not try to raise `--max-old-space-size` and retry the same full-file read. Switch to a streaming script.\n\n```typescript\nimport { parseSnapshot, buildGraph } from '..\u002Fhelpers\u002FparseSnapshot.ts';\n\nconst data = parseSnapshot('\u002Fpath\u002Fto\u002Fsnapshot.heapsnapshot');\nconst graph = buildGraph(data);\n```\n\n#### Snapshots Larger Than 2 GiB\n\nWhen a snapshot is too large to load into a single Buffer, write scratchpad scripts that scan and parse only the sections needed for the question. Use [streamSnapshot.mjs](.\u002Fhelpers\u002FstreamSnapshot.mjs) for the common streaming primitives instead of copying them between scratch scripts.\n\nUseful tricks:\n\n- Find top-level section offsets first. Scan the file as bytes for markers like `\"nodes\":`, `\"edges\":`, `\"strings\":`, and `\"trace_function_infos\":`. This lets follow-up scripts jump directly to the large arrays instead of searching the whole file repeatedly.\n- Parse `snapshot.meta` separately from the small header at the start of the file. Use `meta.node_fields`, `meta.node_types`, `meta.edge_fields`, and `meta.edge_types` to avoid hard-coding tuple widths.\n- Stream numeric arrays in chunks. For `nodes` and `edges`, keep a small carryover string between chunks, split on commas, and process complete numeric tokens as they arrive.\n- Avoid materializing the full `strings` table unless the investigation truly needs it. If you only need suspicious names, collect string indexes from matching nodes\u002Fedges first, then resolve only those indexes in a second streaming pass.\n- If you do need many strings, store only short previews and category counters. Full source strings, ref-listing strings, and prompt payloads can dominate memory and make the analyzer become the leak.\n- Write intermediate outputs to files in the scratchpad. Large heap analysis is iterative and slow; cached node ids, offsets, and retainer traces save repeated multi-minute passes.\n- Prefer self-size attribution and field-level ownership for huge graphs. Full retained-size walks can wildly overcount shared services, roots, maps, and singleton caches.\n- When quantifying a suspected owner, count obvious owned fields separately: wrapper object, key arrays, array elements, direct strings, and parent strings of sliced\u002Fconcatenated strings. This often gives a better lower-bound than a single direct string bucket.\n- Be explicit about approximation boundaries. A field-level subtotal usually undercounts listeners\u002Fwatchers\u002Fback-references but avoids the much worse problem of attributing the whole runtime to one object.\n\nExample large-snapshot workflow:\n\n```javascript\nimport { findArrayStart, findTokenOffsets, parseMeta, streamNumberTuples } from '..\u002F..\u002Fhelpers\u002FstreamSnapshot.mjs';\n\nconst { size, offsets } = findTokenOffsets(snapshotPath);\nconst meta = parseMeta(snapshotPath);\nconst nodeFieldCount = meta.node_fields.length;\nconst nodesStart = findArrayStart(snapshotPath, offsets.get('\"nodes\"'));\n\nstreamNumberTuples(snapshotPath, nodesStart, offsets.get('\"edges\"'), nodeFieldCount, (node, nodeIndex) => {\n    \u002F\u002F node is reused for speed; copy it before storing.\n});\n```\n\n```bash\ncd .github\u002Fskills\u002Fheap-snapshot-analysis\nnode --max-old-space-size=24576 scratchpad\u002FYYYY-MM-DD-topic\u002FfindOffsets.mjs \u002Fpath\u002Fto\u002FHeap.heapsnapshot\nnode --max-old-space-size=24576 scratchpad\u002FYYYY-MM-DD-topic\u002FstreamAnalyze.mjs \u002Fpath\u002Fto\u002FHeap.heapsnapshot > scratchpad\u002FYYYY-MM-DD-topic\u002FstreamAnalyze.out\nnode --max-old-space-size=24576 scratchpad\u002FYYYY-MM-DD-topic\u002FtraceNodes.mjs \u002Fpath\u002Fto\u002FHeap.heapsnapshot 12345 67890 > scratchpad\u002FYYYY-MM-DD-topic\u002FtraceNodes.out\n```\n\n### 2. Compare Before\u002FAfter\n\nUse [compareSnapshots.ts](.\u002Fhelpers\u002FcompareSnapshots.ts) to diff two snapshots:\n\n```typescript\nimport { compareSnapshots } from '..\u002Fhelpers\u002FcompareSnapshots.ts';\n\nconst result = compareSnapshots('\u002Fpath\u002Fto\u002Fbefore.heapsnapshot', '\u002Fpath\u002Fto\u002Fafter.heapsnapshot');\n\u002F\u002F result.topBySize, result.topByCount, result.newObjectGroups, result.summary\n```\n\n### 3. Find Retainer Paths\n\nUse [findRetainers.ts](.\u002Fhelpers\u002FfindRetainers.ts) to trace why an object is alive:\n\n```typescript\nimport { findRetainerPaths } from '..\u002Fhelpers\u002FfindRetainers.ts';\n\n\u002F\u002F Find what keeps ChatModel instances alive (skipping weak edges)\nfindRetainerPaths(graph, 'ChatModel', { maxPaths: 5, maxDepth: 25, maxAttempts: 200 });\n```\n\n### 4. Write Investigation Scripts\n\nWrite investigation-specific scripts in the [scratchpad](.\u002Fscratchpad\u002F) directory. This folder is gitignored — use it freely for one-off analysis.\n\nOrganize scratchpad work into **dated subfolders** named `YYYY-MM-DD-short-description\u002F` (e.g., `2026-04-09-chat-model-retainers\u002F`). Each subfolder should contain:\n\n- The analysis scripts (`.mjs`, `.mts`, etc.)\n- A **`findings.md`** file documenting the full investigation: all ideas considered, which ones led to changes and which were rejected (and why), before\u002Fafter measurements, and a summary of the outcome. This lets the user review the agent's reasoning, decide which changes to keep, and follow up on deferred ideas.\n\nScripts can import the helpers:\n\n```bash\ncd .github\u002Fskills\u002Fheap-snapshot-analysis\nnode --max-old-space-size=16384 scratchpad\u002F2026-04-09-chat-model-retainers\u002Fanalyze.mjs\n```\n\n## Key Concepts\n\n### V8 Heap Snapshot Format\n\nThe `.heapsnapshot` file is JSON with these key sections:\n- **`snapshot.meta`**: Field definitions for nodes and edges\n- **`nodes`**: Flat array, every N values = one node (N = `meta.node_fields.length`, typically 6: `type, name, id, self_size, edge_count, detachedness`)\n- **`edges`**: Flat array, every M values = one edge (M = `meta.edge_fields.length`, typically 3: `type, name_or_index, to_node`)\n- **`strings`**: String table indexed by `name` fields in nodes\u002Fedges\n\n### Edge Types That Matter\n\n| Type | Meaning | Prevents GC? |\n|------|---------|-------------|\n| `property` | Named JS property | Yes |\n| `element` | Array index | Yes |\n| `context` | Closure variable | Yes |\n| `internal` | V8 internal reference | Yes |\n| `hidden` | V8 hidden reference | Yes |\n| **`weak`** | WeakRef\u002FWeakMap key | **No** |\n| `shortcut` | Convenience link | Depends |\n\n**Always skip `weak` edges when tracing retainer paths.** WeakMap entries show up as edges from key → backing array, but they don't prevent collection — they're red herrings.\n\n### Common VS Code Retention Patterns\n\n1. **RowCache templates**: ListView's `RowCache` stores template rows. Templates have `currentElement` pointing to old viewmodel items. If not cleared on session switch, retains entire model chains.\n\n2. **Resource pools**: `pool.clear()` only disposes idle items. If `_onDidUpdateViewModel.fire()` runs AFTER `pool.clear()`, released items re-enter the empty pool and are never disposed. Fire event first, then clear.\n\n3. **`autorunIterableDelta` lastValues**: The closure captures a `Map` of previous iteration values. Values stay until the autorun re-runs. Async disposal delays keep models in observable stores longer than expected.\n\n4. **`HoverService._delayedHovers`**: Global singleton Map retaining disposed objects via `show` closure → `resolveHoverOptions` closure → `this`. If hover cleanup disposable doesn't fire, the entire object tree is retained.\n\n5. **`ObjectMutationLog._previous`**: The incremental serializer keeps a full snapshot of the last-serialized state. Every loaded ChatModel holds 2x its data: live + `_previous`.\n\n6. **`_previousModelRef` pattern**: `MutableDisposable` setter disposes the old value. Reading `.value` and storing it elsewhere, then setting `.value = undefined`, disposes the stored reference. Use `clearAndLeak()` to extract without disposing.\n\n### Defensive Nulling\n\nNull heavy fields in `dispose()` to break retention chains even when something retains the disposed object:\n\n```typescript\noverride dispose() {\n    super.dispose();\n    this._requests.length = 0;      \u002F\u002F conversation data\n    this.dataSerializer = undefined;  \u002F\u002F serialization snapshot\n    this._editingSession = undefined; \u002F\u002F editing session + TextModels\n    this._session = undefined!;       \u002F\u002F back-reference cycles\n}\n```\n\n**Caveat**: Don't null fields on viewmodel items (`ChatResponseViewModel._model`). The tree's `diffIdentityProvider` accesses them after the parent viewmodel is disposed but before `setChildren` replaces them.\n\n### False Retainers to Watch For\n\n- **DevTools debugger global handles**: If the snapshot was captured after opening DevTools, large source strings, compiled scripts, preview data, inspected objects, or debugger bookkeeping can be retained by paths like `DevTools debugger(internal)` → `synthetic::(Global handles)` → GC roots. Treat these as debugger-induced until proven otherwise. They may not exist in the app before DevTools opens, and they should not be confused with application-owned leaks.\n- **`DevToolsLogger._aliveInstances`** (Map): Enabled by `VSCODE_DEV_DEBUG_OBSERVABLES` env var. Retains ALL observed observables. Check if this is active before investigating observable-rooted paths.\n- **`GCBasedDisposableTracker` (FinalizationRegistry)**: If `register(target, held, target)` is used (target === unregister token), creates a strong self-reference preventing GC. Currently commented out in production.\n- **WeakMap backing arrays**: Show up in retainer paths but don't prevent collection.\n\n## Running Analysis\n\nAll helper scripts use ESM and need Node with extra memory:\n\n```bash\nnode --max-old-space-size=16384 scratchpad\u002Fanalyze.mjs\n```\n\nTypical analysis takes 30-120 seconds per snapshot depending on size.\n","# Heap Snapshot Analysis\n\nInvestigate memory leaks from V8 heap snapshots (`.heapsnapshot` files). This skill starts when snapshots already exist: either the user provided them, DevTools exported them, or another workflow produced them. Use the helpers here to compare snapshots, group object deltas, and trace retainer paths.\n\n## IGNORE Prior Investigations\n\n**Start every investigation fresh.** Do NOT read, consult, or be influenced by prior investigations found in:\n\n- `\u002Fmemories\u002F` (user, session, or repo memory)\n- `.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fscratchpad\u002F` (previous dated subfolders and their `findings.md` files)\n- Any other notes from earlier sessions\n\nPrevious findings can bias the analysis toward suspects that are no longer relevant, or cause the agent to skip steps and jump to conclusions. Let the current snapshots speak for themselves. Only reference prior work if the user explicitly asks you to.\n\n## When to Use\n\n- User provides `.heapsnapshot` files (before\u002Fafter a workflow)\n- User has heap snapshots captured by another skill or script\n- Need to find what retains disposed objects (retainer path analysis)\n- Comparing object counts\u002Fsizes between two snapshots\n- Investigating why particular objects survive GC\n\n## Workflow\n\nIf the user needs the agent to launch VS Code, drive a scenario, and capture snapshots first, use the VS Code performance workflow skill before returning here for low-level snapshot analysis.\n\n### 1. Parse Snapshots\n\nUse the helpers in [parseSnapshot.ts](.\u002Fhelpers\u002FparseSnapshot.ts) to load snapshots. The files are often >500MB and too large for `JSON.parse` as a string — the helpers use Buffer-based extraction. In scratchpad scripts, import helpers from `..\u002Fhelpers\u002F*.ts`.\n\nFor very large snapshots, the helper may still be too eager. Node cannot create a Buffer larger than roughly 2 GiB, so snapshots above that size can fail with `ERR_FS_FILE_TOO_LARGE` even before parsing. In that case, do not try to raise `--max-old-space-size` and retry the same full-file read. Switch to a streaming script.\n\n```typescript\nimport { parseSnapshot, buildGraph } from '..\u002Fhelpers\u002FparseSnapshot.ts';\n\nconst data = parseSnapshot('\u002Fpath\u002Fto\u002Fsnapshot.heapsnapshot');\nconst graph = buildGraph(data);\n```\n\n#### Snapshots Larger Than 2 GiB\n\nWhen a snapshot is too large to load into a single Buffer, write scratchpad scripts that scan and parse only the sections needed for the question. Use [streamSnapshot.mjs](.\u002Fhelpers\u002FstreamSnapshot.mjs) for the common streaming primitives instead of copying them between scratch scripts.\n\nUseful tricks:\n\n- Find top-level section offsets first. Scan the file as bytes for markers like `\"nodes\":`, `\"edges\":`, `\"strings\":`, and `\"trace_function_infos\":`. This lets follow-up scripts jump directly to the large arrays instead of searching the whole file repeatedly.\n- Parse `snapshot.meta` separately from the small header at the start of the file. Use `meta.node_fields`, `meta.node_types`, `meta.edge_fields`, and `meta.edge_types` to avoid hard-coding tuple widths.\n- Stream numeric arrays in chunks. For `nodes` and `edges`, keep a small carryover string between chunks, split on commas, and process complete numeric tokens as they arrive.\n- Avoid materializing the full `strings` table unless the investigation truly needs it. If you only need suspicious names, collect string indexes from matching nodes\u002Fedges first, then resolve only those indexes in a second streaming pass.\n- If you do need many strings, store only short previews and category counters. Full source strings, ref-listing strings, and prompt payloads can dominate memory and make the analyzer become the leak.\n- Write intermediate outputs to files in the scratchpad. Large heap analysis is iterative and slow; cached node ids, offsets, and retainer traces save repeated multi-minute passes.\n- Prefer self-size attribution and field-level ownership for huge graphs. Full retained-size walks can wildly overcount shared services, roots, maps, and singleton caches.\n- When quantifying a suspected owner, count obvious owned fields separately: wrapper object, key arrays, array elements, direct strings, and parent strings of sliced\u002Fconcatenated strings. This often gives a better lower-bound than a single direct string bucket.\n- Be explicit about approximation boundaries. A field-level subtotal usually undercounts listeners\u002Fwatchers\u002Fback-references but avoids the much worse problem of attributing the whole runtime to one object.\n\nExample large-snapshot workflow:\n\n```javascript\nimport { findArrayStart, findTokenOffsets, parseMeta, streamNumberTuples } from '..\u002F..\u002Fhelpers\u002FstreamSnapshot.mjs';\n\nconst { size, offsets } = findTokenOffsets(snapshotPath);\nconst meta = parseMeta(snapshotPath);\nconst nodeFieldCount = meta.node_fields.length;\nconst nodesStart = findArrayStart(snapshotPath, offsets.get('\"nodes\"'));\n\nstreamNumberTuples(snapshotPath, nodesStart, offsets.get('\"edges\"'), nodeFieldCount, (node, nodeIndex) => {\n    \u002F\u002F node is reused for speed; copy it before storing.\n});\n```\n\n```bash\ncd .github\u002Fskills\u002Fheap-snapshot-analysis\nnode --max-old-space-size=24576 scratchpad\u002FYYYY-MM-DD-topic\u002FfindOffsets.mjs \u002Fpath\u002Fto\u002FHeap.heapsnapshot\nnode --max-old-space-size=24576 scratchpad\u002FYYYY-MM-DD-topic\u002FstreamAnalyze.mjs \u002Fpath\u002Fto\u002FHeap.heapsnapshot > scratchpad\u002FYYYY-MM-DD-topic\u002FstreamAnalyze.out\nnode --max-old-space-size=24576 scratchpad\u002FYYYY-MM-DD-topic\u002FtraceNodes.mjs \u002Fpath\u002Fto\u002FHeap.heapsnapshot 12345 67890 > scratchpad\u002FYYYY-MM-DD-topic\u002FtraceNodes.out\n```\n\n### 2. Compare Before\u002FAfter\n\nUse [compareSnapshots.ts](.\u002Fhelpers\u002FcompareSnapshots.ts) to diff two snapshots:\n\n```typescript\nimport { compareSnapshots } from '..\u002Fhelpers\u002FcompareSnapshots.ts';\n\nconst result = compareSnapshots('\u002Fpath\u002Fto\u002Fbefore.heapsnapshot', '\u002Fpath\u002Fto\u002Fafter.heapsnapshot');\n\u002F\u002F result.topBySize, result.topByCount, result.newObjectGroups, result.summary\n```\n\n### 3. Find Retainer Paths\n\nUse [findRetainers.ts](.\u002Fhelpers\u002FfindRetainers.ts) to trace why an object is alive:\n\n```typescript\nimport { findRetainerPaths } from '..\u002Fhelpers\u002FfindRetainers.ts';\n\n\u002F\u002F Find what keeps ChatModel instances alive (skipping weak edges)\nfindRetainerPaths(graph, 'ChatModel', { maxPaths: 5, maxDepth: 25, maxAttempts: 200 });\n```\n\n### 4. Write Investigation Scripts\n\nWrite investigation-specific scripts in the [scratchpad](.\u002Fscratchpad\u002F) directory. This folder is gitignored — use it freely for one-off analysis.\n\nOrganize scratchpad work into **dated subfolders** named `YYYY-MM-DD-short-description\u002F` (e.g., `2026-04-09-chat-model-retainers\u002F`). Each subfolder should contain:\n\n- The analysis scripts (`.mjs`, `.mts`, etc.)\n- A **`findings.md`** file documenting the full investigation: all ideas considered, which ones led to changes and which were rejected (and why), before\u002Fafter measurements, and a summary of the outcome. This lets the user review the agent's reasoning, decide which changes to keep, and follow up on deferred ideas.\n\nScripts can import the helpers:\n\n```bash\ncd .github\u002Fskills\u002Fheap-snapshot-analysis\nnode --max-old-space-size=16384 scratchpad\u002F2026-04-09-chat-model-retainers\u002Fanalyze.mjs\n```\n\n## Key Concepts\n\n### V8 Heap Snapshot Format\n\nThe `.heapsnapshot` file is JSON with these key sections:\n- **`snapshot.meta`**: Field definitions for nodes and edges\n- **`nodes`**: Flat array, every N values = one node (N = `meta.node_fields.length`, typically 6: `type, name, id, self_size, edge_count, detachedness`)\n- **`edges`**: Flat array, every M values = one edge (M = `meta.edge_fields.length`, typically 3: `type, name_or_index, to_node`)\n- **`strings`**: String table indexed by `name` fields in nodes\u002Fedges\n\n### Edge Types That Matter\n\n| Type | Meaning | Prevents GC? |\n|------|---------|-------------|\n| `property` | Named JS property | Yes |\n| `element` | Array index | Yes |\n| `context` | Closure variable | Yes |\n| `internal` | V8 internal reference | Yes |\n| `hidden` | V8 hidden reference | Yes |\n| **`weak`** | WeakRef\u002FWeakMap key | **No** |\n| `shortcut` | Convenience link | Depends |\n\n**Always skip `weak` edges when tracing retainer paths.** WeakMap entries show up as edges from key → backing array, but they don't prevent collection — they're red herrings.\n\n### Common VS Code Retention Patterns\n\n1. **RowCache templates**: ListView's `RowCache` stores template rows. Templates have `currentElement` pointing to old viewmodel items. If not cleared on session switch, retains entire model chains.\n\n2. **Resource pools**: `pool.clear()` only disposes idle items. If `_onDidUpdateViewModel.fire()` runs AFTER `pool.clear()`, released items re-enter the empty pool and are never disposed. Fire event first, then clear.\n\n3. **`autorunIterableDelta` lastValues**: The closure captures a `Map` of previous iteration values. Values stay until the autorun re-runs. Async disposal delays keep models in observable stores longer than expected.\n\n4. **`HoverService._delayedHovers`**: Global singleton Map retaining disposed objects via `show` closure → `resolveHoverOptions` closure → `this`. If hover cleanup disposable doesn't fire, the entire object tree is retained.\n\n5. **`ObjectMutationLog._previous`**: The incremental serializer keeps a full snapshot of the last-serialized state. Every loaded ChatModel holds 2x its data: live + `_previous`.\n\n6. **`_previousModelRef` pattern**: `MutableDisposable` setter disposes the old value. Reading `.value` and storing it elsewhere, then setting `.value = undefined`, disposes the stored reference. Use `clearAndLeak()` to extract without disposing.\n\n### Defensive Nulling\n\nNull heavy fields in `dispose()` to break retention chains even when something retains the disposed object:\n\n```typescript\noverride dispose() {\n    super.dispose();\n    this._requests.length = 0;      \u002F\u002F conversation data\n    this.dataSerializer = undefined;  \u002F\u002F serialization snapshot\n    this._editingSession = undefined; \u002F\u002F editing session + TextModels\n    this._session = undefined!;       \u002F\u002F back-reference cycles\n}\n```\n\n**Caveat**: Don't null fields on viewmodel items (`ChatResponseViewModel._model`). The tree's `diffIdentityProvider` accesses them after the parent viewmodel is disposed but before `setChildren` replaces them.\n\n### False Retainers to Watch For\n\n- **DevTools debugger global handles**: If the snapshot was captured after opening DevTools, large source strings, compiled scripts, preview data, inspected objects, or debugger bookkeeping can be retained by paths like `DevTools debugger(internal)` → `synthetic::(Global handles)` → GC roots. Treat these as debugger-induced until proven otherwise. They may not exist in the app before DevTools opens, and they should not be confused with application-owned leaks.\n- **`DevToolsLogger._aliveInstances`** (Map): Enabled by `VSCODE_DEV_DEBUG_OBSERVABLES` env var. Retains ALL observed observables. Check if this is active before investigating observable-rooted paths.\n- **`GCBasedDisposableTracker` (FinalizationRegistry)**: If `register(target, held, target)` is used (target === unregister token), creates a strong self-reference preventing GC. Currently commented out in production.\n- **WeakMap backing arrays**: Show up in retainer paths but don't prevent collection.\n\n## Running Analysis\n\nAll helper scripts use ESM and need Node with extra memory:\n\n```bash\nnode --max-old-space-size=16384 scratchpad\u002Fanalyze.mjs\n```\n\nTypical analysis takes 30-120 seconds per snapshot depending on size.\n",{"name":5,"description":7},"main","11ba54a90a0ff8720cf47c086f5a9948eef9cf2c",".github\u002Fskills\u002Fheap-snapshot-analysis",43791,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode","https:\u002F\u002Fcode.visualstudio.com","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-12 03:13:10",{"source":8,"skillName":5,"commands":55,"prompt":65,"zipAvailable":66},[56,59,62],{"tool":57,"command":58},"npx","npx skills add microsoft\u002Fvscode --skill heap-snapshot-analysis",{"tool":60,"command":61},"bunx","bunx skills add microsoft\u002Fvscode --skill heap-snapshot-analysis",{"tool":63,"command":64},"pnpm","pnpm dlx skills add microsoft\u002Fvscode --skill heap-snapshot-analysis","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002FSKILL.md",true,[68,74,78,82,86,92],{"path":69,"resourceType":70,"resourceTypeDesc":71,"sizeBytes":72,"sourceHtmlUrl":73},"helpers\u002FcompareSnapshots.ts","SCRIPT","脚本",6109,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fhelpers\u002FcompareSnapshots.ts",{"path":75,"resourceType":70,"resourceTypeDesc":71,"sizeBytes":76,"sourceHtmlUrl":77},"helpers\u002FfindRetainers.ts",4942,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fhelpers\u002FfindRetainers.ts",{"path":79,"resourceType":70,"resourceTypeDesc":71,"sizeBytes":80,"sourceHtmlUrl":81},"helpers\u002FparseSnapshot.ts",7536,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fhelpers\u002FparseSnapshot.ts",{"path":83,"resourceType":70,"resourceTypeDesc":71,"sizeBytes":84,"sourceHtmlUrl":85},"helpers\u002FstreamSnapshot.mjs",6926,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fhelpers\u002FstreamSnapshot.mjs",{"path":87,"resourceType":88,"resourceTypeDesc":89,"sizeBytes":90,"sourceHtmlUrl":91},"scratchpad\u002F.gitignore","OTHER","其他",25,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fscratchpad\u002F.gitignore",{"path":93,"resourceType":94,"resourceTypeDesc":95,"sizeBytes":96,"sourceHtmlUrl":97},"scratchpad\u002FREADME.md","REFERENCE","参考文档",810,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fheap-snapshot-analysis\u002Fscratchpad\u002FREADME.md",[99,124,146,160,175,191],{"skillNo":100,"name":101,"nameZh":6,"description":102,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":103,"categories":104,"keywords":105,"publishedAt":39},"01M70H0ANBKZAVEP1NVV4K28M3","sweeper-query","Answer natural-language questions about the VS Code Sweeper's published issue index (index-\u003Cname>.json for microsoft\u002Fvscode and the enrolled family repos) — which issues are agent-ready to implement or plan, what the sweeper proposed for an assignee or area, whether the sweeper knows a report as a duplicate, the highest-value open bugs in a lane — and, for one issue, the review's full record (evidence with file citations, fix brief, proposed comment). Use ONLY when the request explicitly names the sweeper — \"sweeper\", \"vscodesweeper\", \"sweeper-query\", a sweeper verdict, record, brief or index, an agent-ready issue — or asks what \"the sweeper\" found, proposed or knows. Do NOT use for general questions about issues, duplicates, priorities or ranking that do not mention the sweeper. Read-only; discovery only.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002F.github\u002Fskills\u002Fsweeper-query\u002FSKILL.md",[],[106,107,108,109,110,25,111,112,113,114,115,116,117,118,119,120,121,122,38,123],"sweeper query","query","sweeper","review","fix","request","which","natural-language","ranking","duplicates","ready","only","read","plan","open","Citations","brief","code",{"skillNo":125,"name":126,"nameZh":6,"description":127,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":128,"categories":129,"keywords":130,"publishedAt":39},"01M699DRJS4YJCV600Q72SVVWE","update-skills","Create or update repository skills and instructions when major learnings are discovered during a session. Use when the user says \"learn!\", when a significant pattern or pitfall is identified, or when reusable domain knowledge should be captured for future sessions.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002Fsrc\u002Fvs\u002Fsessions\u002Fskills\u002Fupdate-skills\u002FSKILL.md",[],[131,132,133,134,135,136,137,34,138,139,140,141,142,143,144,145,25],"update","update skills","for","pattern","domain","learnings","reusable","session","user","instructions","knowledge","repository","learn","create","sessions",{"skillNo":147,"name":148,"nameZh":6,"description":149,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":39},"01M699DPWHZ72B66P82EW31RNE","update-pr","Update the pull request for the current session. Use when the user wants to push new changes to an existing PR.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002Fsrc\u002Fvs\u002Fsessions\u002Fskills\u002Fupdate-pr\u002FSKILL.md",[],[131,153,154,25,111,155,133,34,138,139,156,157,158,159],"pr","update pr","Existing","push","pull","pull request","changes",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":166,"publishedAt":39},"01M699DJMR6CE1MH5XCJZ0GFQ0","sync","Sync the current session branch with its upstream branch, or publish the current session branch to a remote. Use when the user asks to sync a branch, pull latest changes, rebase onto upstream, push current branch, publish branch, or set upstream.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002Fsrc\u002Fvs\u002Fsessions\u002Fskills\u002Fsync\u002FSKILL.md",[],[162,167,168,156,157,169,25,159,170,171,172,173,34,138,139,174],"SET","publish","rebase","latest","with","remote","upstream","branch",{"skillNo":176,"name":177,"nameZh":6,"description":178,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":179,"categories":180,"keywords":181,"publishedAt":39},"01M699DGXHX6A2A72X57R6GGHJ","sync-upstream","Update a stale session branch by rebasing onto the latest origin. Use when the upstream has moved significantly and the session needs to catch up, resolving conflicts by preserving upstream changes and adapting session work to fit.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002Fsrc\u002Fvs\u002Fsessions\u002Fskills\u002Fsync-upstream\u002FSKILL.md",[],[173,162,182,183,25,184,185,159,170,186,187,188,189,34,138,174,190,131],"sync upstream","fit","catch","catch up","origin","conflicts","stale","up","work",{"skillNo":192,"name":193,"nameZh":6,"description":194,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":195,"categories":196,"keywords":197,"publishedAt":39},"01M699DEXYFWDGW0BAXJ5QQ377","merge","Merge changes from the topic branch to the merge base branch. Use when the user wants to merge their session's work back to the base branch.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode\u002Fblob\u002Fmain\u002Fsrc\u002Fvs\u002Fsessions\u002Fskills\u002Fmerge\u002FSKILL.md",[],[193,138,34,190,174,139,25,198,199,159],"back","base"]