Skip to content

Commit 9211c6b

Browse files
chore(memory): expose a tool for getting the inital data. (#1909)
This refactors the code to extract the Id logic from the PageCollector and provide it in the heapsnapshot. We need to use an UID as we need the internal ClassKey to query the heapsnapshot, but that is a strange string (usually looking like `,ClassName`) which may get the LLM confused as we use comma separated output.
1 parent ec895f1 commit 9211c6b

16 files changed

Lines changed: 392 additions & 64 deletions

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,11 +497,10 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
497497
- **Emulation** (2 tools)
498498
- [`emulate`](docs/tool-reference.md#emulate)
499499
- [`resize_page`](docs/tool-reference.md#resize_page)
500-
- **Performance** (4 tools)
500+
- **Performance** (3 tools)
501501
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
502502
- [`performance_start_trace`](docs/tool-reference.md#performance_start_trace)
503503
- [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace)
504-
- [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
505504
- **Network** (2 tools)
506505
- [`get_network_request`](docs/tool-reference.md#get_network_request)
507506
- [`list_network_requests`](docs/tool-reference.md#list_network_requests)
@@ -512,6 +511,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
512511
- [`list_console_messages`](docs/tool-reference.md#list_console_messages)
513512
- [`take_screenshot`](docs/tool-reference.md#take_screenshot)
514513
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
514+
- **Memory** (1 tools)
515+
- [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
515516

516517
<!-- END AUTO GENERATED TOOLS -->
517518

docs/tool-reference.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222
- **[Emulation](#emulation)** (2 tools)
2323
- [`emulate`](#emulate)
2424
- [`resize_page`](#resize_page)
25-
- **[Performance](#performance)** (4 tools)
25+
- **[Performance](#performance)** (3 tools)
2626
- [`performance_analyze_insight`](#performance_analyze_insight)
2727
- [`performance_start_trace`](#performance_start_trace)
2828
- [`performance_stop_trace`](#performance_stop_trace)
29-
- [`take_memory_snapshot`](#take_memory_snapshot)
3029
- **[Network](#network)** (2 tools)
3130
- [`get_network_request`](#get_network_request)
3231
- [`list_network_requests`](#list_network_requests)
@@ -37,6 +36,8 @@
3736
- [`list_console_messages`](#list_console_messages)
3837
- [`take_screenshot`](#take_screenshot)
3938
- [`take_snapshot`](#take_snapshot)
39+
- **[Memory](#memory)** (1 tools)
40+
- [`take_memory_snapshot`](#take_memory_snapshot)
4041

4142
## Input automation
4243

@@ -276,16 +277,6 @@
276277

277278
---
278279

279-
### `take_memory_snapshot`
280-
281-
**Description:** Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.
282-
283-
**Parameters:**
284-
285-
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to save the heapsnapshot to.
286-
287-
---
288-
289280
## Network
290281

291282
### `get_network_request`
@@ -398,3 +389,15 @@ in the DevTools Elements panel (if any).
398389
- **verbose** (boolean) _(optional)_: Whether to include all possible information available in the full a11y tree. Default is false.
399390

400391
---
392+
393+
## Memory
394+
395+
### `take_memory_snapshot`
396+
397+
**Description:** Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.
398+
399+
**Parameters:**
400+
401+
- **filePath** (string) **(required)**: A path to a .heapsnapshot file to save the heapsnapshot to.
402+
403+
---

src/HeapSnapshotManager.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,25 @@ import fsSync from 'node:fs';
88
import path from 'node:path';
99

1010
import {DevTools} from './third_party/index.js';
11+
import {
12+
createIdGenerator,
13+
stableIdSymbol,
14+
type WithSymbolId,
15+
} from './utils/id.js';
16+
17+
export type AggregatedInfoWithUid =
18+
WithSymbolId<DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>;
1119

1220
export class HeapSnapshotManager {
1321
#snapshots = new Map<
1422
string,
1523
{
1624
snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy;
1725
worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy;
26+
// TODO: use a multimap
27+
uidToClassKey: Map<number, string>;
28+
classKeyToUid: Map<string, number>;
29+
idGenerator: () => number;
1830
}
1931
>();
2032

@@ -28,20 +40,35 @@ export class HeapSnapshotManager {
2840
}
2941

3042
const {snapshot, worker} = await this.#loadSnapshot(absolutePath);
31-
this.#snapshots.set(absolutePath, {snapshot, worker});
43+
this.#snapshots.set(absolutePath, {
44+
snapshot,
45+
worker,
46+
uidToClassKey: new Map<number, string>(),
47+
classKeyToUid: new Map<string, number>(),
48+
idGenerator: createIdGenerator(),
49+
});
3250

3351
return snapshot;
3452
}
3553

3654
async getAggregates(
3755
filePath: string,
38-
): Promise<
39-
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
40-
> {
56+
): Promise<Record<string, AggregatedInfoWithUid>> {
4157
const snapshot = await this.getSnapshot(filePath);
4258
const filter =
4359
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
44-
return await snapshot.aggregatesWithFilter(filter);
60+
const aggregates: Record<string, AggregatedInfoWithUid> =
61+
await snapshot.aggregatesWithFilter(filter);
62+
63+
for (const key of Object.keys(aggregates)) {
64+
const uid = await this.getOrCreateUidForClassKey(filePath, key);
65+
const aggregate = aggregates[key];
66+
if (aggregate) {
67+
aggregate[stableIdSymbol] = uid;
68+
}
69+
}
70+
71+
return aggregates;
4572
}
4673

4774
async getStats(
@@ -58,6 +85,29 @@ export class HeapSnapshotManager {
5885
return snapshot.staticData;
5986
}
6087

88+
async getOrCreateUidForClassKey(
89+
filePath: string,
90+
classKey: string,
91+
): Promise<number> {
92+
const cached = this.#getCachedSnapshot(filePath);
93+
let uid = cached.classKeyToUid.get(classKey);
94+
if (!uid) {
95+
uid = cached.idGenerator();
96+
cached.classKeyToUid.set(classKey, uid);
97+
cached.uidToClassKey.set(uid, classKey);
98+
}
99+
return uid;
100+
}
101+
102+
#getCachedSnapshot(filePath: string) {
103+
const absolutePath = path.resolve(filePath);
104+
const cached = this.#snapshots.get(absolutePath);
105+
if (!cached) {
106+
throw new Error(`Snapshot not loaded for ${filePath}`);
107+
}
108+
return cached;
109+
}
110+
61111
async #loadSnapshot(absolutePath: string): Promise<{
62112
snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy;
63113
worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy;

src/McpContext.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from 'node:path';
1010
import type {TargetUniverse} from './DevtoolsUtils.js';
1111
import {UniverseManager} from './DevtoolsUtils.js';
1212
import {HeapSnapshotManager} from './HeapSnapshotManager.js';
13+
import type {AggregatedInfoWithUid} from './HeapSnapshotManager.js';
1314
import {McpPage} from './McpPage.js';
1415
import {
1516
NetworkCollector,
@@ -906,9 +907,7 @@ export class McpContext implements Context {
906907

907908
async getHeapSnapshotAggregates(
908909
filePath: string,
909-
): Promise<
910-
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
911-
> {
910+
): Promise<Record<string, AggregatedInfoWithUid>> {
912911
return await this.#heapSnapshotManager.getAggregates(filePath);
913912
}
914913

src/PageCollector.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import {
2222
type Page,
2323
type PageEvents as PuppeteerPageEvents,
2424
} from './third_party/index.js';
25+
import {
26+
createIdGenerator,
27+
stableIdSymbol,
28+
type WithSymbolId,
29+
} from './utils/id.js';
2530

2631
export class UncaughtError {
2732
readonly details: Protocol.Runtime.ExceptionDetails;
@@ -42,21 +47,6 @@ export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
4247
[K in keyof EventMap]?: (event: EventMap[K]) => void;
4348
};
4449

45-
function createIdGenerator() {
46-
let i = 1;
47-
return () => {
48-
if (i === Number.MAX_SAFE_INTEGER) {
49-
i = 0;
50-
}
51-
return i++;
52-
};
53-
}
54-
55-
export const stableIdSymbol = Symbol('stableIdSymbol');
56-
type WithSymbolId<T> = T & {
57-
[stableIdSymbol]?: number;
58-
};
59-
6050
export class PageCollector<T> {
6151
#browser: Browser;
6252
#listenersInitializer: (

src/bin/cliDefinitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ export const commands: Commands = {
590590
take_memory_snapshot: {
591591
description:
592592
'Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.',
593-
category: 'Performance',
593+
category: 'Memory',
594594
args: {
595595
filePath: {
596596
name: 'filePath',

src/formatters/HeapSnapshotFormatter.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,39 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type {AggregatedInfoWithUid} from '../HeapSnapshotManager.js';
78
import type {DevTools} from '../third_party/index.js';
9+
import {stableIdSymbol} from '../utils/id.js';
810

911
export interface FormattedSnapshotEntry {
1012
className: string;
13+
classUid?: number;
1114
count: number;
1215
selfSize: number;
1316
retainedSize: number;
1417
}
1518

1619
export class HeapSnapshotFormatter {
17-
#aggregates: Record<
18-
string,
19-
DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo
20-
>;
20+
#aggregates: Record<string, AggregatedInfoWithUid>;
2121

22-
constructor(
23-
aggregates: Record<
24-
string,
25-
DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo
26-
>,
27-
) {
22+
constructor(aggregates: Record<string, AggregatedInfoWithUid>) {
2823
this.#aggregates = aggregates;
2924
}
3025

31-
#getSortedAggregates(): DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo[] {
26+
#getSortedAggregates(): AggregatedInfoWithUid[] {
3227
return Object.values(this.#aggregates).sort((a, b) => b.self - a.self);
3328
}
3429

3530
toString(): string {
3631
const sorted = this.#getSortedAggregates();
3732
const lines: string[] = [];
38-
lines.push('className,count,selfSize,maxRetainedSize');
33+
lines.push('uid,className,count,selfSize,maxRetainedSize');
3934

4035
for (const info of sorted) {
41-
lines.push(`"${info.name}",${info.count},${info.self},${info.maxRet}`);
36+
const uid = info[stableIdSymbol] ?? '';
37+
lines.push(
38+
`${uid},"${info.name}",${info.count},${info.self},${info.maxRet}`,
39+
);
4240
}
4341

4442
return lines.join('\n');
@@ -47,6 +45,7 @@ export class HeapSnapshotFormatter {
4745
toJSON(): FormattedSnapshotEntry[] {
4846
const sorted = this.#getSortedAggregates();
4947
return sorted.map(info => ({
48+
uid: info[stableIdSymbol],
5049
className: info.name,
5150
count: info.count,
5251
selfSize: info.self,

src/telemetry/tool_call_metrics.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,5 +569,22 @@
569569
"argType": "number"
570570
}
571571
]
572+
},
573+
{
574+
"name": "get_memory_snapshot_details",
575+
"args": [
576+
{
577+
"name": "file_path_length",
578+
"argType": "number"
579+
},
580+
{
581+
"name": "page_idx",
582+
"argType": "number"
583+
},
584+
{
585+
"name": "page_size",
586+
"argType": "number"
587+
}
588+
]
572589
}
573590
]

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import type {ParsedArguments} from '../bin/chrome-devtools-mcp-cli-options.js';
8+
import type {AggregatedInfoWithUid} from '../HeapSnapshotManager.js';
89
import type {McpPage} from '../McpPage.js';
910
import {zod} from '../third_party/index.js';
1011
import type {
@@ -227,9 +228,7 @@ export type Context = Readonly<{
227228
): string | undefined;
228229
getHeapSnapshotAggregates(
229230
filePath: string,
230-
): Promise<
231-
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
232-
>;
231+
): Promise<Record<string, AggregatedInfoWithUid>>;
233232
getHeapSnapshotStats(
234233
filePath: string,
235234
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics>;

src/tools/memory.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const takeMemorySnapshot = definePageTool({
1414
name: 'take_memory_snapshot',
1515
description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
1616
annotations: {
17-
category: ToolCategory.PERFORMANCE,
17+
category: ToolCategory.MEMORY,
1818
readOnlyHint: false,
1919
},
2020
schema: {
@@ -38,7 +38,7 @@ export const takeMemorySnapshot = definePageTool({
3838
export const exploreMemorySnapshot = defineTool({
3939
name: 'load_memory_snapshot',
4040
description:
41-
'Loads a memory heapsnapshot and returns snapshot summary stats. ',
41+
'Loads a memory heapsnapshot and returns snapshot summary stats.',
4242
annotations: {
4343
category: ToolCategory.MEMORY,
4444
readOnlyHint: true,
@@ -56,3 +56,35 @@ export const exploreMemorySnapshot = defineTool({
5656
response.setHeapSnapshotStats(stats, staticData);
5757
},
5858
});
59+
60+
export const getMemorySnapshotDetails = defineTool({
61+
name: 'get_memory_snapshot_details',
62+
description:
63+
'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates.',
64+
annotations: {
65+
category: ToolCategory.MEMORY,
66+
readOnlyHint: true,
67+
conditions: ['experimentalMemory'],
68+
},
69+
schema: {
70+
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
71+
pageIdx: zod
72+
.number()
73+
.optional()
74+
.describe('The page index for pagination of aggregates.'),
75+
pageSize: zod
76+
.number()
77+
.optional()
78+
.describe('The page size for pagination of aggregates.'),
79+
},
80+
handler: async (request, response, context) => {
81+
const aggregates = await context.getHeapSnapshotAggregates(
82+
request.params.filePath,
83+
);
84+
85+
response.setHeapSnapshotAggregates(aggregates, {
86+
pageIdx: request.params.pageIdx,
87+
pageSize: request.params.pageSize,
88+
});
89+
},
90+
});

0 commit comments

Comments
 (0)