mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
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:
@@ -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
@@ -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)
|
||||
|
||||
Generated
+4
-4
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -797,5 +797,18 @@
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get_heapsnapshot_dominators",
|
||||
"args": [
|
||||
{
|
||||
"name": "file_path_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "node_id",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user