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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,12 +514,13 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`take_snapshot`](docs/tool-reference.md#take_snapshot)
- [`screencast_start`](docs/tool-reference.md#screencast_start)
- [`screencast_stop`](docs/tool-reference.md#screencast_stop)
- **Memory** (6 tools)
- **Memory** (7 tools)
- [`take_heapsnapshot`](docs/tool-reference.md#take_heapsnapshot)
- [`close_heapsnapshot`](docs/tool-reference.md#close_heapsnapshot)
- [`get_heapsnapshot_class_nodes`](docs/tool-reference.md#get_heapsnapshot_class_nodes)
- [`get_heapsnapshot_details`](docs/tool-reference.md#get_heapsnapshot_details)
- [`get_heapsnapshot_retainers`](docs/tool-reference.md#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](docs/tool-reference.md#get_heapsnapshot_retaining_paths)
- [`get_heapsnapshot_summary`](docs/tool-reference.md#get_heapsnapshot_summary)
- **Extensions** (5 tools)
- [`install_extension`](docs/tool-reference.md#install_extension)
Expand Down
17 changes: 16 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@
- [`take_snapshot`](#take_snapshot)
- [`screencast_start`](#screencast_start)
- [`screencast_stop`](#screencast_stop)
- **[Memory](#memory)** (6 tools)
- **[Memory](#memory)** (7 tools)
- [`take_heapsnapshot`](#take_heapsnapshot)
- [`close_heapsnapshot`](#close_heapsnapshot)
- [`get_heapsnapshot_class_nodes`](#get_heapsnapshot_class_nodes)
- [`get_heapsnapshot_details`](#get_heapsnapshot_details)
- [`get_heapsnapshot_retainers`](#get_heapsnapshot_retainers)
- [`get_heapsnapshot_retaining_paths`](#get_heapsnapshot_retaining_paths)
- [`get_heapsnapshot_summary`](#get_heapsnapshot_summary)
- **[Extensions](#extensions)** (5 tools)
- [`install_extension`](#install_extension)
Expand Down Expand Up @@ -503,6 +504,20 @@ in the DevTools Elements panel (if any).

---

### `get_heapsnapshot_retaining_paths`

**Description:** Loads a memory heapsnapshot and returns retaining paths for a specific node ID. This helps to understand why a node is not being garbage collected. (requires flag: --memoryDebugging=true)

**Parameters:**

- **filePath** (string) **(required)**: A path to a .heapsnapshot file to read.
- **nodeId** (number) **(required)**: The node ID to get retaining paths for.
- **maxDepth** (number) _(optional)_: The maximum depth to search for retaining paths.
- **maxNodes** (number) _(optional)_: The maximum number of nodes to return.
- **maxSiblings** (number) _(optional)_: The maximum number of siblings to return.

---

### `get_heapsnapshot_summary`

**Description:** Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --memoryDebugging=true)
Expand Down
20 changes: 20 additions & 0 deletions src/HeapSnapshotManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,26 @@ export class HeapSnapshotManager {
return await provider.serializeItemsRange(0, Infinity);
}

async getRetainingPaths(
filePath: string,
nodeId: number,
maxDepth?: number,
maxNodes?: number,
maxSiblings?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths> {
const nodeIndex = await this.findNodeIndexById(filePath, nodeId);
if (nodeIndex === undefined) {
throw new Error(`Node with ID ${nodeId} not found`);
}
const snapshot = await this.getSnapshot(filePath);
return await snapshot.getRetainingPaths(
nodeIndex,
maxDepth,
maxNodes,
maxSiblings,
);
}

#getCachedSnapshot(filePath: string) {
const absolutePath = path.resolve(filePath);
const cached = this.#snapshots.get(absolutePath);
Expand Down
16 changes: 16 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -906,4 +906,20 @@ export class McpContext implements Context {
hasHeapSnapshots(): boolean {
return this.#heapSnapshotManager.hasSnapshots();
}

async getHeapSnapshotRetainingPaths(
filePath: string,
nodeId: number,
maxDepth?: number,
maxNodes?: number,
maxSiblings?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths> {
return await this.#heapSnapshotManager.getRetainingPaths(
filePath,
nodeId,
maxDepth,
maxNodes,
maxSiblings,
);
}
}
32 changes: 32 additions & 0 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export class McpResponse implements Response {
stats?: DevTools.HeapSnapshotModel.HeapSnapshotModel.Statistics;
staticData?: DevTools.HeapSnapshotModel.HeapSnapshotModel.StaticData | null;
nodes?: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange;
retainingPaths?: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths;
};
#networkRequestsOptions?: {
include: boolean;
Expand Down Expand Up @@ -472,6 +473,16 @@ export class McpResponse implements Response {
};
}

setHeapSnapshotRetainingPaths(
retainingPaths: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths,
) {
this.#heapSnapshotOptions = {
...this.#heapSnapshotOptions,
include: true,
retainingPaths,
};
}

attachImage(value: ImageContentData): void {
this.#images.push(value);
}
Expand Down Expand Up @@ -780,6 +791,7 @@ export class McpResponse implements Response {
};
heapSnapshotData?: object[];
heapSnapshotNodes?: readonly object[];
heapSnapshotRetainingPaths?: object;
extensionServiceWorkers?: object[];
extensionPages?: object[];
errorMessage?: string;
Expand Down Expand Up @@ -1056,6 +1068,26 @@ Call ${handleDialog.name} to handle it before continuing.`);

structuredContent.heapSnapshotNodes = paginationData.items;
}
const retainingPaths = this.#heapSnapshotOptions.retainingPaths;
if (retainingPaths) {
response.push('### Retaining Paths');
Comment thread
Lightning00Blade marked this conversation as resolved.
const {paths, limitsReached} = retainingPaths;
if (paths.length === 0) {
response.push('No retaining paths found.');
} else {
response.push(HeapSnapshotFormatter.formatRetainingPaths(paths));
}
const reached = Object.entries(limitsReached)
.filter(([, hit]) => hit)
.map(([limit]) => limit);
if (reached.length > 0) {
response.push(
`Note: results are truncated, the following limits were reached: ${reached.join(', ')}.`,
);
}
structuredContent.heapSnapshotRetainingPaths =
retainingPaths as unknown as object;
}
}

if (data.detailedNetworkRequest) {
Expand Down
37 changes: 37 additions & 0 deletions src/bin/chrome-devtools-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,43 @@ export const commands: Commands = {
},
},
},
get_heapsnapshot_retaining_paths: {
description:
'Loads a memory heapsnapshot and returns retaining paths for a specific node ID. This helps to understand why a node is not being garbage collected. (requires flag: --memoryDebugging=true)',
category: 'Memory',
args: {
filePath: {
name: 'filePath',
type: 'string',
description: 'A path to a .heapsnapshot file to read.',
required: true,
},
nodeId: {
name: 'nodeId',
type: 'number',
description: 'The node ID to get retaining paths for.',
required: true,
},
maxDepth: {
name: 'maxDepth',
type: 'number',
description: 'The maximum depth to search for retaining paths.',
required: false,
},
maxNodes: {
name: 'maxNodes',
type: 'number',
description: 'The maximum number of nodes to return.',
required: false,
},
maxSiblings: {
name: 'maxSiblings',
type: 'number',
description: 'The maximum number of siblings to return.',
required: false,
},
},
},
get_heapsnapshot_summary: {
description:
'Loads a memory heapsnapshot and returns snapshot summary stats. (requires flag: --memoryDebugging=true)',
Expand Down
25 changes: 25 additions & 0 deletions src/formatters/HeapSnapshotFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ export class HeapSnapshotFormatter {
return lines.join('\n');
}

static formatRetainingPaths(
retainingPaths: readonly DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingEdge[],
): string {
const lines: string[] = [];

function formatEdge(
edge: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingEdge,
depth: number,
) {
const indent = ' '.repeat(depth);
lines.push(
`${indent}<- @${edge.nodeId} ${edge.nodeName} via ${edge.edgeType} ${edge.edgeName} (distance: ${edge.distance})`,
);
for (const child of edge.children) {
formatEdge(child, depth + 1);
}
}

for (const path of retainingPaths) {
formatEdge(path, 0);
}

return lines.join('\n');
}

#getSortedAggregates(): AggregatedInfoWithId[] {
return Object.values(this.#aggregates).sort((a, b) => b.maxRet - a.maxRet);
}
Expand Down
25 changes: 25 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -747,5 +747,30 @@
"argType": "number"
}
]
},
{
"name": "get_heapsnapshot_retaining_paths",
"args": [
{
"name": "file_path_length",
"argType": "number"
},
{
"name": "node_id",
"argType": "number"
},
{
"name": "max_depth",
"argType": "number"
},
{
"name": "max_nodes",
"argType": "number"
},
{
"name": "max_siblings",
"argType": "number"
}
]
}
]
10 changes: 10 additions & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ export interface Response {
nodes: DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange,
options?: PaginationOptions,
): void;
setHeapSnapshotRetainingPaths(
retainingPaths: DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths,
): void;
setIncludePages(value: boolean): void;
setIncludeNetworkRequests(
value: boolean,
Expand Down Expand Up @@ -249,6 +252,13 @@ export type Context = Readonly<{
nodeId: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.ItemsRange>;
closeHeapSnapshot(filePath: string): Promise<boolean>;
getHeapSnapshotRetainingPaths(
filePath: string,
nodeId: number,
maxDepth?: number,
maxNodes?: number,
maxSiblings?: number,
): Promise<DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingPaths>;
}>;

/**
Expand Down
40 changes: 40 additions & 0 deletions src/tools/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,43 @@ export const closeHeapSnapshot = defineTool({
);
},
});

export const getHeapSnapshotRetainingPaths = defineTool({
name: 'get_heapsnapshot_retaining_paths',
description:
'Loads a memory heapsnapshot and returns retaining paths for a specific node ID. This helps to understand why a node is not being garbage collected.',
annotations: {
category: ToolCategory.MEMORY,
readOnlyHint: true,
conditions: ['memoryDebugging'],
},
verifyFilesSchema: ['filePath'],
blockedByDialog: false,
schema: {
filePath: zod.string().describe('A path to a .heapsnapshot file to read.'),
nodeId: zod.number().describe('The node ID to get retaining paths for.'),
maxDepth: zod
.number()
.optional()
.describe('The maximum depth to search for retaining paths.'),
maxNodes: zod
.number()
.optional()
.describe('The maximum number of nodes to return.'),
maxSiblings: zod
.number()
.optional()
.describe('The maximum number of siblings to return.'),
},
handler: async (request, response, context) => {
const retainingPaths = await context.getHeapSnapshotRetainingPaths(
request.params.filePath,
request.params.nodeId,
request.params.maxDepth,
request.params.maxNodes,
request.params.maxSiblings,
);

response.setHeapSnapshotRetainingPaths(retainingPaths);
},
});
37 changes: 37 additions & 0 deletions tests/formatters/HeapSnapshotFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,41 @@ describe('HeapSnapshotFormatter', () => {
assert.strictEqual(result[1][0], 'ObjectB');
});
});

describe('formatRetainingPaths', () => {
it('formats retaining paths correctly', () => {
const mockRetainingPaths = [
{
edgeIndex: 0,
edgeName: 'foo',
edgeType: 'property',
nodeId: 10,
nodeIndex: 1,
nodeName: 'ClassA',
distance: 2,
children: [
{
edgeIndex: 0,
edgeName: 'bar',
edgeType: 'element',
nodeId: 20,
nodeIndex: 2,
nodeName: 'ClassB',
distance: 1,
children: [],
},
],
},
] as unknown as DevTools.HeapSnapshotModel.HeapSnapshotModel.RetainingEdge[];

const result =
HeapSnapshotFormatter.formatRetainingPaths(mockRetainingPaths);
const expected = [
'<- @10 ClassA via property foo (distance: 2)',
' <- @20 ClassB via element bar (distance: 1)',
].join('\n');

assert.strictEqual(result, expected);
});
});
});
Loading
Loading