项目文件夹

文件
Dominik Inführ 08c234ea4b feat: Add get_heapsnapshot_dominators MCP tool (#2215)
Adding the get_heapsnapshot_dominators MCP tool to show the dominators
for a given node. In combination with get_heapsnapshot_retaining_paths
this should help understand what keeps an object reachable and thus
alive.

Co-authored-by: Dominik Inführ <dinfuehr@chromium.org>
Co-authored-by: Nicholas Roscino <nroscino@google.com>
2026-06-16 12:07:41 +00:00

365 行
9.6 KiB
TypeScript

/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import assert from 'node:assert';
import {existsSync} from 'node:fs';
import {rm} from 'node:fs/promises';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {describe, it} from 'node:test';
import {
takeHeapSnapshot,
getHeapSnapshotSummary,
getHeapSnapshotDetails,
getHeapSnapshotClassNodes,
getHeapSnapshotRetainers,
closeHeapSnapshot,
getHeapSnapshotRetainingPaths,
getHeapSnapshotEdges,
getHeapSnapshotDominators,
} from '../../src/tools/memory.js';
import {withMcpContext} from '../utils.js';
describe('memory', () => {
describe('take_heapsnapshot', () => {
it('with default options', async () => {
await withMcpContext(async (response, context) => {
const filePath = join(tmpdir(), 'test-screenshot.heapsnapshot');
try {
await takeHeapSnapshot.handler(
{params: {filePath}, page: context.getSelectedMcpPage()},
response,
context,
);
assert.equal(
response.responseLines.at(0),
`Heap snapshot saved to ${filePath}`,
);
assert.ok(existsSync(filePath));
} finally {
await rm(filePath, {force: true});
}
});
});
});
describe('get_heapsnapshot_summary', () => {
it('with default options', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
assert.ok(existsSync(filePath), `Fixture not found at ${filePath}`);
await getHeapSnapshotSummary.handler(
{params: {filePath}},
response,
context,
);
// Call handle to trigger formatting (similar to network tests)
const responseData = await response.handle(
getHeapSnapshotSummary.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
describe('get_heapsnapshot_details', () => {
it('with default options', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotDetails.handler(
{params: {filePath}},
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', () => {
it('with default options', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await context.getHeapSnapshotAggregates(filePath);
await getHeapSnapshotClassNodes.handler(
{params: {filePath, id: 19}},
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(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await context.getHeapSnapshotAggregates(filePath);
await assert.rejects(
getHeapSnapshotClassNodes.handler(
{params: {filePath, id: 999999}},
response,
context,
),
{message: 'Class with ID 999999 not found in heap snapshot'},
);
});
});
});
describe('get_heapsnapshot_retainers', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotRetainers.handler(
{params: {filePath, nodeId: 25341}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotRetainers.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
describe('close_heapsnapshot', () => {
it('with default options', async () => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotSummary.handler(
{params: {filePath}},
response,
context,
);
assert.ok(context.hasHeapSnapshots());
await closeHeapSnapshot.handler(
{params: {filePath}},
response,
context,
);
assert.ok(
response.responseLines.includes(`Closed heap snapshot: ${filePath}`),
);
assert.ok(!context.hasHeapSnapshots());
});
});
it('with non-existent snapshot', async () => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await assert.rejects(
closeHeapSnapshot.handler({params: {filePath}}, response, context),
{
message: `Failed to close heap snapshot: ${filePath} was not loaded.`,
},
);
});
});
});
describe('get_heapsnapshot_retaining_paths', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotRetainingPaths.handler(
{params: {filePath, nodeId: 45901}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotRetainingPaths.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
it('reports when limits are reached', async () => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotRetainingPaths.handler(
{params: {filePath, nodeId: 45901, maxDepth: 1}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotRetainingPaths.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
assert.match(output, /No retaining paths found\./);
assert.match(
output,
/Note: results are truncated, the following limits were reached: depth\./,
);
});
});
});
describe('get_heapsnapshot_edges', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotEdges.handler(
{params: {filePath, nodeId: 25341}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotEdges.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
it('with pagination', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotEdges.handler(
{params: {filePath, nodeId: 25341, pageSize: 2}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotEdges.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
describe('get_heapsnapshot_dominators', () => {
it('with valid nodeId', async t => {
await withMcpContext(async (response, context) => {
const filePath = join(
process.cwd(),
'tests/fixtures/example.heapsnapshot',
);
await getHeapSnapshotDominators.handler(
{params: {filePath, nodeId: 25341}},
response,
context,
);
const responseData = await response.handle(
getHeapSnapshotDominators.name,
context,
);
const output = responseData.content
.map(c => (c.type === 'text' ? c.text : ''))
.join('\n');
t.assert.snapshot(output);
});
});
});
});