feat: Add get_heapsnapshot_object_details MCP tool (#2374)
Add a get_heapsnapshot_object_details MCP tool which lets the agent query all known information about a node in the heap snapshot. Co-authored-by: Dominik Inführ <dinfuehr@chromium.org>
This commit is contained in:
@@ -524,7 +524,7 @@ 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** (11 tools)
|
||||
- **Memory** (12 tools)
|
||||
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
|
||||
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
|
||||
- [`compare_heapsnapshots`](docs/tool-reference.md#compare_heapsnapshots)
|
||||
@@ -533,6 +533,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
||||
- [`get_heapsnapshot_dominators`](docs/tool-reference.md#get_heapsnapshot_dominators)
|
||||
- [`get_heapsnapshot_duplicate_strings`](docs/tool-reference.md#get_heapsnapshot_duplicate_strings)
|
||||
- [`get_heapsnapshot_edges`](docs/tool-reference.md#get_heapsnapshot_edges)
|
||||
- [`get_heapsnapshot_object_details`](docs/tool-reference.md#get_heapsnapshot_object_details)
|
||||
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
|
||||
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
|
||||
- [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
|
||||
|
||||
+13
-1
@@ -39,7 +39,7 @@
|
||||
- [`take_snapshot`](#take_snapshot)
|
||||
- [`screencast_start`](#screencast_start)
|
||||
- [`screencast_stop`](#screencast_stop)
|
||||
- **[Memory](#memory)** (11 tools)
|
||||
- **[Memory](#memory)** (12 tools)
|
||||
- [`take_heapsnapshot`](#take_heapsnapshot)
|
||||
- [`close_heapsnapshot`](#close_heapsnapshot)
|
||||
- [`compare_heapsnapshots`](#compare_heapsnapshots)
|
||||
@@ -48,6 +48,7 @@
|
||||
- [`get_heapsnapshot_dominators`](#get_heapsnapshot_dominators)
|
||||
- [`get_heapsnapshot_duplicate_strings`](#get_heapsnapshot_duplicate_strings)
|
||||
- [`get_heapsnapshot_edges`](#get_heapsnapshot_edges)
|
||||
- [`get_heapsnapshot_object_details`](#get_heapsnapshot_object_details)
|
||||
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
|
||||
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
|
||||
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
|
||||
@@ -539,6 +540,17 @@ in the DevTools Elements panel (if any).
|
||||
|
||||
---
|
||||
|
||||
### `get_heapsnapshot_object_details`
|
||||
|
||||
**Description:** Loads a memory heapsnapshot and returns detailed information about a specific object by its node ID, including size, type, distance, and DOM detachedness. (requires flag: --memoryDebugging=true)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
|
||||
- **nodeId** (number) **(required)**: The node ID to get object details for.
|
||||
|
||||
---
|
||||
|
||||
### `get_heapsnapshot_retainers`
|
||||
|
||||
**Description:** Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)
|
||||
|
||||
@@ -174,6 +174,18 @@ export class HeapSnapshotManager {
|
||||
return await provider.serializeItemsRange(0, Infinity);
|
||||
}
|
||||
|
||||
async getObjectInfo(
|
||||
filePath: string,
|
||||
nodeId: number,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo> {
|
||||
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.getObjectInfo(nodeIndex);
|
||||
}
|
||||
|
||||
async getRetainingPaths(
|
||||
filePath: string,
|
||||
nodeId: number,
|
||||
|
||||
@@ -693,6 +693,13 @@ export class McpContext implements Context {
|
||||
return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
|
||||
}
|
||||
|
||||
async getHeapSnapshotObjectDetails(
|
||||
filePath: string,
|
||||
nodeId: number,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo> {
|
||||
return await this.#heapSnapshotManager.getObjectInfo(filePath, nodeId);
|
||||
}
|
||||
|
||||
async closeHeapSnapshot(filePath: string): Promise<boolean> {
|
||||
return this.#heapSnapshotManager.dispose(filePath);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export class McpResponse implements Response {
|
||||
classDiffs?: HeapSnapshotClassDiff[];
|
||||
detailedClassDiff?: HeapSnapshotDetailedClassDiff;
|
||||
duplicateStrings?: DuplicateStringGroup[];
|
||||
objectInfo?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo;
|
||||
};
|
||||
#networkRequestsOptions?: {
|
||||
include: boolean;
|
||||
@@ -404,6 +405,16 @@ export class McpResponse implements Response {
|
||||
};
|
||||
}
|
||||
|
||||
setHeapSnapshotObjectDetails(
|
||||
objectInfo: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
|
||||
) {
|
||||
this.#heapSnapshotOptions = {
|
||||
...this.#heapSnapshotOptions,
|
||||
include: true,
|
||||
objectInfo,
|
||||
};
|
||||
}
|
||||
|
||||
attachImage(value: ImageContentData): void {
|
||||
this.#images.push(value);
|
||||
}
|
||||
@@ -746,6 +757,7 @@ export class McpResponse implements Response {
|
||||
heapSnapshotClassDiffs?: HeapSnapshotClassDiff[];
|
||||
heapSnapshotDetailedClassDiff?: HeapSnapshotDetailedClassDiff;
|
||||
heapSnapshotDuplicateStrings?: readonly DuplicateStringGroup[];
|
||||
heapSnapshotObjectDetails?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo;
|
||||
extensionServiceWorkers?: object[];
|
||||
extensionPages?: object[];
|
||||
errorMessage?: string;
|
||||
@@ -1177,6 +1189,16 @@ Call ${handleDialog.name} to handle it before continuing.`);
|
||||
|
||||
structuredContent.heapSnapshotDuplicateStrings = paginationData.items;
|
||||
}
|
||||
const objectInfo = this.#heapSnapshotOptions.objectInfo;
|
||||
if (objectInfo) {
|
||||
response.push('### Object Details');
|
||||
response.push(
|
||||
compactEncode
|
||||
? compactEncode(objectInfo)
|
||||
: HeapSnapshotFormatter.formatObjectInfo(objectInfo),
|
||||
);
|
||||
structuredContent.heapSnapshotObjectDetails = objectInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.detailedNetworkRequest) {
|
||||
|
||||
@@ -487,6 +487,25 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
get_heapsnapshot_object_details: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and returns detailed information about a specific object by its node ID, including size, type, distance, and DOM detachedness. (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 object details for.',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
get_heapsnapshot_retainers: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and returns retainers for a specific node ID. (requires flag: --memoryDebugging=true)',
|
||||
|
||||
@@ -243,6 +243,37 @@ export class HeapSnapshotFormatter {
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
static formatObjectInfo(
|
||||
info: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
|
||||
): string {
|
||||
const lines = [
|
||||
`id: @${info.id}`,
|
||||
`name: ${info.name}`,
|
||||
`type: ${info.type}`,
|
||||
`detachedness: ${formatDOMLinkState(info.detachedness)}`,
|
||||
`selfSize: ${DevTools.I18n.ByteUtilities.formatBytesToKb(info.selfSize)}`,
|
||||
`retainedSize: ${DevTools.I18n.ByteUtilities.formatBytesToKb(info.retainedSize)}`,
|
||||
`distance: ${info.distance}`,
|
||||
`edgeCount: ${info.edgeCount}`,
|
||||
`retainerCount: ${info.retainerCount}`,
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function formatDOMLinkState(
|
||||
state: DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState,
|
||||
): string {
|
||||
switch (state) {
|
||||
case DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState.ATTACHED:
|
||||
return 'attached';
|
||||
case DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState.DETACHED:
|
||||
return 'detached';
|
||||
case DevTools.HeapSnapshotModel.HeapSnapshotModel.DOMLinkState.UNKNOWN:
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function formatSignedCount(n: number): string {
|
||||
|
||||
@@ -884,5 +884,18 @@
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get_heapsnapshot_object_details",
|
||||
"args": [
|
||||
{
|
||||
"name": "file_path_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "node_id",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -134,6 +134,9 @@ export interface Response {
|
||||
setHeapSnapshotDetailedClassDiff(
|
||||
detailedClassDiff: HeapSnapshotDetailedClassDiff,
|
||||
): void;
|
||||
setHeapSnapshotObjectDetails(
|
||||
objectInfo: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
|
||||
): void;
|
||||
setIncludePages(value: boolean): void;
|
||||
setIncludeNetworkRequests(
|
||||
value: boolean,
|
||||
@@ -256,6 +259,10 @@ export type Context = Readonly<{
|
||||
filePath: string,
|
||||
nodeId: number,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
|
||||
getHeapSnapshotObjectDetails(
|
||||
filePath: string,
|
||||
nodeId: number,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo>;
|
||||
closeHeapSnapshot(filePath: string): Promise<boolean>;
|
||||
getHeapSnapshotRetainingPaths(
|
||||
filePath: string,
|
||||
|
||||
@@ -372,3 +372,28 @@ export const getHeapSnapshotDuplicateStrings = defineTool({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const getHeapSnapshotObjectDetails = defineTool({
|
||||
name: 'get_heapsnapshot_object_details',
|
||||
description:
|
||||
'Loads a memory heapsnapshot and returns detailed information about a specific object by its node ID, including size, type, distance, and DOM detachedness.',
|
||||
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 object details for.'),
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
const objectInfo = await context.getHeapSnapshotObjectDetails(
|
||||
request.params.filePath,
|
||||
request.params.nodeId,
|
||||
);
|
||||
|
||||
response.setHeapSnapshotObjectDetails(objectInfo);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -332,6 +332,20 @@ map,internal,25577,system / Map
|
||||
Showing 1-56 of 56 (Page 1 of 1).
|
||||
`;
|
||||
|
||||
exports[`memory > get_heapsnapshot_object_details > with valid nodeId 1`] = `
|
||||
## Heap Snapshot Data
|
||||
### Object Details
|
||||
id: @25341
|
||||
name: String
|
||||
type: object
|
||||
detachedness: unknown
|
||||
selfSize: 0.2 kB
|
||||
retainedSize: 1.6 kB
|
||||
distance: 2
|
||||
edgeCount: 56
|
||||
retainerCount: 3
|
||||
`;
|
||||
|
||||
exports[`memory > get_heapsnapshot_retainers > with valid nodeId 1`] = `
|
||||
## Heap Snapshot Data
|
||||
name,type,nodeId,nodeName
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getHeapSnapshotDominators,
|
||||
compareHeapSnapshots,
|
||||
getHeapSnapshotDuplicateStrings,
|
||||
getHeapSnapshotObjectDetails,
|
||||
} from '../../src/tools/memory.js';
|
||||
import {stableIdSymbol} from '../../src/utils/id.js';
|
||||
import {withMcpContext} from '../utils.js';
|
||||
@@ -246,6 +247,33 @@ describe('memory', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_heapsnapshot_object_details', () => {
|
||||
it('with valid nodeId', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
'tests/fixtures/example.heapsnapshot',
|
||||
);
|
||||
|
||||
await getHeapSnapshotObjectDetails.handler(
|
||||
{params: {filePath, nodeId: 25341}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const responseData = await response.handle(
|
||||
getHeapSnapshotObjectDetails.name,
|
||||
context,
|
||||
);
|
||||
const output = responseData.content
|
||||
.map(c => (c.type === 'text' ? c.text : ''))
|
||||
.join('\n');
|
||||
|
||||
t.assert.snapshot(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close_heapsnapshot', () => {
|
||||
it('with default options', async () => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
|
||||
Reference in New Issue
Block a user