Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,8 @@ in the DevTools Elements panel (if any).

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **id** (number) **(required)**: The ID for the class, obtained from details.
- **filterName** (enum: "objectsRetainedByDetachedDomNodes", "objectsRetainedByConsole", "objectsRetainedByEventHandlers", "objectsRetainedByContexts") _(optional)_: An optional filter to apply to the nodes.
- **filterName** (enum: "objectsRetainedByDetachedDomNodes", "objectsRetainedByConsole", "objectsRetainedByEventHandlers", "objectsRetainedByContexts", "sharedNativeContext", "noNativeContext", "attributedToSpecificNativeContext") _(optional)_: An optional filter to apply to the nodes.
- **objectId** (number) _(optional)_: The object ID (nodeId) of the specific native context to filter by when filterName is attributedToSpecificNativeContext.
- **pageIdx** (number) _(optional)_: The page index for pagination.
- **pageSize** (number) _(optional)_: The page size for pagination.

Expand All @@ -498,7 +499,8 @@ in the DevTools Elements panel (if any).
**Parameters:**

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **filterName** (enum: "objectsRetainedByDetachedDomNodes", "objectsRetainedByConsole", "objectsRetainedByEventHandlers", "objectsRetainedByContexts") _(optional)_: An optional filter to apply to the aggregates.
- **filterName** (enum: "objectsRetainedByDetachedDomNodes", "objectsRetainedByConsole", "objectsRetainedByEventHandlers", "objectsRetainedByContexts", "sharedNativeContext", "noNativeContext", "attributedToSpecificNativeContext") _(optional)_: An optional filter to apply to the aggregates.
- **objectId** (number) _(optional)_: The object ID (nodeId) of the specific native context to filter by when filterName is attributedToSpecificNativeContext.
- **pageIdx** (number) _(optional)_: The page index for pagination of aggregates.
- **pageSize** (number) _(optional)_: The page size for pagination of aggregates.

Expand Down
32 changes: 26 additions & 6 deletions src/HeapSnapshotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,37 @@ export class HeapSnapshotManager {
return snapshot;
}

async #applyNodeFilter(
snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy,
filter: DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter,
filterName?: string,
objectId?: number,
): Promise<void> {
if (filterName === 'attributedToSpecificNativeContext') {
if (objectId === undefined) {
throw new Error(
'objectId is required when filterName is attributedToSpecificNativeContext',
);
}
const nodeIndex = await snapshot.nodeIndexForId(objectId);
if (nodeIndex === undefined) {
throw new Error(`Node with ID ${objectId} not found`);
}
filter.filterName = `nativeContext_${nodeIndex}`;
} else if (filterName) {
filter.filterName = filterName;
}
}

async getAggregates(
filePath: string,
filterName?: string,
objectId?: number,
): Promise<HeapSnapshotAggregateData> {
const snapshot = await this.getSnapshot(filePath);
const filter =
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
if (filterName) {
filter.filterName = filterName;
}
await this.#applyNodeFilter(snapshot, filter, filterName, objectId);
const aggregates: Record<string, AggregatedInfoWithId> =
await snapshot.aggregatesWithFilter(filter);
let objectCount = 0;
Expand Down Expand Up @@ -145,13 +166,12 @@ export class HeapSnapshotManager {
filePath: string,
id: number,
filterName?: string,
objectId?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
const snapshot = await this.getSnapshot(filePath);
const filter =
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
if (filterName) {
filter.filterName = filterName;
}
await this.#applyNodeFilter(snapshot, filter, filterName, objectId);
const className = await this.resolveClassKeyFromId(filePath, id);
if (!className) {
throw new Error(`Class with ID ${id} not found in heap snapshot`);
Expand Down
9 changes: 8 additions & 1 deletion src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,13 @@ export class McpContext implements Context {
async getHeapSnapshotAggregates(
filePath: string,
filterName?: string,
objectId?: number,
): Promise<HeapSnapshotAggregateData> {
return await this.#heapSnapshotManager.getAggregates(filePath, filterName);
return await this.#heapSnapshotManager.getAggregates(
filePath,
filterName,
objectId,
);
}

async getHeapSnapshotDuplicateStrings(
Expand Down Expand Up @@ -678,11 +683,13 @@ export class McpContext implements Context {
filePath: string,
id: number,
filterName?: string,
objectId?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
return await this.#heapSnapshotManager.getNodesById(
filePath,
id,
filterName,
objectId,
);
}

Expand Down
20 changes: 20 additions & 0 deletions src/bin/chrome-devtools-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,18 @@ export const commands: Commands = {
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
'sharedNativeContext',
'noNativeContext',
'attributedToSpecificNativeContext',
],
},
objectId: {
name: 'objectId',
type: 'number',
description:
'The object ID (nodeId) of the specific native context to filter by when filterName is attributedToSpecificNativeContext.',
required: false,
},
pageIdx: {
name: 'pageIdx',
type: 'number',
Expand Down Expand Up @@ -396,8 +406,18 @@ export const commands: Commands = {
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
'sharedNativeContext',
'noNativeContext',
'attributedToSpecificNativeContext',
],
},
objectId: {
name: 'objectId',
type: 'number',
description:
'The object ID (nodeId) of the specific native context to filter by when filterName is attributedToSpecificNativeContext.',
required: false,
},
pageIdx: {
name: 'pageIdx',
type: 'number',
Expand Down
8 changes: 8 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,10 @@
{
"name": "filter_name",
"argType": "string"
},
{
"name": "object_id",
"argType": "number"
}
]
},
Expand All @@ -709,6 +713,10 @@
{
"name": "filter_name",
"argType": "string"
},
{
"name": "object_id",
"argType": "number"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ export type Context = Readonly<{
getHeapSnapshotAggregates(
filePath: string,
filterName?: string,
objectId?: number,
): Promise<HeapSnapshotAggregateData>;
getHeapSnapshotDuplicateStrings(
filePath: string,
Expand All @@ -254,6 +255,7 @@ export type Context = Readonly<{
filePath: string,
id: number,
filterName?: string,
objectId?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
getHeapSnapshotRetainers(
filePath: string,
Expand Down
17 changes: 17 additions & 0 deletions src/tools/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const HEAP_SNAPSHOT_FILTERS: readonly [string, ...string[]] = [
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
'sharedNativeContext',
'noNativeContext',
'attributedToSpecificNativeContext',
];

export const takeHeapSnapshot = definePageTool({
Expand Down Expand Up @@ -87,6 +90,12 @@ export const getHeapSnapshotDetails = defineTool({
.enum(HEAP_SNAPSHOT_FILTERS)
.optional()
.describe('An optional filter to apply to the aggregates.'),
objectId: zod
.number()
.optional()
.describe(
'The object ID (nodeId) of the specific native context to filter by when filterName is attributedToSpecificNativeContext.',
),
pageIdx: zod
.number()
.optional()
Expand All @@ -102,6 +111,7 @@ export const getHeapSnapshotDetails = defineTool({
const aggregates = await context.getHeapSnapshotAggregates(
request.params.filePath,
request.params.filterName,
request.params.objectId,
);

response.setHeapSnapshotAggregates(aggregates, {
Expand All @@ -127,6 +137,12 @@ export const getHeapSnapshotClassNodes = defineTool({
.enum(HEAP_SNAPSHOT_FILTERS)
.optional()
.describe('An optional filter to apply to the nodes.'),
objectId: zod
.number()
.optional()
.describe(
'The object ID (nodeId) of the specific native context to filter by when filterName is attributedToSpecificNativeContext.',
),
pageIdx: zod.number().optional().describe('The page index for pagination.'),
pageSize: zod.number().optional().describe('The page size for pagination.'),
},
Expand All @@ -137,6 +153,7 @@ export const getHeapSnapshotClassNodes = defineTool({
request.params.filePath,
request.params.id,
request.params.filterName,
request.params.objectId,
);

response.setHeapSnapshotNodes(nodes, {
Expand Down
32 changes: 32 additions & 0 deletions tests/tools/memory.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ nodeId,nodeName,type,distance,selfSize,retainedSize
Showing 1-6 of 6 (Page 1 of 1).
`;

exports[`memory > get_heapsnapshot_details > with attributedToSpecificNativeContext filterName and objectId 1`] = `
## Heap Snapshot Data
Objects: 4589
Total shallow size: 350 kB
Showing 1-10 of 150 (Page 1 of 15).
Next page: 1
id,name,count,selfSize,maxRetainedSize
1,(system),1739,115 kB,350 kB
7,Function,1095,34.9 kB,72.2 kB
11,(object shape),769,52.6 kB,53.3 kB
10,Window (global*) / https://example.com,1,40.5 kB,51.0 kB
36,(array),6,9.1 kB,46.0 kB
17,Window (prototype) / https://example.com,2,26.7 kB,26.9 kB
4,(compiled code),323,19.3 kB,19.4 kB
3,(string),376,10.5 kB,10.5 kB
19,Window (internal cache) / https://example.com,2,0.2 kB,10.1 kB
72,Document,1,8.4 kB,8.7 kB
`;

exports[`memory > get_heapsnapshot_details > with default options 1`] = `
## Heap Snapshot Data
Objects: 11940
Expand Down Expand Up @@ -238,6 +257,19 @@ id,name,count,selfSize,maxRetainedSize
4,Array,4,0.3 kB,0.3 kB
`;

exports[`memory > get_heapsnapshot_details > with sharedNativeContext filterName 1`] = `
## Heap Snapshot Data
Objects: 558
Total shallow size: 22.9 kB
Showing 1-5 of 5 (Page 1 of 1).
id,name,count,selfSize,maxRetainedSize
3,(string),443,16.6 kB,16.6 kB
1,(compiled code),85,5.9 kB,9.8 kB
4,(system),12,0.2 kB,0.6 kB
2,(number),16,0.2 kB,0.2 kB
5,(concatenated string),2,0.0 kB,0.1 kB
`;

exports[`memory > get_heapsnapshot_dominators > with valid nodeId 1`] = `
## Heap Snapshot Data
### Dominator Chain
Expand Down
57 changes: 57 additions & 0 deletions tests/tools/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,63 @@ describe('memory', () => {
t.assert.snapshot(output);
});
});

it('with sharedNativeContext filterName', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);

await getHeapSnapshotDetails.handler(
{params: {filePath, filterName: 'sharedNativeContext'}},
response,
context,
);

const responseData = await response.handle(
getHeapSnapshotDetails.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');

t.assert.snapshot(output);
});
});

it('with attributedToSpecificNativeContext filterName and objectId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);

await getHeapSnapshotDetails.handler(
{
params: {
filePath,
filterName: 'attributedToSpecificNativeContext',
objectId: 7249,
pageSize: 10,
},
},
response,
context,
);

const responseData = await response.handle(
getHeapSnapshotDetails.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');

t.assert.snapshot(output);
});
});
});

describe('get_heapsnapshot_class_nodes', () => {
Expand Down
Loading