feat: Report memory kept alive by contexts in get_heapsnapshot_summary (#2570)

Prints the size of objects kept alive by context objects in the heap
snapshot in the initial overview. This can help guide the agent into
looking into such objects in more detail.
This commit is contained in:
Dominik Inführ
2026-08-13 08:37:16 +00:00
committed by GitHub
parent 345ac9d678
commit 4103808ea5
10 changed files with 93 additions and 4 deletions
+1 -1
View File
@@ -592,7 +592,7 @@ in the DevTools Elements panel (if any).
### `get_heapsnapshot_summary`
**Description:** Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes. (requires flag: --memoryDebugging=true)
**Description:** Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes, and retained by context summary. (requires flag: --memoryDebugging=true)
**Parameters:**
+8
View File
@@ -756,6 +756,14 @@ export class McpContext implements Context {
return await this.#heapSnapshotManager.getNativeContextSizes(filePath);
}
async getHeapSnapshotRetainedByContextSummary(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary> {
return await this.#heapSnapshotManager.getRetainedByContextSummary(
filePath,
);
}
async getHeapSnapshotNodesById(
filePath: string,
id: number,
+17
View File
@@ -90,6 +90,7 @@ export class McpResponse implements Response {
stats?: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics;
staticData?: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null;
nativeContextSizes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes;
retainedByContextSummary?: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary;
nodes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange;
retainingPaths?: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths;
dominators?: DevTools.HeapSnapshotModel.HeapSnapshotModel.DominatorChain;
@@ -342,6 +343,7 @@ export class McpResponse implements Response {
stats: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics,
staticData: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null,
nativeContextSizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes,
retainedByContextSummary: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary,
) {
this.#heapSnapshotOptions = {
...this.#heapSnapshotOptions,
@@ -349,6 +351,7 @@ export class McpResponse implements Response {
stats,
staticData,
nativeContextSizes,
retainedByContextSummary,
};
}
@@ -786,6 +789,7 @@ export class McpResponse implements Response {
stats?: object;
staticData?: object;
nativeContextSizes?: object;
retainedByContextSummary?: object;
aggregateStats?: {
objectCount: number;
totalSelfSize: number;
@@ -1106,6 +1110,19 @@ Call ${handleDialog.name} to handle it before continuing.`);
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
structuredContent.heapSnapshot.nativeContextSizes = nativeContextSizes;
}
const retainedByContextSummary =
this.#heapSnapshotOptions.retainedByContextSummary;
if (retainedByContextSummary) {
response.push('### Retained by Context Summary');
response.push(
HeapSnapshotFormatter.formatRetainedByContextSummary(
retainedByContextSummary,
),
);
structuredContent.heapSnapshot = structuredContent.heapSnapshot || {};
structuredContent.heapSnapshot.retainedByContextSummary =
retainedByContextSummary;
}
const aggregateData = this.#heapSnapshotOptions.aggregateData;
if (aggregateData) {
const sortedEntries = HeapSnapshotFormatter.sort(
+1 -1
View File
@@ -623,7 +623,7 @@ export const commands: Commands = {
},
get_heapsnapshot_summary: {
description:
'Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes. (requires flag: --memoryDebugging=true)',
'Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes, and retained by context summary. (requires flag: --memoryDebugging=true)',
category: 'Memory',
args: {
filePath: {
+15
View File
@@ -159,6 +159,21 @@ export class HeapSnapshotFormatter {
return lines.join('\n');
}
static formatRetainedByContextSummary(
summary: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary,
): string {
const lines: string[] = [];
lines.push(`Context count: ${summary.contextCount}`);
lines.push(
`Retained by context size: ${formatBytesToKb(summary.retainedByContextSize)} (${summary.retainedByContextCount} objects)`,
);
lines.push(
`Not retained by context size: ${formatBytesToKb(summary.notRetainedByContextSize)} (${summary.notRetainedByContextCount} objects)`,
);
lines.push(`Total size: ${formatBytesToKb(summary.totalSize)}`);
return lines.join('\n');
}
#getSortedAggregates(): AggregatedInfoWithId[] {
return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
}
+7
View File
@@ -154,6 +154,13 @@ export class HeapSnapshotManager {
return await snapshot.getNativeContextSizes();
}
async getRetainedByContextSummary(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary> {
const snapshot = await this.getSnapshot(filePath);
return await snapshot.getRetainedByContextSummary();
}
async getOrCreateIdForClassKey(
filePath: string,
classKey: string,
+4
View File
@@ -124,6 +124,7 @@ export interface Response {
stats: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics,
staticData: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null,
nativeContextSizes: DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes,
retainedByContextSummary: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary,
): void;
setHeapSnapshotNodes(
nodes: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange,
@@ -265,6 +266,9 @@ export type Context = Readonly<{
getHeapSnapshotNativeContextSizes(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.NativeContextSizes>;
getHeapSnapshotRetainedByContextSummary(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainedByContextSummary>;
getHeapSnapshotNodesById(
filePath: string,
id: number,
+11 -2
View File
@@ -51,7 +51,7 @@ export const takeHeapSnapshot = definePageTool({
export const getHeapSnapshotSummary = defineTool({
name: 'get_heapsnapshot_summary',
description:
'Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes.',
'Loads a memory heapsnapshot and returns snapshot summary stats, including native contexts and their sizes, and retained by context summary.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
@@ -70,8 +70,17 @@ export const getHeapSnapshotSummary = defineTool({
const nativeContextSizes = await context.getHeapSnapshotNativeContextSizes(
request.params.filePath,
);
const retainedByContextSummary =
await context.getHeapSnapshotRetainedByContextSummary(
request.params.filePath,
);
response.setHeapSnapshotStats(stats, staticData, nativeContextSizes);
response.setHeapSnapshotStats(
stats,
staticData,
nativeContextSizes,
retainedByContextSummary,
);
},
});
@@ -334,4 +334,28 @@ describe('HeapSnapshotFormatter', () => {
assert.strictEqual(result, expected);
});
});
describe('formatRetainedByContextSummary', () => {
it('formats retained by context summary correctly', () => {
const mockSummary = {
contextCount: 2,
retainedByContextSize: 5000,
retainedByContextCount: 10,
notRetainedByContextSize: 1000,
notRetainedByContextCount: 5,
totalSize: 6000,
};
const result =
HeapSnapshotFormatter.formatRetainedByContextSummary(mockSummary);
const expected = [
'Context count: 2',
`Retained by context size: ${formatBytesToKb(5000)} (10 objects)`,
`Not retained by context size: ${formatBytesToKb(1000)} (5 objects)`,
`Total size: ${formatBytesToKb(6000)}`,
].join('\n');
assert.strictEqual(result, expected);
});
});
});
+5
View File
@@ -443,4 +443,9 @@ nodeId,nodeName,selfSize,retainedSize,attributedSize
7199,system / NativeContext,1.2 kB,84.4 kB,84.4 kB
Shared Size: 22.9 kB
Unattributed Size: 44.8 kB
### Retained by Context Summary
Context count: 116
Retained by context size: 3.5 kB (148 objects)
Not retained by context size: 798 kB (11792 objects)
Total size: 802 kB
`;