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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user