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:
@@ -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:**
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user