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>
This commit is contained in:
Dominik Inführ
2026-06-16 14:07:41 +02:00
committed by GitHub
parent 9e32002a69
commit 08c234ea4b
15 changed files with 216 additions and 7 deletions
+2 -1
View File
@@ -514,11 +514,12 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
- [`screencast_start`](docs/tool-reference.md#screencast_start)
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
- **Memory** (8 tools)
- **Memory** (9 tools)
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
- [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
- [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
- [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
- [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
+13 -1
View File
@@ -39,11 +39,12 @@
- [`take_snapshot`](#take_snapshot)
- [`screencast_start`](#screencast_start)
- [`screencast_stop`](#screencast_stop)
- **[Memory](#memory)** (8 tools)
- **[Memory](#memory)** (9 tools)
- [`take_heapsnapshot`](#take_heapsnapshot)
- [`close_heapsnapshot`](#close_heapsnapshot)
- [`get_heapsnapshot_class_nodes`](#get_heapsnapshot_class_nodes)
- [`get_heapsnapshot_details`](#get_heapsnapshot_details)
- [`get_heapsnapshot_dominators`](#get_heapsnapshot_dominators)
- [`get_heapsnapshot_edges`](#get_heapsnapshot_edges)
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
@@ -493,6 +494,17 @@ in the DevTools Elements panel (if any).
---
### `get_heapsnapshot_dominators`
**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)
**Parameters:**
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **nodeId** (number) **(required)**: The node ID to get the dominator chain for.
---
### `get_heapsnapshot_edges`
**Description:** Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (requires flag: --memoryDebugging=true)
+4 -4
View File
@@ -29,7 +29,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
"chrome-devtools-frontend": "1.0.1642845",
"chrome-devtools-frontend": "1.0.1645245",
"core-js": "3.49.0",
"debug": "4.4.3",
"eslint": "^9.35.0",
@@ -3284,9 +3284,9 @@
}
},
"node_modules/chrome-devtools-frontend": {
"version": "1.0.1642845",
"resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1642845.tgz",
"integrity": "sha512-74XZ4L7VFHbwNvOZdLEy1iLBESAf0asei+32VPtLFvdhxjlcrscWyf5tqV3NVWLd7NziJhANwIV8oJSu5IVvAA==",
"version": "1.0.1645245",
"resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1645245.tgz",
"integrity": "sha512-noAeBfaJ2lveTLLyfwWi6Ypc4PxF085PdiCU5WKslMJ3ojDSEQhZsclpJdQ1N4gRdSgsvOjGlBOkyTLt2BpV5Q==",
"dev": true,
"license": "BSD-3-Clause"
},
+1 -1
View File
@@ -63,7 +63,7 @@
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.43.0",
"@typescript-eslint/parser": "^8.43.0",
"chrome-devtools-frontend": "1.0.1642845",
"chrome-devtools-frontend": "1.0.1645245",
"core-js": "3.49.0",
"debug": "4.4.3",
"eslint": "^9.35.0",
+12
View File
@@ -148,6 +148,18 @@ export class HeapSnapshotManager {
);
}
async getDominatorsOf(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain> {
const snapshot = await this.getSnapshot(filePath);
const nodeIndex = await snapshot.nodeIndexForId(nodeId);
if (nodeIndex === undefined) {
throw new Error(`Node with ID ${nodeId} not found`);
}
return await snapshot.getDominatorsOf(nodeIndex);
}
async getEdges(
filePath: string,
nodeId: number,
+7
View File
@@ -945,6 +945,13 @@ export class McpContext implements Context {
);
}
async getHeapSnapshotDominators(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain> {
return await this.#heapSnapshotManager.getDominatorsOf(filePath, nodeId);
}
async loadResource(path: string): Promise<string> {
const url = new URL(path);
+22
View File
@@ -223,6 +223,7 @@ export class McpResponse implements Response {
staticData?: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null;
nodes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange;
retainingPaths?: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths;
dominators?: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain;
};
#networkRequestsOptions?: {
include: boolean;
@@ -489,6 +490,16 @@ export class McpResponse implements Response {
};
}
setHeapSnapshotDominators(
dominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain,
) {
this.#heapSnapshotOptions = {
...this.#heapSnapshotOptions,
include: true,
dominators,
};
}
attachImage(value: ImageContentData): void {
this.#images.push(value);
}
@@ -819,6 +830,7 @@ export class McpResponse implements Response {
heapSnapshotData?: object[];
heapSnapshotNodes?: readonly object[];
heapSnapshotRetainingPaths?: object;
heapSnapshotDominators?: readonly object[];
extensionServiceWorkers?: object[];
extensionPages?: object[];
errorMessage?: string;
@@ -1133,6 +1145,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
structuredContent.heapSnapshotRetainingPaths =
retainingPaths as unknown as object;
}
const dominators = this.#heapSnapshotOptions.dominators;
if (dominators) {
response.push('### Dominator Chain');
if (dominators.length === 0) {
response.push('No dominators found.');
} else {
response.push(HeapSnapshotFormatter.formatDominators(dominators));
}
structuredContent.heapSnapshotDominators = dominators;
}
}
if (data.detailedNetworkRequest) {
+19
View File
@@ -360,6 +360,25 @@ export const commands: Commands = {
},
},
},
get_heapsnapshot_dominators: {
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)',
category: 'Memory',
args: {
filePath: {
name: 'filePath',
type: 'string',
description: 'A path to a .heapsnapshot file to read.',
required: true,
},
nodeId: {
name: 'nodeId',
type: 'number',
description: 'The node ID to get the dominator chain for.',
required: true,
},
},
},
get_heapsnapshot_edges: {
description:
'Loads a memory heapsnapshot and returns outgoing edges (references) for a specific node ID. (requires flag: --memoryDebugging=true)',
+13
View File
@@ -104,6 +104,19 @@ export class HeapSnapshotFormatter {
return lines.join('\n');
}
static formatDominators(
dominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain,
): string {
const lines: string[] = [];
lines.push('nodeId,nodeName,selfSize,retainedSize');
for (const node of dominators) {
lines.push(
`${node.nodeId},${node.nodeName},${DevTools.I18n.ByteUtilities.formatBytesToKb(node.selfSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(node.retainedSize)}`,
);
}
return lines.join('\n');
}
#getSortedAggregates(): AggregatedInfoWithId[] {
return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
}
+13
View File
@@ -797,5 +797,18 @@
"argType": "number"
}
]
},
{
"name": "get_heapsnapshot_dominators",
"args": [
{
"name": "file_path_length",
"argType": "number"
},
{
"name": "node_id",
"argType": "number"
}
]
}
]
+7
View File
@@ -119,6 +119,9 @@ export interface Response {
setHeapSnapshotRetainingPaths(
retainingPaths: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths,
): void;
setHeapSnapshotDominators(
dominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain,
): void;
setIncludePages(value: boolean): void;
setIncludeNetworkRequests(
value: boolean,
@@ -260,6 +263,10 @@ export type Context = Readonly<{
maxNodes?: number,
maxSiblings?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths>;
getHeapSnapshotDominators(
filePath: string,
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain>;
getHeapSnapshotEdges(
filePath: string,
nodeId: number,
+27
View File
@@ -253,3 +253,30 @@ export const getHeapSnapshotEdges = defineTool({
});
},
});
export const getHeapSnapshotDominators = defineTool({
name: 'get_heapsnapshot_dominators',
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.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
conditions: ['memoryDebugging'],
},
blockedByDialog: false,
verifyFilesSchema: ['filePath'],
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
nodeId: zod
.number()
.describe('The node ID to get the dominator chain for.'),
},
handler: async (request, response, context) => {
const dominators = await context.getHeapSnapshotDominators(
request.params.filePath,
request.params.nodeId,
);
response.setHeapSnapshotDominators(dominators);
},
});
@@ -194,4 +194,43 @@ describe('HeapSnapshotFormatter', () => {
assert.strictEqual(result, expected);
});
});
describe('formatDominators', () => {
it('formats dominator chain correctly', () => {
const mockDominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain =
[
{
nodeId: 10,
nodeIndex: 1,
nodeName: 'ClassA',
retainedSize: 1000,
selfSize: 100,
},
{
nodeId: 20,
nodeIndex: 2,
nodeName: 'ClassB',
retainedSize: 500,
selfSize: 50,
},
];
const result = HeapSnapshotFormatter.formatDominators(mockDominators);
const expected = [
'nodeId,nodeName,selfSize,retainedSize',
`10,ClassA,${DevTools.I18n.ByteUtilities.formatBytesToKb(100)},${DevTools.I18n.ByteUtilities.formatBytesToKb(1000)}`,
`20,ClassB,${DevTools.I18n.ByteUtilities.formatBytesToKb(50)},${DevTools.I18n.ByteUtilities.formatBytesToKb(500)}`,
].join('\n');
assert.strictEqual(result, expected);
});
it('formats empty dominator chain correctly', () => {
const mockDominators: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain =
[];
const result = HeapSnapshotFormatter.formatDominators(mockDominators);
const expected = 'nodeId,nodeName,selfSize,retainedSize';
assert.strictEqual(result, expected);
});
});
});
+9
View File
@@ -175,6 +175,15 @@ id,name,count,selfSize,maxRetainedSize
108,HTMLBodyElement (internal cache) / https://example.com,1,0.0 kB,0.0 kB
`;
exports[`memory > get_heapsnapshot_dominators > with valid nodeId 1`] = `
## Heap Snapshot Data
### Dominator Chain
nodeId,nodeName,selfSize,retainedSize
25341,String,0.2 kB,1.6 kB
7199,system / NativeContext,1.2 kB,84.4 kB
1,,0.0 kB,802 kB
`;
exports[`memory > get_heapsnapshot_edges > with pagination 1`] = `
## Heap Snapshot Data
name,type,nodeId,nodeName
+28
View File
@@ -20,6 +20,7 @@ import {
closeHeapSnapshot,
getHeapSnapshotRetainingPaths,
getHeapSnapshotEdges,
getHeapSnapshotDominators,
} from '../../src/tools/memory.js';
import {withMcpContext} from '../utils.js';
@@ -333,4 +334,31 @@ describe('memory', () => {
});
});
});
describe('get_heapsnapshot_dominators', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotDominators.handler(
{params: {filePath, nodeId: 25341}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotDominators.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
});