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
2 changes: 2 additions & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ in the DevTools Elements panel (if any).

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **id** (number) **(required)**: The ID for the class, obtained from details.
- **filterName** (enum: "objectsRetainedByDetachedDomNodes", "objectsRetainedByConsole", "objectsRetainedByEventHandlers", "objectsRetainedByContexts") _(optional)_: An optional filter to apply to the nodes.
- **pageIdx** (number) _(optional)_: The page index for pagination.
- **pageSize** (number) _(optional)_: The page size for pagination.

Expand All @@ -496,6 +497,7 @@ in the DevTools Elements panel (if any).
**Parameters:**

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **filterName** (enum: "objectsRetainedByDetachedDomNodes", "objectsRetainedByConsole", "objectsRetainedByEventHandlers", "objectsRetainedByContexts") _(optional)_: An optional filter to apply to the aggregates.
- **pageIdx** (number) _(optional)_: The page index for pagination of aggregates.
- **pageSize** (number) _(optional)_: The page size for pagination of aggregates.

Expand Down
8 changes: 8 additions & 0 deletions src/HeapSnapshotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ export class HeapSnapshotManager {

async getAggregates(
filePath: string,
filterName?: string,
): Promise<Record<string, AggregatedInfoWithId>> {
const snapshot = await this.getSnapshot(filePath);
const filter =
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
if (filterName) {
filter.filterName = filterName;
}
const aggregates: Record<string, AggregatedInfoWithId> =
await snapshot.aggregatesWithFilter(filter);

Expand Down Expand Up @@ -122,10 +126,14 @@ export class HeapSnapshotManager {
async getNodesById(
filePath: string,
id: number,
filterName?: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
const snapshot = await this.getSnapshot(filePath);
const filter =
new DevTools.HeapSnapshotModel.HeapSnapshotModel.NodeFilter();
if (filterName) {
filter.filterName = filterName;
}
const className = await this.resolveClassKeyFromId(filePath, id);
if (!className) {
throw new Error(`Class with ID ${id} not found in heap snapshot`);
Expand Down
10 changes: 8 additions & 2 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,8 +952,9 @@ export class McpContext implements Context {

async getHeapSnapshotAggregates(
filePath: string,
filterName?: string,
): Promise<Record<string, AggregatedInfoWithId>> {
return await this.#heapSnapshotManager.getAggregates(filePath);
return await this.#heapSnapshotManager.getAggregates(filePath, filterName);
}

async getHeapSnapshotDuplicateStrings(
Expand All @@ -977,8 +978,13 @@ export class McpContext implements Context {
async getHeapSnapshotNodesById(
filePath: string,
id: number,
filterName?: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange> {
return await this.#heapSnapshotManager.getNodesById(filePath, id);
return await this.#heapSnapshotManager.getNodesById(
filePath,
id,
filterName,
);
}

async getHeapSnapshotRetainers(
Expand Down
24 changes: 24 additions & 0 deletions src/bin/chrome-devtools-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,18 @@ export const commands: Commands = {
description: 'The ID for the class, obtained from details.',
required: true,
},
filterName: {
name: 'filterName',
type: 'string',
description: 'An optional filter to apply to the nodes.',
required: false,
enum: [
'objectsRetainedByDetachedDomNodes',
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
],
},
pageIdx: {
name: 'pageIdx',
type: 'number',
Expand All @@ -374,6 +386,18 @@ export const commands: Commands = {
description: 'A path to a .heapsnapshot file to read.',
required: true,
},
filterName: {
name: 'filterName',
type: 'string',
description: 'An optional filter to apply to the aggregates.',
required: false,
enum: [
'objectsRetainedByDetachedDomNodes',
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
],
},
pageIdx: {
name: 'pageIdx',
type: 'number',
Expand Down
8 changes: 8 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,10 @@
{
"name": "id",
"argType": "number"
},
{
"name": "filter_name",
"argType": "string"
}
]
},
Expand All @@ -701,6 +705,10 @@
{
"name": "page_size",
"argType": "number"
},
{
"name": "filter_name",
"argType": "string"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export type Context = Readonly<{
): string | undefined;
getHeapSnapshotAggregates(
filePath: string,
filterName?: string,
): Promise<Record<string, AggregatedInfoWithId>>;
getHeapSnapshotDuplicateStrings(
filePath: string,
Expand All @@ -270,6 +271,7 @@ export type Context = Readonly<{
getHeapSnapshotNodesById(
filePath: string,
id: number,
filterName?: string,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
getHeapSnapshotRetainers(
filePath: string,
Expand Down
17 changes: 17 additions & 0 deletions src/tools/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import {zod} from '../third_party/index.js';
import {ToolCategory} from './categories.js';
import {definePageTool, defineTool} from './ToolDefinition.js';

const HEAP_SNAPSHOT_FILTERS: readonly [string, ...string[]] = [
'objectsRetainedByDetachedDomNodes',
'objectsRetainedByConsole',
'objectsRetainedByEventHandlers',
'objectsRetainedByContexts',
];

export const takeHeapSnapshot = definePageTool({
name: 'take_heapsnapshot',
description: `Capture a heap snapshot of the currently selected page. Use to analyze the memory distribution of JavaScript objects and debug memory leaks.`,
Expand Down Expand Up @@ -73,6 +80,10 @@ export const getHeapSnapshotDetails = defineTool({
},
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
filterName: zod
.enum(HEAP_SNAPSHOT_FILTERS)
.optional()
.describe('An optional filter to apply to the aggregates.'),
pageIdx: zod
.number()
.optional()
Expand All @@ -87,6 +98,7 @@ export const getHeapSnapshotDetails = defineTool({
handler: async (request, response, context) => {
const aggregates = await context.getHeapSnapshotAggregates(
request.params.filePath,
request.params.filterName,
);

response.setHeapSnapshotAggregates(aggregates, {
Expand All @@ -108,6 +120,10 @@ export const getHeapSnapshotClassNodes = defineTool({
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
id: zod.number().describe('The ID for the class, obtained from details.'),
filterName: zod
.enum(HEAP_SNAPSHOT_FILTERS)
.optional()
.describe('An optional filter to apply to the nodes.'),
pageIdx: zod.number().optional().describe('The page index for pagination.'),
pageSize: zod.number().optional().describe('The page size for pagination.'),
},
Expand All @@ -117,6 +133,7 @@ export const getHeapSnapshotClassNodes = defineTool({
const nodes = await context.getHeapSnapshotNodesById(
request.params.filePath,
request.params.id,
request.params.filterName,
);

response.setHeapSnapshotNodes(nodes, {
Expand Down
22 changes: 22 additions & 0 deletions tests/tools/memory.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ nodeId,nodeName,type,distance,selfSize,retainedSize
Showing 1-8 of 8 (Page 1 of 1).
`;

exports[`memory > get_heapsnapshot_class_nodes > with objectsRetainedByContexts filterName 1`] = `
## Heap Snapshot Data
nodeId,nodeName,type,distance,selfSize,retainedSize
45897,makeCallable,closure,5,0.0 kB,0.2 kB
46147,makeCallable,closure,5,0.0 kB,0.2 kB
45893,Apply,closure,5,0.0 kB,0.0 kB
45895,Save,closure,5,0.0 kB,0.0 kB
46143,Apply,closure,5,0.0 kB,0.0 kB
46145,Save,closure,5,0.0 kB,0.0 kB
Showing 1-6 of 6 (Page 1 of 1).
`;

exports[`memory > get_heapsnapshot_details > with default options 1`] = `
## Heap Snapshot Data
Showing 1-157 of 157 (Page 1 of 1).
Expand Down Expand Up @@ -212,6 +224,16 @@ id,name,count,selfSize,maxRetainedSize
108,HTMLBodyElement (internal cache) / https://example.com,1,0.0 kB,0.0 kB
`;

exports[`memory > get_heapsnapshot_details > with objectsRetainedByContexts filterName 1`] = `
## Heap Snapshot Data
Showing 1-4 of 4 (Page 1 of 1).
id,name,count,selfSize,maxRetainedSize
1,system / Context,116,2.5 kB,3.5 kB
3,Function,6,0.2 kB,0.7 kB
2,(compiled code),22,0.5 kB,0.5 kB
4,Array,4,0.3 kB,0.3 kB
`;

exports[`memory > get_heapsnapshot_dominators > with valid nodeId 1`] = `
## Heap Snapshot Data
### Dominator Chain
Expand Down
63 changes: 63 additions & 0 deletions tests/tools/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
compareHeapSnapshots,
getHeapSnapshotDuplicateStrings,
} from '../../src/tools/memory.js';
import {stableIdSymbol} from '../../src/utils/id.js';
import {withMcpContext} from '../utils.js';

describe('memory', () => {
Expand Down Expand Up @@ -104,6 +105,31 @@ describe('memory', () => {
t.assert.snapshot(output);
});
});

it('with objectsRetainedByContexts filterName', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);

await getHeapSnapshotDetails.handler(
{params: {filePath, filterName: 'objectsRetainedByContexts'}},
response,
context,
);

const responseData = await response.handle(
getHeapSnapshotDetails.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');

t.assert.snapshot(output);
});
});
});

describe('get_heapsnapshot_class_nodes', () => {
Expand Down Expand Up @@ -135,6 +161,43 @@ describe('memory', () => {
});
});

it('with objectsRetainedByContexts filterName', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);

const aggregates = await context.getHeapSnapshotAggregates(
filePath,
'objectsRetainedByContexts',
);
const aggregate = Object.values(aggregates).find(
a => a.name === 'Function',
);
assert.ok(aggregate);
const id = aggregate[stableIdSymbol];
assert.ok(id);

await getHeapSnapshotClassNodes.handler(
{params: {filePath, id, filterName: 'objectsRetainedByContexts'}},
response,
context,
);

const responseData = await response.handle(
getHeapSnapshotClassNodes.name,
context,
);

const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');

t.assert.snapshot(output);
});
});

it('with non-existent class name', async () => {
await withMcpContext(async (response, context) => {
const filePath = join(
Expand Down