Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,10 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- **Emulation** (2 tools)
- [`emulate`](docs/tool-reference.md#emulate)
- [`resize_page`](docs/tool-reference.md#resize_page)
- **Performance** (4 tools)
- **Performance** (3 tools)
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
- [`performance_start_trace`](docs/tool-reference.md#performance_start_trace)
- [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace)
- [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)
- **Network** (2 tools)
- [`get_network_request`](docs/tool-reference.md#get_network_request)
- [`list_network_requests`](docs/tool-reference.md#list_network_requests)
Expand All @@ -512,6 +511,8 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`list_console_messages`](docs/tool-reference.md#list_console_messages)
- [`take_screenshot`](docs/tool-reference.md#take_screenshot)
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
- **Memory** (1 tools)
- [`take_memory_snapshot`](docs/tool-reference.md#take_memory_snapshot)

<!-- END AUTO GENERATED TOOLS -->

Expand Down
27 changes: 15 additions & 12 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@
- **[Emulation](#emulation)** (2 tools)
- [`emulate`](#emulate)
- [`resize_page`](#resize_page)
- **[Performance](#performance)** (4 tools)
- **[Performance](#performance)** (3 tools)
- [`performance_analyze_insight`](#performance_analyze_insight)
- [`performance_start_trace`](#performance_start_trace)
- [`performance_stop_trace`](#performance_stop_trace)
- [`take_memory_snapshot`](#take_memory_snapshot)
- **[Network](#network)** (2 tools)
- [`get_network_request`](#get_network_request)
- [`list_network_requests`](#list_network_requests)
Expand All @@ -37,6 +36,8 @@
- [`list_console_messages`](#list_console_messages)
- [`take_screenshot`](#take_screenshot)
- [`take_snapshot`](#take_snapshot)
- **[Memory](#memory)** (1 tools)
- [`take_memory_snapshot`](#take_memory_snapshot)

## Input automation

Expand Down Expand Up @@ -276,16 +277,6 @@

---

### `take_memory_snapshot`

**Description:** Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.

**Parameters:**

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to save the heapsnapshot to.

---

## Network

### `get_network_request`
Expand Down Expand Up @@ -397,3 +388,15 @@ in the DevTools Elements panel (if any).
- **verbose** (boolean) _(optional)_: Whether to include all possible information available in the full a11y tree. Default is false.

---

## Memory

### `take_memory_snapshot`

**Description:** Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.

**Parameters:**

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to save the heapsnapshot to.

---
60 changes: 55 additions & 5 deletions src/HeapSnapshotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,25 @@ import fsSync from 'node:fs';
import path from 'node:path';

import {DevTools} from './third_party/index.js';
import {
createIdGenerator,
stableIdSymbol,
type WithSymbolId,
} from './utils/id.js';

export type AggregatedInfoWithUid =
WithSymbolId<DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>;

export class HeapSnapshotManager {
#snapshots = new Map<
string,
{
snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy;
worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy;
// TODO: use a multimap
uidToClassKey: Map<number, string>;
classKeyToUid: Map<string, number>;
idGenerator: () => number;
}
>();

Expand All @@ -28,20 +40,35 @@ export class HeapSnapshotManager {
}

const {snapshot, worker} = await this.#loadSnapshot(absolutePath);
this.#snapshots.set(absolutePath, {snapshot, worker});
this.#snapshots.set(absolutePath, {
snapshot,
worker,
uidToClassKey: new Map<number, string>(),
classKeyToUid: new Map<string, number>(),
Comment thread
Lightning00Blade marked this conversation as resolved.
idGenerator: createIdGenerator(),
});

return snapshot;
}

async getAggregates(
filePath: string,
): Promise<
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
> {
): Promise<Record<string, AggregatedInfoWithUid>> {
const snapshot = await this.getSnapshot(filePath);
const filter =
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
return await snapshot.aggregatesWithFilter(filter);
const aggregates: Record<string, AggregatedInfoWithUid> =
await snapshot.aggregatesWithFilter(filter);

for (const key of Object.keys(aggregates)) {
const uid = await this.getOrCreateUidForClassKey(filePath, key);
const aggregate = aggregates[key];
if (aggregate) {
aggregate[stableIdSymbol] = uid;
}
}

return aggregates;
}

async getStats(
Expand All @@ -58,6 +85,29 @@ export class HeapSnapshotManager {
return snapshot.staticData;
}

async getOrCreateUidForClassKey(
filePath: string,
classKey: string,
): Promise<number> {
const cached = this.#getCachedSnapshot(filePath);
let uid = cached.classKeyToUid.get(classKey);
if (!uid) {
uid = cached.idGenerator();
cached.classKeyToUid.set(classKey, uid);
cached.uidToClassKey.set(uid, classKey);
}
return uid;
}

#getCachedSnapshot(filePath: string) {
const absolutePath = path.resolve(filePath);
const cached = this.#snapshots.get(absolutePath);
if (!cached) {
throw new Error(`Snapshot not loaded for ${filePath}`);
}
return cached;
}

async #loadSnapshot(absolutePath: string): Promise<{
snapshot: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy;
worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy;
Expand Down
5 changes: 2 additions & 3 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path from 'node:path';
import type {TargetUniverse} from './DevtoolsUtils.js';
import {UniverseManager} from './DevtoolsUtils.js';
import {HeapSnapshotManager} from './HeapSnapshotManager.js';
import type {AggregatedInfoWithUid} from './HeapSnapshotManager.js';
import {McpPage} from './McpPage.js';
import {
NetworkCollector,
Expand Down Expand Up @@ -918,9 +919,7 @@ export class McpContext implements Context {

async getHeapSnapshotAggregates(
filePath: string,
): Promise<
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
> {
): Promise<Record<string, AggregatedInfoWithUid>> {
return await this.#heapSnapshotManager.getAggregates(filePath);
}

Expand Down
20 changes: 5 additions & 15 deletions src/PageCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import {
type Page,
type PageEvents as PuppeteerPageEvents,
} from './third_party/index.js';
import {
createIdGenerator,
stableIdSymbol,
type WithSymbolId,
} from './utils/id.js';

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

function createIdGenerator() {
let i = 1;
return () => {
if (i === Number.MAX_SAFE_INTEGER) {
i = 0;
}
return i++;
};
}

export const stableIdSymbol = Symbol('stableIdSymbol');
type WithSymbolId<T> = T & {
[stableIdSymbol]?: number;
};

export class PageCollector<T> {
#browser: Browser;
#listenersInitializer: (
Expand Down
2 changes: 1 addition & 1 deletion src/bin/cliDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ export const commands: Commands = {
take_memory_snapshot: {
description:
'Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.',
category: 'Performance',
category: 'Memory',
args: {
filePath: {
name: 'filePath',
Expand Down
25 changes: 12 additions & 13 deletions src/formatters/HeapSnapshotFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,39 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type {AggregatedInfoWithUid} from '../HeapSnapshotManager.js';
import type {DevTools} from '../third_party/index.js';
import {stableIdSymbol} from '../utils/id.js';

export interface FormattedSnapshotEntry {
className: string;
classUid?: number;
count: number;
selfSize: number;
retainedSize: number;
}

export class HeapSnapshotFormatter {
#aggregates: Record<
string,
DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo
>;
#aggregates: Record<string, AggregatedInfoWithUid>;

constructor(
aggregates: Record<
string,
DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo
>,
) {
constructor(aggregates: Record<string, AggregatedInfoWithUid>) {
this.#aggregates = aggregates;
}

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

toString(): string {
const sorted = this.#getSortedAggregates();
const lines: string[] = [];
lines.push('className,count,selfSize,maxRetainedSize');
lines.push('uid,className,count,selfSize,maxRetainedSize');

for (const info of sorted) {
lines.push(`"${info.name}",${info.count},${info.self},${info.maxRet}`);
const uid = info[stableIdSymbol] ?? '';
lines.push(
`${uid},"${info.name}",${info.count},${info.self},${info.maxRet}`,
);
}

return lines.join('\n');
Expand All @@ -47,6 +45,7 @@ export class HeapSnapshotFormatter {
toJSON(): FormattedSnapshotEntry[] {
const sorted = this.#getSortedAggregates();
return sorted.map(info => ({
uid: info[stableIdSymbol],
className: info.name,
count: info.count,
selfSize: info.self,
Expand Down
17 changes: 17 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -565,5 +565,22 @@
"argType": "number"
}
]
},
{
"name": "get_memory_snapshot_details",
"args": [
{
"name": "file_path_length",
"argType": "number"
},
{
"name": "page_idx",
"argType": "number"
},
{
"name": "page_size",
"argType": "number"
}
]
}
]
5 changes: 2 additions & 3 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import type {ParsedArguments} from '../bin/chrome-devtools-mcp-cli-options.js';
import type {AggregatedInfoWithUid} from '../HeapSnapshotManager.js';
import type {McpPage} from '../McpPage.js';
import {zod} from '../third_party/index.js';
import type {
Expand Down Expand Up @@ -227,9 +228,7 @@ export type Context = Readonly<{
): string | undefined;
getHeapSnapshotAggregates(
filePath: string,
): Promise<
Record<string, DevTools.HeapSnapshotModel.HeapSnapshotModel.AggregatedInfo>
>;
): Promise<Record<string, AggregatedInfoWithUid>>;
getHeapSnapshotStats(
filePath: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics>;
Expand Down
36 changes: 34 additions & 2 deletions src/tools/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const takeMemorySnapshot = definePageTool({
name: 'take_memory_snapshot',
description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
annotations: {
category: ToolCategory.PERFORMANCE,
category: ToolCategory.MEMORY,
readOnlyHint: false,
},
schema: {
Expand All @@ -38,7 +38,7 @@ export const takeMemorySnapshot = definePageTool({
export const exploreMemorySnapshot = defineTool({
name: 'load_memory_snapshot',
description:
'Loads a memory heapsnapshot and returns snapshot summary stats. ',
'Loads a memory heapsnapshot and returns snapshot summary stats.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
Expand All @@ -56,3 +56,35 @@ export const exploreMemorySnapshot = defineTool({
response.setHeapSnapshotStats(stats, staticData);
},
});

export const getMemorySnapshotDetails = defineTool({
name: 'get_memory_snapshot_details',
description:
'Loads a memory heapsnapshot and returns all available information including statistics, static data, and aggregated node information. Supports pagination for aggregates.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
conditions: ['experimentalMemory'],
},
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
pageIdx: zod
.number()
.optional()
.describe('The page index for pagination of aggregates.'),
pageSize: zod
.number()
.optional()
.describe('The page size for pagination of aggregates.'),
},
handler: async (request, response, context) => {
const aggregates = await context.getHeapSnapshotAggregates(
request.params.filePath,
);

response.setHeapSnapshotAggregates(aggregates, {
pageIdx: request.params.pageIdx,
pageSize: request.params.pageSize,
});
},
});
Loading
Loading