mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
feat: Add query_heapsnapshot MCP tool (#2553)
`query_heapsnapshot_objects` allows the agent to filter objects by properties like class name, self size, retained size, property name and/or detachedness. The resulting list of objects can be sorted as well.
This commit is contained in:
@@ -557,7 +557,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** (12 tools)
|
||||
- **Memory** (13 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)
|
||||
@@ -570,6 +570,7 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
|
||||
- [`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)
|
||||
- [`query_heapsnapshot_objects`](docs/tool-reference.md#query_heapsnapshot_objects)
|
||||
- **Extensions** (5 tools)
|
||||
- [`install_extension`](docs/tool-reference.md#install_extension)
|
||||
- [`list_extensions`](docs/tool-reference.md#list_extensions)
|
||||
|
||||
+23
-1
@@ -39,7 +39,7 @@
|
||||
- [`take_snapshot`](#take_snapshot)
|
||||
- [`screencast_start`](#screencast_start)
|
||||
- [`screencast_stop`](#screencast_stop)
|
||||
- **[Memory](#memory)** (12 tools)
|
||||
- **[Memory](#memory)** (13 tools)
|
||||
- [`take_heapsnapshot`](#take_heapsnapshot)
|
||||
- [`close_heapsnapshot`](#close_heapsnapshot)
|
||||
- [`compare_heapsnapshots`](#compare_heapsnapshots)
|
||||
@@ -52,6 +52,7 @@
|
||||
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
|
||||
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
|
||||
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
|
||||
- [`query_heapsnapshot_objects`](#query_heapsnapshot_objects)
|
||||
- **[Extensions](#extensions)** (5 tools)
|
||||
- [`install_extension`](#install_extension)
|
||||
- [`list_extensions`](#list_extensions)
|
||||
@@ -600,6 +601,27 @@ in the DevTools Elements panel (if any).
|
||||
|
||||
---
|
||||
|
||||
### `query_heapsnapshot_objects`
|
||||
|
||||
**Description:** Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
|
||||
- **className** (string) _(optional)_: Optional regex or text matching object class name.
|
||||
- **isDetached** (boolean) _(optional)_: Whether to filter for detached DOM nodes.
|
||||
- **maxRetainedSize** (number) _(optional)_: Maximum retained size in bytes.
|
||||
- **maxSelfSize** (number) _(optional)_: Maximum self size in bytes.
|
||||
- **minRetainedSize** (number) _(optional)_: Minimum retained size in bytes.
|
||||
- **minSelfSize** (number) _(optional)_: Minimum self size in bytes.
|
||||
- **nodeType** (string) _(optional)_: Optional V8 node type filter (e.g. object, closure, string, array, code).
|
||||
- **pageIdx** (number) _(optional)_: The page index for pagination.
|
||||
- **pageSize** (number) _(optional)_: The page size for pagination.
|
||||
- **propertyName** (string) _(optional)_: Optional property name filter for outgoing reference edges.
|
||||
- **sortBy** (enum: "retainedSize", "selfSize", "id") _(optional)_: Sort order for results. Default is retainedSize.
|
||||
|
||||
---
|
||||
|
||||
## Extensions
|
||||
|
||||
> NOTE: The Extensions category is not active by default. Use the '--categoryExtensions' flag.
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
HeapSnapshotDetailedClassDiff,
|
||||
DuplicateStringGroup,
|
||||
HeapEdgesQueryOptions,
|
||||
HeapQueryOptions,
|
||||
} from './processors/HeapSnapshotManager.js';
|
||||
import {McpPage} from './McpPage.js';
|
||||
import {type UncaughtError} from './collectors/PageCollector.js';
|
||||
@@ -750,6 +751,13 @@ export class McpContext implements Context {
|
||||
return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
|
||||
}
|
||||
|
||||
async queryHeapSnapshotObjects(
|
||||
filePath: string,
|
||||
options: HeapQueryOptions,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
|
||||
return await this.#heapSnapshotManager.queryObjects(filePath, options);
|
||||
}
|
||||
|
||||
async getHeapSnapshotStats(
|
||||
filePath: string,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics> {
|
||||
|
||||
@@ -1100,6 +1100,88 @@ export const commands: Commands = {
|
||||
},
|
||||
},
|
||||
},
|
||||
query_heapsnapshot_objects: {
|
||||
description:
|
||||
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy). (requires flag: --memoryDebugging=true)',
|
||||
category: 'Memory',
|
||||
args: {
|
||||
filePath: {
|
||||
name: 'filePath',
|
||||
type: 'string',
|
||||
description: 'A path to a .heapsnapshot file to read.',
|
||||
required: true,
|
||||
},
|
||||
className: {
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: 'Optional regex or text matching object class name.',
|
||||
required: false,
|
||||
},
|
||||
propertyName: {
|
||||
name: 'propertyName',
|
||||
type: 'string',
|
||||
description:
|
||||
'Optional property name filter for outgoing reference edges.',
|
||||
required: false,
|
||||
},
|
||||
nodeType: {
|
||||
name: 'nodeType',
|
||||
type: 'string',
|
||||
description:
|
||||
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
|
||||
required: false,
|
||||
},
|
||||
minRetainedSize: {
|
||||
name: 'minRetainedSize',
|
||||
type: 'number',
|
||||
description: 'Minimum retained size in bytes.',
|
||||
required: false,
|
||||
},
|
||||
maxRetainedSize: {
|
||||
name: 'maxRetainedSize',
|
||||
type: 'number',
|
||||
description: 'Maximum retained size in bytes.',
|
||||
required: false,
|
||||
},
|
||||
minSelfSize: {
|
||||
name: 'minSelfSize',
|
||||
type: 'number',
|
||||
description: 'Minimum self size in bytes.',
|
||||
required: false,
|
||||
},
|
||||
maxSelfSize: {
|
||||
name: 'maxSelfSize',
|
||||
type: 'number',
|
||||
description: 'Maximum self size in bytes.',
|
||||
required: false,
|
||||
},
|
||||
isDetached: {
|
||||
name: 'isDetached',
|
||||
type: 'boolean',
|
||||
description: 'Whether to filter for detached DOM nodes.',
|
||||
required: false,
|
||||
},
|
||||
sortBy: {
|
||||
name: 'sortBy',
|
||||
type: 'string',
|
||||
description: 'Sort order for results. Default is retainedSize.',
|
||||
required: false,
|
||||
enum: ['retainedSize', 'selfSize', 'id'],
|
||||
},
|
||||
pageIdx: {
|
||||
name: 'pageIdx',
|
||||
type: 'number',
|
||||
description: 'The page index for pagination.',
|
||||
required: false,
|
||||
},
|
||||
pageSize: {
|
||||
name: 'pageSize',
|
||||
type: 'number',
|
||||
description: 'The page size for pagination.',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
reload_extension: {
|
||||
description:
|
||||
'Reloads an unpacked Chrome extension by its ID. (requires flag: --categoryExtensions=true)',
|
||||
|
||||
@@ -48,7 +48,6 @@ export type HeapQueryOptions =
|
||||
|
||||
export type HeapEdgesQueryOptions =
|
||||
DevTools.HeapSnapshotModel.HeapSnapshotModel.HeapEdgesQueryOptions;
|
||||
|
||||
const VALID_EXTENSIONS: readonly string[] = ['.heapsnapshot', '.heaptimeline'];
|
||||
|
||||
function hasValidHeapSnapshotExtension(filePath: string): boolean {
|
||||
@@ -423,6 +422,15 @@ export class HeapSnapshotManager {
|
||||
return await snapshot.getDuplicateStrings();
|
||||
}
|
||||
|
||||
async queryObjects(
|
||||
filePath: string,
|
||||
options: HeapQueryOptions,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
|
||||
const snapshot = await this.getSnapshot(filePath);
|
||||
const provider = snapshot.queryObjects(options);
|
||||
return await provider.serializeItemsRange(0, Infinity);
|
||||
}
|
||||
|
||||
hasSnapshots(): boolean {
|
||||
return this.#snapshots.size > 0;
|
||||
}
|
||||
|
||||
@@ -978,5 +978,58 @@
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "query_heapsnapshot_objects",
|
||||
"args": [
|
||||
{
|
||||
"name": "file_path_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "class_name_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "property_name_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "node_type_length",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "min_retained_size",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "max_retained_size",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "min_self_size",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "max_self_size",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "is_detached",
|
||||
"argType": "boolean"
|
||||
},
|
||||
{
|
||||
"name": "sort_by",
|
||||
"argType": "string"
|
||||
},
|
||||
{
|
||||
"name": "page_idx",
|
||||
"argType": "number"
|
||||
},
|
||||
{
|
||||
"name": "page_size",
|
||||
"argType": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
HeapSnapshotDetailedClassDiff,
|
||||
DuplicateStringGroup,
|
||||
HeapEdgesQueryOptions,
|
||||
HeapQueryOptions,
|
||||
} from '../processors/HeapSnapshotManager.js';
|
||||
import type {McpPage} from '../McpPage.js';
|
||||
import {zod} from '../third_party/index.js';
|
||||
@@ -315,6 +316,10 @@ export type Context = Readonly<{
|
||||
currentFilePath: string,
|
||||
classIndex: number,
|
||||
): Promise<HeapSnapshotDetailedClassDiff>;
|
||||
queryHeapSnapshotObjects(
|
||||
filePath: string,
|
||||
options: HeapQueryOptions,
|
||||
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -465,3 +465,80 @@ export const getHeapSnapshotObjectDetails = defineTool({
|
||||
response.setHeapSnapshotObjectDetails(objectInfo);
|
||||
},
|
||||
});
|
||||
|
||||
export const queryHeapSnapshotObjects = defineTool({
|
||||
name: 'query_heapsnapshot_objects',
|
||||
description:
|
||||
'Loads a memory heapsnapshot and queries objects matching specific filters (className, propertyName, nodeType, minRetainedSize, maxRetainedSize, minSelfSize, isDetached, sortBy).',
|
||||
annotations: {
|
||||
category: ToolCategory.MEMORY,
|
||||
readOnlyHint: true,
|
||||
conditions: ['memoryDebugging'],
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: {filePath: true},
|
||||
schema: {
|
||||
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
|
||||
className: zod
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional regex or text matching object class name.'),
|
||||
propertyName: zod
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Optional property name filter for outgoing reference edges.'),
|
||||
nodeType: zod
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Optional V8 node type filter (e.g. object, closure, string, array, code).',
|
||||
),
|
||||
minRetainedSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Minimum retained size in bytes.'),
|
||||
maxRetainedSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Maximum retained size in bytes.'),
|
||||
minSelfSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Minimum self size in bytes.'),
|
||||
maxSelfSize: zod
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Maximum self size in bytes.'),
|
||||
isDetached: zod
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether to filter for detached DOM nodes.'),
|
||||
sortBy: zod
|
||||
.enum(['retainedSize', 'selfSize', 'id'])
|
||||
.optional()
|
||||
.describe('Sort order for results. Default is retainedSize.'),
|
||||
pageIdx: zod.number().optional().describe('The page index for pagination.'),
|
||||
pageSize: zod.number().optional().describe('The page size for pagination.'),
|
||||
},
|
||||
handler: async (request, response, context) => {
|
||||
const range = await context.queryHeapSnapshotObjects(
|
||||
request.params.filePath,
|
||||
{
|
||||
className: request.params.className,
|
||||
propertyName: request.params.propertyName,
|
||||
nodeType: request.params.nodeType,
|
||||
minRetainedSize: request.params.minRetainedSize,
|
||||
maxRetainedSize: request.params.maxRetainedSize,
|
||||
minSelfSize: request.params.minSelfSize,
|
||||
maxSelfSize: request.params.maxSelfSize,
|
||||
isDetached: request.params.isDetached,
|
||||
sortBy: request.params.sortBy,
|
||||
},
|
||||
);
|
||||
|
||||
response.setHeapSnapshotNodes(range, {
|
||||
pageIdx: request.params.pageIdx,
|
||||
pageSize: request.params.pageSize,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -449,3 +449,66 @@ Retained by context size: 3.5 kB (148 objects)
|
||||
Not retained by context size: 798 kB (11792 objects)
|
||||
Total size: 802 kB
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with className filter 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
|
||||
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
|
||||
30967,Window (prototype) / https://example.com,object,4,26.6 kB,26.7 kB
|
||||
42085,Window (prototype) / https://example.com,object,4,26.6 kB,26.7 kB
|
||||
32995,Window (internal cache) / https://example.com,object,3,0.2 kB,10.1 kB
|
||||
41995,Window (internal cache) / https://example.com,object,3,0.2 kB,10.1 kB
|
||||
16321,Window / https://example.com,native,2,0.9 kB,1.9 kB
|
||||
16329,Window,closure,3,0.1 kB,0.4 kB
|
||||
16341,Window,closure,3,0.1 kB,0.4 kB
|
||||
30975,Window (prototype) / https://example.com,object,3,0.0 kB,0.2 kB
|
||||
Showing 1-10 of 34 (Page 1 of 4).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with default options 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
1,,synthetic,100000000,0.0 kB,802 kB
|
||||
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
|
||||
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
|
||||
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
|
||||
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
|
||||
3,(GC roots),synthetic,100000001,0.0 kB,45.6 kB
|
||||
49547,,array,2,4.1 kB,41.0 kB
|
||||
36241,,array,2,4.1 kB,41.0 kB
|
||||
Showing 1-10 of 27466 (Page 1 of 2747).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with minRetainedSize filter 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
1,,synthetic,100000000,0.0 kB,802 kB
|
||||
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
|
||||
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
|
||||
27635,Window (global*) / https://example.com,object,2,40.5 kB,51.0 kB
|
||||
30887,Window (global*) / https://example.com,object,2,40.5 kB,50.9 kB
|
||||
3,(GC roots),synthetic,100000001,0.0 kB,45.6 kB
|
||||
49547,,array,2,4.1 kB,41.0 kB
|
||||
36241,,array,2,4.1 kB,41.0 kB
|
||||
Showing 1-10 of 148 (Page 1 of 15).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
exports[`memory > query_heapsnapshot_objects > with sortBy selfSize and pagination 1`] = `
|
||||
## Heap Snapshot Data
|
||||
nodeId,nodeName,type,distance,selfSize,retainedSize
|
||||
1,,synthetic,100000000,0.0 kB,802 kB
|
||||
7249,system / NativeContext / https://example.com,hidden,1,1.2 kB,350 kB
|
||||
7199,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7307,system / NativeContext,hidden,1,1.2 kB,84.4 kB
|
||||
7195,system / NativeContext / https://example.com,hidden,1,1.2 kB,60.1 kB
|
||||
Showing 1-5 of 27466 (Page 1 of 5494).
|
||||
Next page: 1
|
||||
`;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
compareHeapSnapshots,
|
||||
getHeapSnapshotDuplicateStrings,
|
||||
getHeapSnapshotObjectDetails,
|
||||
queryHeapSnapshotObjects,
|
||||
} from '../../src/tools/memory.js';
|
||||
import {stableIdSymbol} from '../../src/utils/id.js';
|
||||
import {resolveCanonicalPath} from '../../src/utils/files.js';
|
||||
@@ -650,4 +651,94 @@ describe('memory', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('query_heapsnapshot_objects', () => {
|
||||
it('with default options', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
'tests/fixtures/example.heapsnapshot',
|
||||
);
|
||||
|
||||
await queryHeapSnapshotObjects.handler(
|
||||
{params: {filePath, pageSize: 10}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const responseData = await response.handle(context);
|
||||
const output = responseData.content
|
||||
.map(c => (c.type === 'text' ? c.text : ''))
|
||||
.join('\n');
|
||||
|
||||
t.assert.snapshot(output);
|
||||
});
|
||||
});
|
||||
|
||||
it('with className filter', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
'tests/fixtures/example.heapsnapshot',
|
||||
);
|
||||
|
||||
await queryHeapSnapshotObjects.handler(
|
||||
{params: {filePath, className: 'Window', pageSize: 10}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const responseData = await response.handle(context);
|
||||
const output = responseData.content
|
||||
.map(c => (c.type === 'text' ? c.text : ''))
|
||||
.join('\n');
|
||||
|
||||
t.assert.snapshot(output);
|
||||
});
|
||||
});
|
||||
|
||||
it('with minRetainedSize filter', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
'tests/fixtures/example.heapsnapshot',
|
||||
);
|
||||
|
||||
await queryHeapSnapshotObjects.handler(
|
||||
{params: {filePath, minRetainedSize: 1000, pageSize: 10}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const responseData = await response.handle(context);
|
||||
const output = responseData.content
|
||||
.map(c => (c.type === 'text' ? c.text : ''))
|
||||
.join('\n');
|
||||
|
||||
t.assert.snapshot(output);
|
||||
});
|
||||
});
|
||||
|
||||
it('with sortBy selfSize and pagination', async t => {
|
||||
await withMcpContext(async (response, context) => {
|
||||
const filePath = join(
|
||||
process.cwd(),
|
||||
'tests/fixtures/example.heapsnapshot',
|
||||
);
|
||||
|
||||
await queryHeapSnapshotObjects.handler(
|
||||
{params: {filePath, sortBy: 'selfSize', pageSize: 5, pageIdx: 0}},
|
||||
response,
|
||||
context,
|
||||
);
|
||||
|
||||
const responseData = await response.handle(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