Skip to content

Commit 726fd35

Browse files
chore: use id generation for memory aggragates
1 parent 0331f6a commit 726fd35

8 files changed

Lines changed: 76 additions & 42 deletions

File tree

src/HeapSnapshotManager.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,24 @@ 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+
uidToClassKey: Map<number, string>;
27+
classKeyToUid: Map<string, number>;
28+
idGenerator: () => number;
1829
}
1930
>();
2031

@@ -28,20 +39,35 @@ export class HeapSnapshotManager {
2839
}
2940

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

3350
return snapshot;
3451
}
3552

3653
async getAggregates(
3754
filePath: string,
38-
): Promise<
39-
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
40-
> {
55+
): Promise<Record<string, AggregatedInfoWithUid>> {
4156
const snapshot = await this.getSnapshot(filePath);
4257
const filter =
4358
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
44-
return await snapshot.aggregatesWithFilter(filter);
59+
const aggregates: Record<string, AggregatedInfoWithUid> =
60+
await snapshot.aggregatesWithFilter(filter);
61+
62+
for (const key of Object.keys(aggregates)) {
63+
const uid = await this.getOrCreateUidForClassKey(filePath, key);
64+
const aggregate = aggregates[key];
65+
if (aggregate) {
66+
aggregate[stableIdSymbol] = uid;
67+
}
68+
}
69+
70+
return aggregates;
4571
}
4672

4773
async getStats(

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,
@@ -918,9 +919,7 @@ export class McpContext implements Context {
918919

919920
async getHeapSnapshotAggregates(
920921
filePath: string,
921-
): Promise<
922-
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
923-
> {
922+
): Promise<Record<string, AggregatedInfoWithUid>> {
924923
return await this.#heapSnapshotManager.getAggregates(filePath);
925924
}
926925

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/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/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: 2 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,

src/utils/id.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export function createIdGenerator() {
8+
let i = 1;
9+
return () => {
10+
if (i === Number.MAX_SAFE_INTEGER) {
11+
i = 0;
12+
}
13+
return i++;
14+
};
15+
}
16+
17+
export const stableIdSymbol = Symbol('stableIdSymbol');
18+
19+
export type WithSymbolId<T> = T & {
20+
[stableIdSymbol]?: number;
21+
};

tests/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import sinon from 'sinon';
2424
import type {ParsedArguments} from '../src/bin/chrome-devtools-mcp-cli-options.js';
2525
import {McpContext} from '../src/McpContext.js';
2626
import {McpResponse} from '../src/McpResponse.js';
27-
import {stableIdSymbol} from '../src/PageCollector.js';
2827
import {DevTools} from '../src/third_party/index.js';
28+
import {stableIdSymbol} from '../src/utils/id.js';
2929

3030
export function getTextContent(
3131
content: CallToolResult['content'][number],

0 commit comments

Comments
 (0)