feat: Filter heap snapshot objects by native context (#2377)

get_heapsnapshot_details can filter objects in the heap snapshot by
their native context.
This commit is contained in:
Dominik Inführ
2026-07-17 10:55:08 +02:00
committed by GitHub
parent 77e1d3f961
commit 76fd242498
9 changed files with 174 additions and 9 deletions
+4 -2
View File
@@ -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.
@@ -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.
+26 -6
View File
@@ -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;
@@ -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`);
+8 -1
View File
@@ -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(
@@ -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,
);
}
+20
View File
@@ -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',
@@ -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',
+8
View File
@@ -688,6 +688,10 @@
{
"name": "filter_name",
"argType": "string"
},
{
"name": "object_id",
"argType": "number"
}
]
},
@@ -709,6 +713,10 @@
{
"name": "filter_name",
"argType": "string"
},
{
"name": "object_id",
"argType": "number"
}
]
},
+2
View File
@@ -237,6 +237,7 @@ export type Context = Readonly<{
getHeapSnapshotAggregates(
filePath: string,
filterName?: string,
objectId?: number,
): Promise<HeapSnapshotAggregateData>;
getHeapSnapshotDuplicateStrings(
filePath: string,
@@ -254,6 +255,7 @@ export type Context = Readonly<{
filePath: string,
id: number,
filterName?: string,
objectId?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
getHeapSnapshotRetainers(
filePath: string,
+17
View File
@@ -14,6 +14,9 @@ const HEAP_SNAPSHOT_FILTERS: readonly [string, ...string[]] = [
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
'sharedNativeContext',
'noNativeContext',
'attributedToSpecificNativeContext',
];
export const takeHeapSnapshot = definePageTool({
@@ -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()
@@ -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, {
@@ -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.'),
},
@@ -137,6 +153,7 @@ export const getHeapSnapshotClassNodes = defineTool({
request.params.filePath,
request.params.id,
request.params.filterName,
request.params.objectId,
);
response.setHeapSnapshotNodes(nodes, {
+32
View File
@@ -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
@@ -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
+57
View File
@@ -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', () => {