feat: Emit native contexts in snapshot summary (#2375)

Emits native contexts and their sizes with the get_heapsnapshot_summary
MCP tool. This should help the agent get an overview about which native
contexts consume the most memory.

Co-authored-by: Dominik Inführ <dinfuehr@chromium.org>
This commit is contained in:
Dominik Inführ
2026-07-16 13:16:00 +02:00
committed by GitHub
parent 9a7e04daa2
commit f78a911dc0
10 changed files with 106 additions and 4 deletions
+1 -1
View File
@@ -568,7 +568,7 @@ in the DevTools Elements panel (if any).
### `get_heapsnapshot_summary`
**Description:** Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --memoryDebugging=true)
**Description:** Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes. (requires flag: --memoryDebugging=true)
**Parameters:**
+7
View File
@@ -120,6 +120,13 @@ export class HeapSnapshotManager {
return snapshot.staticData;
}
async getNativeContextSizes(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes> {
const snapshot = await this.getSnapshot(filePath);
return await snapshot.getNativeContextSizes();
}
async getOrCreateIdForClassKey(
filePath: string,
classKey: string,
+6
View File
@@ -668,6 +668,12 @@ export class McpContext implements Context {
return await this.#heapSnapshotManager.getStaticData(filePath);
}
async getHeapSnapshotNativeContextSizes(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes> {
return await this.#heapSnapshotManager.getNativeContextSizes(filePath);
}
async getHeapSnapshotNodesById(
filePath: string,
id: number,
+13
View File
@@ -83,6 +83,7 @@ export class McpResponse implements Response {
pagination?: PaginationOptions;
stats?: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics;
staticData?: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null;
nativeContextSizes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes;
nodes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange;
retainingPaths?: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths;
dominators?: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain;
@@ -330,12 +331,14 @@ export class McpResponse implements Response {
setHeapSnapshotStats(
stats: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics,
staticData: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null,
nativeContextSizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes,
) {
this.#heapSnapshotOptions = {
...this.#heapSnapshotOptions,
include: true,
stats,
staticData,
nativeContextSizes,
};
}
@@ -730,6 +733,7 @@ export class McpResponse implements Response {
heapSnapshot?: {
stats?: object;
staticData?: object;
nativeContextSizes?: object;
aggregateStats?: {
objectCount: number;
totalSelfSize: number;
@@ -1035,6 +1039,15 @@ Call ${handleDialog.name} to handle it before continuing.`);
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
structuredContent.heapSnapshot.staticData = staticData;
}
const nativeContextSizes = this.#heapSnapshotOptions.nativeContextSizes;
if (nativeContextSizes) {
response.push('### Native Contexts');
response.push(
HeapSnapshotFormatter.formatNativeContextSizes(nativeContextSizes),
);
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
structuredContent.heapSnapshot.nativeContextSizes = nativeContextSizes;
}
const aggregateData = this.#heapSnapshotOptions.aggregateData;
if (aggregateData) {
const sortedEntries = HeapSnapshotFormatter.sort(
+1 -1
View File
@@ -557,7 +557,7 @@ export const commands: Commands = {
},
get_heapsnapshot_summary: {
description:
'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --memoryDebugging=true)',
'Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes. (requires flag: --memoryDebugging=true)',
category: 'Memory',
args: {
filePath: {
+22
View File
@@ -137,6 +137,28 @@ export class HeapSnapshotFormatter {
return lines.join('\n');
}
static formatNativeContextSizes(
sizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes,
): string {
const lines: string[] = [];
lines.push('nodeId,nodeName,selfSize,retainedSize,attributedSize');
const sortedContexts = [...sizes.nativeContexts].sort(
(a, b) => b.attributedSize - a.attributedSize,
);
for (const nc of sortedContexts) {
lines.push(
`${nc.nodeId},${nc.nodeName},${DevTools.I18n.ByteUtilities.formatBytesToKb(nc.selfSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(nc.retainedSize)},${DevTools.I18n.ByteUtilities.formatBytesToKb(nc.attributedSize)}`,
);
}
lines.push(
`Shared Size: ${DevTools.I18n.ByteUtilities.formatBytesToKb(sizes.sharedSize)}`,
);
lines.push(
`Unattributed Size: ${DevTools.I18n.ByteUtilities.formatBytesToKb(sizes.noAttributionSize)}`,
);
return lines.join('\n');
}
#getSortedAggregates(): AggregatedInfoWithId[] {
return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
}
+4
View File
@@ -114,6 +114,7 @@ export interface Response {
setHeapSnapshotStats(
stats: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics,
staticData: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null,
nativeContextSizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes,
): void;
setHeapSnapshotNodes(
nodes: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange,
@@ -243,6 +244,9 @@ export type Context = Readonly<{
getHeapSnapshotStaticData(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null>;
getHeapSnapshotNativeContextSizes(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes>;
getHeapSnapshotNodesById(
filePath: string,
id: number,
+5 -2
View File
@@ -48,7 +48,7 @@ export const takeHeapSnapshot = definePageTool({
export const getHeapSnapshotSummary = defineTool({
name: 'get_heapsnapshot_summary',
description:
'Loads a memory heapsnapshot and returns snapshot summary stats.',
'Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
@@ -64,8 +64,11 @@ export const getHeapSnapshotSummary = defineTool({
const staticData = await context.getHeapSnapshotStaticData(
request.params.filePath,
);
const nativeContextSizes = await context.getHeapSnapshotNativeContextSizes(
request.params.filePath,
);
response.setHeapSnapshotStats(stats, staticData);
response.setHeapSnapshotStats(stats, staticData, nativeContextSizes);
},
});
@@ -293,4 +293,43 @@ describe('HeapSnapshotFormatter', () => {
assert.strictEqual(result, expected);
});
});
describe('formatNativeContextSizes', () => {
it('formats native context sizes as CSV with summary lines', () => {
const mockSizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes =
{
nativeContexts: [
{
nodeId: 10,
nodeIndex: 1,
nodeName: 'system / NativeContext',
attributedSize: 500,
retainedSize: 1000,
selfSize: 100,
},
{
nodeId: 20,
nodeIndex: 2,
nodeName: 'system / NativeContext / https://example.com',
attributedSize: 2000,
retainedSize: 5000,
selfSize: 200,
},
],
sharedSize: 300,
noAttributionSize: 400,
};
const result = HeapSnapshotFormatter.formatNativeContextSizes(mockSizes);
const expected = [
'nodeId,nodeName,selfSize,retainedSize,attributedSize',
`20,system / NativeContext / https://example.com,${DevTools.I18n.ByteUtilities.formatBytesToKb(200)},${DevTools.I18n.ByteUtilities.formatBytesToKb(5000)},${DevTools.I18n.ByteUtilities.formatBytesToKb(2000)}`,
`10,system / NativeContext,${DevTools.I18n.ByteUtilities.formatBytesToKb(100)},${DevTools.I18n.ByteUtilities.formatBytesToKb(1000)},${DevTools.I18n.ByteUtilities.formatBytesToKb(500)}`,
`Shared Size: ${DevTools.I18n.ByteUtilities.formatBytesToKb(300)}`,
`Unattributed Size: ${DevTools.I18n.ByteUtilities.formatBytesToKb(400)}`,
].join('\n');
assert.strictEqual(result, expected);
});
});
});
+8
View File
@@ -389,4 +389,12 @@ Static Data: {
"totalSize": 1121496,
"maxJSObjectId": 54005
}
### Native Contexts
nodeId,nodeName,selfSize,retainedSize,attributedSize
7249,system / NativeContext / https://example.com,1.2 kB,350 kB,350 kB
7195,system / NativeContext / https://example.com,1.2 kB,60.1 kB,215 kB
7307,system / NativeContext,1.2 kB,84.4 kB,84.6 kB
7199,system / NativeContext,1.2 kB,84.4 kB,84.4 kB
Shared Size: 22.9 kB
Unattributed Size: 44.8 kB
`;