Skip to content

Commit 08c234e

Browse files
dinfuehrDominik Inführnroscino
authored
feat: Add get_heapsnapshot_dominators MCP tool (#2215)
Adding the get_heapsnapshot_dominators MCP tool to show the dominators for a given node. In combination with get_heapsnapshot_retaining_paths this should help understand what keeps an object reachable and thus alive. Co-authored-by: Dominik Inführ <dinfuehr@chromium.org> Co-authored-by: Nicholas Roscino <nroscino@google.com>
1 parent 9e32002 commit 08c234e

15 files changed

Lines changed: 216 additions & 7 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,11 +514,12 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
514514
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
515515
- [`screencast_start`](docs/tool-reference.md#screencast_start)
516516
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
517-
- **Memory** (8 tools)
517+
- **Memory** (9 tools)
518518
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
519519
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
520520
- [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
521521
- [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
522+
- [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
522523
- [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
523524
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
524525
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)

docs/tool-reference.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@
3939
- [`take_snapshot`](#take_snapshot)
4040
- [`screencast_start`](#screencast_start)
4141
- [`screencast_stop`](#screencast_stop)
42-
- **[Memory](#memory)** (8 tools)
42+
- **[Memory](#memory)** (9 tools)
4343
- [`take_heapsnapshot`](#take_heapsnapshot)
4444
- [`close_heapsnapshot`](#close_heapsnapshot)
4545
- [`get_heapsnapshot_class_nodes`](#get_heapsnapshot_class_nodes)
4646
- [`get_heapsnapshot_details`](#get_heapsnapshot_details)
47+
- [`get_heapsnapshot_dominators`](#get_heapsnapshot_dominators)
4748
- [`get_heapsnapshot_edges`](#get_heapsnapshot_edges)
4849
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
4950
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
@@ -493,6 +494,17 @@ in the DevTools Elements panel (if any).
493494

494495
---
495496

497+
### `get_heapsnapshot_dominators`
498+
499+
**Description:** Loads a memory heapsnapshot and returns the dominator chain for a specific node ID. This helps to identify which objects are keeping the target node alive. (requires flag: --memoryDebugging=true)
500+
501+
**Parameters:**
502+
503+
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
504+
- **nodeId** (number) **(required)**: The node ID to get the dominator chain for.
505+
506+
---
507+
496508
### `get_heapsnapshot_edges`
497509

498510
**Description:** Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (requires flag: --memoryDebugging=true)

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@types/yargs": "^17.0.33",
6464
"@typescript-eslint/eslint-plugin": "^8.43.0",
6565
"@typescript-eslint/parser": "^8.43.0",
66-
"chrome-devtools-frontend": "1.0.1642845",
66+
"chrome-devtools-frontend": "1.0.1645245",
6767
"core-js": "3.49.0",
6868
"debug": "4.4.3",
6969
"eslint": "^9.35.0",

src/HeapSnapshotManager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ export class HeapSnapshotManager {
148148
);
149149
}
150150

151+
async getDominatorsOf(
152+
filePath: string,
153+
nodeId: number,
154+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain> {
155+
const snapshot = await this.getSnapshot(filePath);
156+
const nodeIndex = await snapshot.nodeIndexForId(nodeId);
157+
if (nodeIndex === undefined) {
158+
throw new Error(`Node with ID ${nodeId} not found`);
159+
}
160+
return await snapshot.getDominatorsOf(nodeIndex);
161+
}
162+
151163
async getEdges(
152164
filePath: string,
153165
nodeId: number,

src/McpContext.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,13 @@ export class McpContext implements Context {
945945
);
946946
}
947947

948+
async getHeapSnapshotDominators(
949+
filePath: string,
950+
nodeId: number,
951+
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain> {
952+
return await this.#heapSnapshotManager.getDominatorsOf(filePath, nodeId);
953+
}
954+
948955
async loadResource(path: string): Promise<string> {
949956
const url = new URL(path);
950957

src/McpResponse.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export class McpResponse implements Response {
223223
staticData?: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null;
224224
nodes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange;
225225
retainingPaths?: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths;
226+
dominators?: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain;
226227
};
227228
#networkRequestsOptions?: {
228229
include: boolean;
@@ -489,6 +490,16 @@ export class McpResponse implements Response {
489490
};
490491
}
491492

493+
setHeapSnapshotDominators(
494+
dominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain,
495+
) {
496+
this.#heapSnapshotOptions = {
497+
...this.#heapSnapshotOptions,
498+
include: true,
499+
dominators,
500+
};
501+
}
502+
492503
attachImage(value: ImageContentData): void {
493504
this.#images.push(value);
494505
}
@@ -819,6 +830,7 @@ export class McpResponse implements Response {
819830
heapSnapshotData?: object[];
820831
heapSnapshotNodes?: readonly object[];
821832
heapSnapshotRetainingPaths?: object;
833+
heapSnapshotDominators?: readonly object[];
822834
extensionServiceWorkers?: object[];
823835
extensionPages?: object[];
824836
errorMessage?: string;
@@ -1133,6 +1145,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
11331145
structuredContent.heapSnapshotRetainingPaths =
11341146
retainingPaths as unknown as object;
11351147
}
1148+
const dominators = this.#heapSnapshotOptions.dominators;
1149+
if (dominators) {
1150+
response.push('### Dominator Chain');
1151+
if (dominators.length === 0) {
1152+
response.push('No dominators found.');
1153+
} else {
1154+
response.push(HeapSnapshotFormatter.formatDominators(dominators));
1155+
}
1156+
structuredContent.heapSnapshotDominators = dominators;
1157+
}
11361158
}
11371159

11381160
if (data.detailedNetworkRequest) {

src/bin/chrome-devtools-cli-options.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,25 @@ export const commands: Commands = {
360360
},
361361
},
362362
},
363+
get_heapsnapshot_dominators: {
364+
description:
365+
'Loads a memory heapsnapshot and returns the dominator chain for a specific node ID. This helps to identify which objects are keeping the target node alive. (requires flag: --memoryDebugging=true)',
366+
category: 'Memory',
367+
args: {
368+
filePath: {
369+
name: 'filePath',
370+
type: 'string',
371+
description: 'A path to a .heapsnapshot file to read.',
372+
required: true,
373+
},
374+
nodeId: {
375+
name: 'nodeId',
376+
type: 'number',
377+
description: 'The node ID to get the dominator chain for.',
378+
required: true,
379+
},
380+
},
381+
},
363382
get_heapsnapshot_edges: {
364383
description:
365384
'Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (requires flag: --memoryDebugging=true)',

src/formatters/HeapSnapshotFormatter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ export class HeapSnapshotFormatter {
104104
return lines.join('\n');
105105
}
106106

107+
static formatDominators(
108+
dominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain,
109+
): string {
110+
const lines: string[] = [];
111+
lines.push('nodeId,nodeName,selfSize,retainedSize');
112+
for (const node of dominators) {
113+
lines.push(
114+
`${node.nodeId},${node.nodeName},${DevTools.I18n.ByteUtilities.formatBytesToKb(node.selfSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(node.retainedSize)}`,
115+
);
116+
}
117+
return lines.join('\n');
118+
}
119+
107120
#getSortedAggregates(): AggregatedInfoWithId[] {
108121
return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
109122
}

src/telemetry/tool_call_metrics.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,5 +797,18 @@
797797
"argType": "number"
798798
}
799799
]
800+
},
801+
{
802+
"name": "get_heapsnapshot_dominators",
803+
"args": [
804+
{
805+
"name": "file_path_length",
806+
"argType": "number"
807+
},
808+
{
809+
"name": "node_id",
810+
"argType": "number"
811+
}
812+
]
800813
}
801814
]

0 commit comments

Comments
 (0)