mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 05:02:31 +00:00
feat: Paginate detailed diff in compare_heapsnapshots (#3195)
This adds pagination support when compare_heapsnapshots is called with a given `classIndex`.
This commit is contained in:
1 parent
f73a440b49
commit
4a9dd53fe6
10 files changed
+173
-110
No files matched your search
+25
-10
@@ -474,12 +474,13 @@ export class McpResponse implements Response {
|
||||
|
||||
setHeapSnapshotDetailedClassDiff(
|
||||
detailedClassDiff: HeapSnapshotDetailedClassDiff,
|
||||
options?: HeapSnapshotFormatOptions,
|
||||
options?: PaginationOptions & HeapSnapshotFormatOptions,
|
||||
): void {
|
||||
this.#heapSnapshotOptions = {
|
||||
...this.#heapSnapshotOptions,
|
||||
include: true,
|
||||
detailedClassDiff,
|
||||
pagination: options,
|
||||
formatOptions: options,
|
||||
};
|
||||
}
|
||||
@@ -1487,21 +1488,35 @@ Call ${handleDialog(this.#args).name} to handle it before continuing.`);
|
||||
const detailedClassDiff = this.#heapSnapshotOptions.detailedClassDiff;
|
||||
if (detailedClassDiff) {
|
||||
response.push('### Heap Snapshot Detailed Diff');
|
||||
const {classDiff, objects} = detailedClassDiff;
|
||||
const paginationData = this.#dataWithPagination(
|
||||
objects,
|
||||
this.#heapSnapshotOptions.pagination,
|
||||
);
|
||||
structuredContent.pagination = paginationData.pagination;
|
||||
response.push(
|
||||
compactEncode
|
||||
? compactEncode({
|
||||
...detailedClassDiff,
|
||||
className: formatName(
|
||||
detailedClassDiff.className,
|
||||
formatOptions,
|
||||
),
|
||||
...classDiff,
|
||||
className: formatName(classDiff.className, formatOptions),
|
||||
})
|
||||
: HeapSnapshotFormatter.formatDiffDetails(
|
||||
detailedClassDiff,
|
||||
: HeapSnapshotFormatter.formatDetailedClassDiffHeader(
|
||||
classDiff,
|
||||
formatOptions,
|
||||
),
|
||||
);
|
||||
structuredContent.heapSnapshotDetailedClassDiff = detailedClassDiff;
|
||||
response.push(...paginationData.info);
|
||||
response.push(
|
||||
compactEncode
|
||||
? compactEncode(paginationData.items)
|
||||
: HeapSnapshotFormatter.formatDetailedClassDiffObjects(
|
||||
paginationData.items,
|
||||
),
|
||||
);
|
||||
structuredContent.heapSnapshotDetailedClassDiff = {
|
||||
classDiff,
|
||||
objects: paginationData.items,
|
||||
};
|
||||
}
|
||||
const duplicateStrings = this.#heapSnapshotOptions.duplicateStrings;
|
||||
if (duplicateStrings) {
|
||||
@@ -1789,7 +1804,7 @@ Call ${handleDialog(this.#args).name} to handle it before continuing.`);
|
||||
}
|
||||
|
||||
#dataWithPagination<T>(
|
||||
data: T[],
|
||||
data: readonly T[],
|
||||
pagination?: PaginationOptions,
|
||||
): {
|
||||
info: string[];
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
HeapSnapshotAggregate,
|
||||
HeapSnapshotClassDiff,
|
||||
HeapSnapshotClassDiffTotals,
|
||||
HeapSnapshotDetailedClassDiff,
|
||||
HeapSnapshotDiffObject,
|
||||
DuplicateStringGroup,
|
||||
} from '../processors/HeapSnapshotManager.js';
|
||||
import {DevTools} from '../third_party/index.js';
|
||||
@@ -286,36 +286,26 @@ export class HeapSnapshotFormatter {
|
||||
}
|
||||
|
||||
static formatDiffTotals(totals: HeapSnapshotClassDiffTotals): string {
|
||||
return `Total: # new: ${totals.addedCount}, # deleted: ${totals.removedCount}, # delta: ${formatSignedCount(totals.countDelta)}, alloc size: ${formatSignedSize(totals.addedSize)}, freed size: ${formatSignedSize(totals.removedSize)}, size delta: ${formatSignedSize(totals.sizeDelta)}`;
|
||||
return `Total: ${formatDiffCounts(totals)}`;
|
||||
}
|
||||
|
||||
static formatDiffDetails(
|
||||
diff: HeapSnapshotDetailedClassDiff,
|
||||
static formatDetailedClassDiffHeader(
|
||||
diff: HeapSnapshotClassDiff,
|
||||
options: HeapSnapshotFormatOptions,
|
||||
): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(
|
||||
`${formatName(diff.className, options)}: # new: ${diff.addedCount}, # deleted: ${diff.removedCount}, # delta: ${formatSignedCount(diff.countDelta)}, alloc size: ${formatSignedSize(diff.addedSize)}, freed size: ${formatSignedSize(diff.removedSize)}, size delta: ${formatSignedSize(diff.sizeDelta)}`,
|
||||
);
|
||||
return `${formatName(diff.className, options)}: ${formatDiffCounts(diff)}`;
|
||||
}
|
||||
|
||||
const addedIds = diff.addedIds;
|
||||
const addedSelfSizes = diff.addedSelfSizes;
|
||||
const deletedIds = diff.deletedIds;
|
||||
const deletedSelfSizes = diff.deletedSelfSizes;
|
||||
|
||||
lines.push(`Objects:`);
|
||||
|
||||
for (let i = 0; i < addedIds.length; i++) {
|
||||
static formatDetailedClassDiffObjects(
|
||||
objects: readonly HeapSnapshotDiffObject[],
|
||||
): string {
|
||||
const lines = ['Objects:'];
|
||||
for (const object of objects) {
|
||||
const sign = object.change === 'added' ? '+' : '-';
|
||||
lines.push(
|
||||
` + @${addedIds[i]} (self_size: ${formatBytesToKb(addedSelfSizes[i])})`,
|
||||
` ${sign} @${object.id} (self_size: ${formatBytesToKb(object.selfSize)})`,
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < deletedIds.length; i++) {
|
||||
lines.push(
|
||||
` - @${deletedIds[i]} (self_size: ${formatBytesToKb(deletedSelfSizes[i])})`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -416,3 +406,7 @@ function formatSignedSize(bytes: number): string {
|
||||
const formatted = formatBytesToKb(bytes);
|
||||
return bytes > 0 ? `+${formatted}` : formatted;
|
||||
}
|
||||
|
||||
function formatDiffCounts(counts: HeapSnapshotClassDiffTotals): string {
|
||||
return `# new: ${counts.addedCount}, # deleted: ${counts.removedCount}, # delta: ${formatSignedCount(counts.countDelta)}, alloc size: ${formatSignedSize(counts.addedSize)}, freed size: ${formatSignedSize(counts.removedSize)}, size delta: ${formatSignedSize(counts.sizeDelta)}`;
|
||||
}
|
||||
@@ -41,11 +41,15 @@ export interface HeapSnapshotClassDiff {
|
||||
sizeDelta: number;
|
||||
}
|
||||
|
||||
export interface HeapSnapshotDetailedClassDiff extends HeapSnapshotClassDiff {
|
||||
addedIds: number[];
|
||||
addedSelfSizes: number[];
|
||||
deletedIds: number[];
|
||||
deletedSelfSizes: number[];
|
||||
export interface HeapSnapshotDetailedClassDiff {
|
||||
classDiff: HeapSnapshotClassDiff;
|
||||
objects: readonly HeapSnapshotDiffObject[];
|
||||
}
|
||||
|
||||
export interface HeapSnapshotDiffObject {
|
||||
id: number;
|
||||
selfSize: number;
|
||||
change: 'added' | 'deleted';
|
||||
}
|
||||
|
||||
export interface HeapSnapshotClassDiffTotals {
|
||||
@@ -66,6 +70,22 @@ export type HeapQueryOptions =
|
||||
export type HeapEdgesQueryOptions =
|
||||
DevTools.HeapSnapshotModel.HeapSnapshotModel.HeapEdgesQueryOptions;
|
||||
|
||||
function toClassDiff(
|
||||
index: number,
|
||||
rawDiff: DevTools.HeapSnapshotModel.HeapSnapshotModel.Diff,
|
||||
): HeapSnapshotClassDiff {
|
||||
return {
|
||||
index,
|
||||
className: rawDiff.name,
|
||||
addedCount: rawDiff.addedCount,
|
||||
removedCount: rawDiff.removedCount,
|
||||
countDelta: rawDiff.countDelta,
|
||||
addedSize: rawDiff.addedSize,
|
||||
removedSize: rawDiff.removedSize,
|
||||
sizeDelta: rawDiff.sizeDelta,
|
||||
};
|
||||
}
|
||||
|
||||
export class HeapSnapshot {
|
||||
proxy: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotProxy;
|
||||
worker: DevTools.HeapSnapshotModel.HeapSnapshotProxy.HeapSnapshotWorkerProxy;
|
||||
@@ -334,16 +354,7 @@ export class HeapSnapshotManager {
|
||||
sizeDelta: 0,
|
||||
};
|
||||
for (const [index, rawDiff] of rawDiffs.entries()) {
|
||||
classDiffs.push({
|
||||
index,
|
||||
className: rawDiff.name,
|
||||
addedCount: rawDiff.addedCount,
|
||||
removedCount: rawDiff.removedCount,
|
||||
countDelta: rawDiff.countDelta,
|
||||
addedSize: rawDiff.addedSize,
|
||||
removedSize: rawDiff.removedSize,
|
||||
sizeDelta: rawDiff.sizeDelta,
|
||||
});
|
||||
classDiffs.push(toClassDiff(index, rawDiff));
|
||||
totals.addedCount += rawDiff.addedCount;
|
||||
totals.removedCount += rawDiff.removedCount;
|
||||
totals.countDelta += rawDiff.countDelta;
|
||||
@@ -369,20 +380,18 @@ export class HeapSnapshotManager {
|
||||
`Invalid classIndex: ${classIndex}. Total classes with changes: ${classDiffs.length}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
index: classIndex,
|
||||
className: rawDiff.name,
|
||||
addedCount: rawDiff.addedCount,
|
||||
removedCount: rawDiff.removedCount,
|
||||
countDelta: rawDiff.countDelta,
|
||||
addedSize: rawDiff.addedSize,
|
||||
removedSize: rawDiff.removedSize,
|
||||
sizeDelta: rawDiff.sizeDelta,
|
||||
addedIds: rawDiff.addedIds ?? [],
|
||||
addedSelfSizes: rawDiff.addedSelfSizes ?? [],
|
||||
deletedIds: rawDiff.deletedIds ?? [],
|
||||
deletedSelfSizes: rawDiff.deletedSelfSizes ?? [],
|
||||
};
|
||||
const objects: HeapSnapshotDiffObject[] = [];
|
||||
const addedIds = rawDiff.addedIds ?? [];
|
||||
const addedSelfSizes = rawDiff.addedSelfSizes ?? [];
|
||||
for (const [i, id] of addedIds.entries()) {
|
||||
objects.push({id, selfSize: addedSelfSizes[i], change: 'added'});
|
||||
}
|
||||
const deletedIds = rawDiff.deletedIds ?? [];
|
||||
const deletedSelfSizes = rawDiff.deletedSelfSizes ?? [];
|
||||
for (const [i, id] of deletedIds.entries()) {
|
||||
objects.push({id, selfSize: deletedSelfSizes[i], change: 'deleted'});
|
||||
}
|
||||
return {classDiff: toClassDiff(classIndex, rawDiff), objects};
|
||||
}
|
||||
|
||||
async #getSortedRawClassDiffs(
|
||||
|
||||
@@ -236,7 +236,7 @@ export interface Response {
|
||||
): void;
|
||||
setHeapSnapshotDetailedClassDiff(
|
||||
detailedClassDiff: HeapSnapshotDetailedClassDiff,
|
||||
options?: HeapSnapshotFormatOptions,
|
||||
options?: PaginationOptions & HeapSnapshotFormatOptions,
|
||||
): void;
|
||||
setHeapSnapshotObjectDetails(
|
||||
objectInfo: DevTools.HeapSnapshotModel.HeapSnapshotModel.ObjectInfo,
|
||||
|
||||
@@ -449,6 +449,8 @@ export const compareHeapSnapshots = defineBrowserlessTool(() => ({
|
||||
request.params.classIndex,
|
||||
);
|
||||
response.setHeapSnapshotDetailedClassDiff(classDiffResult, {
|
||||
pageIdx: request.params.pageIdx,
|
||||
pageSize: request.params.pageSize,
|
||||
maxNameLength: request.params.maxNameLength,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
createMockClassDiffTotals,
|
||||
createMockContextAnalysisResult,
|
||||
createMockDetailedClassDiff,
|
||||
createMockDiffObject,
|
||||
createMockHeapSnapshotDiff,
|
||||
createMockHeapSnapshotEdge,
|
||||
createMockHeapSnapshotNode,
|
||||
@@ -1724,6 +1725,34 @@ describe('McpResponse heap snapshot formatting', () => {
|
||||
assert.ok(detailedText.includes('TestClass: # new: 1, # deleted: 0'));
|
||||
});
|
||||
|
||||
it('paginates detailed class diff objects below the class header', async () => {
|
||||
const response = new McpResponse(createMockParsedArguments());
|
||||
const objects = [
|
||||
createMockDiffObject({id: 101, change: 'added'}),
|
||||
createMockDiffObject({id: 102, change: 'added'}),
|
||||
createMockDiffObject({id: 201, change: 'deleted'}),
|
||||
];
|
||||
const classDiff = createMockClassDiff({className: 'Leaky'});
|
||||
response.setHeapSnapshotDetailedClassDiff(
|
||||
createMockDetailedClassDiff({classDiff, objects}),
|
||||
{pageIdx: 1, pageSize: 2},
|
||||
);
|
||||
|
||||
const context = createMockMcpContext();
|
||||
const {content, structuredContent} = await response.handle(context);
|
||||
const text = getTextContent(content[0]);
|
||||
|
||||
assert.ok(text.includes('Leaky: # new: 1, # deleted: 0'));
|
||||
assert.ok(text.includes('Showing 3-3 of 3 (Page 2 of 2).'));
|
||||
assert.ok(text.includes(' - @201'));
|
||||
assert.ok(!text.includes('@101'));
|
||||
assert.ok(!text.includes('@102'));
|
||||
assert.deepStrictEqual(
|
||||
Reflect.get(structuredContent, 'heapSnapshotDetailedClassDiff'),
|
||||
{classDiff, objects: [objects[2]]},
|
||||
);
|
||||
});
|
||||
|
||||
it('paginates class diff summary and keeps global indices', async () => {
|
||||
const response = new McpResponse(createMockParsedArguments());
|
||||
const classDiffs = [
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
HeapSnapshotManager,
|
||||
type HeapSnapshotAggregate,
|
||||
type HeapSnapshotDiffObject,
|
||||
} from '../../src/processors/HeapSnapshotManager.js';
|
||||
import {DevTools} from '../../src/third_party/index.js';
|
||||
import {parseByteSizeRange} from '../../src/utils/bytes.js';
|
||||
@@ -211,8 +212,8 @@ describe('HeapSnapshotFormatter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDiffDetails', () => {
|
||||
it('formats detailed diffs correctly', () => {
|
||||
describe('formatDetailedClassDiffHeader and formatDetailedClassDiffObjects', () => {
|
||||
it('formats the class header and its objects', () => {
|
||||
const details = {
|
||||
index: 0,
|
||||
className: 'MyClass',
|
||||
@@ -222,25 +223,35 @@ describe('HeapSnapshotFormatter', () => {
|
||||
addedSize: 120,
|
||||
removedSize: 60,
|
||||
sizeDelta: 60,
|
||||
addedIds: [101, 102],
|
||||
addedSelfSizes: [60, 60],
|
||||
deletedIds: [201],
|
||||
deletedSelfSizes: [60],
|
||||
};
|
||||
const objects: HeapSnapshotDiffObject[] = [
|
||||
{id: 101, selfSize: 60, change: 'added'},
|
||||
{id: 102, selfSize: 60, change: 'added'},
|
||||
{id: 201, selfSize: 60, change: 'deleted'},
|
||||
];
|
||||
|
||||
const formatted = HeapSnapshotFormatter.formatDiffDetails(details, {});
|
||||
const header = HeapSnapshotFormatter.formatDetailedClassDiffHeader(
|
||||
details,
|
||||
{},
|
||||
);
|
||||
const formattedObjects =
|
||||
HeapSnapshotFormatter.formatDetailedClassDiffObjects(objects);
|
||||
const formatted120 = formatBytesToKb(120);
|
||||
const formatted60 = formatBytesToKb(60);
|
||||
|
||||
const expected = [
|
||||
assert.strictEqual(
|
||||
header,
|
||||
`MyClass: # new: 2, # deleted: 1, # delta: +1, alloc size: +${formatted120}, freed size: +${formatted60}, size delta: +${formatted60}`,
|
||||
'Objects:',
|
||||
` + @101 (self_size: ${formatted60})`,
|
||||
` + @102 (self_size: ${formatted60})`,
|
||||
` - @201 (self_size: ${formatted60})`,
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(formatted, expected);
|
||||
);
|
||||
assert.strictEqual(
|
||||
formattedObjects,
|
||||
[
|
||||
'Objects:',
|
||||
` + @101 (self_size: ${formatted60})`,
|
||||
` + @102 (self_size: ${formatted60})`,
|
||||
` - @201 (self_size: ${formatted60})`,
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('truncates className according to maxNameLength', () => {
|
||||
@@ -253,24 +264,20 @@ describe('HeapSnapshotFormatter', () => {
|
||||
addedSize: 60,
|
||||
removedSize: 0,
|
||||
sizeDelta: 60,
|
||||
addedIds: [101],
|
||||
addedSelfSizes: [60],
|
||||
deletedIds: [],
|
||||
deletedSelfSizes: [],
|
||||
};
|
||||
|
||||
const formatted = HeapSnapshotFormatter.formatDiffDetails(details, {
|
||||
maxNameLength: 4,
|
||||
});
|
||||
const header = HeapSnapshotFormatter.formatDetailedClassDiffHeader(
|
||||
details,
|
||||
{
|
||||
maxNameLength: 4,
|
||||
},
|
||||
);
|
||||
const formatted60 = formatBytesToKb(60);
|
||||
|
||||
const expected = [
|
||||
assert.strictEqual(
|
||||
header,
|
||||
`Long...: # new: 1, # deleted: 0, # delta: +1, alloc size: +${formatted60}, freed size: 0.0\u00A0kB, size delta: +${formatted60}`,
|
||||
'Objects:',
|
||||
` + @101 (self_size: ${formatted60})`,
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(formatted, expected);
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -662,13 +669,16 @@ describe('HeapSnapshotFormatter', () => {
|
||||
});
|
||||
|
||||
it('formats detailed diff comparing heap-1 to heap-2 with classIndex filter', async t => {
|
||||
const detailedDiff = await manager.getDetailedClassDiff(
|
||||
const {classDiff, objects} = await manager.getDetailedClassDiff(
|
||||
heap1Path,
|
||||
heap2Path,
|
||||
2,
|
||||
);
|
||||
t.assert.snapshot(
|
||||
HeapSnapshotFormatter.formatDiffDetails(detailedDiff, {}),
|
||||
[
|
||||
HeapSnapshotFormatter.formatDetailedClassDiffHeader(classDiff, {}),
|
||||
HeapSnapshotFormatter.formatDetailedClassDiffObjects(objects),
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+16
-13
@@ -43,6 +43,7 @@ import type {
|
||||
HeapSnapshotClassDiffTotals,
|
||||
HeapSnapshotDetailedClassDiff,
|
||||
HeapSnapshotDiff,
|
||||
HeapSnapshotDiffObject,
|
||||
} from '../src/processors/HeapSnapshotManager.js';
|
||||
import {
|
||||
CdpBrowser,
|
||||
@@ -1105,20 +1106,22 @@ export function createMockHeapSnapshotDiff(
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockDetailedClassDiff(): HeapSnapshotDetailedClassDiff {
|
||||
export function createMockDiffObject(
|
||||
options: Partial<HeapSnapshotDiffObject> = {},
|
||||
): HeapSnapshotDiffObject {
|
||||
return {
|
||||
index: 0,
|
||||
className: 'TestClass',
|
||||
addedCount: 1,
|
||||
removedCount: 0,
|
||||
countDelta: 1,
|
||||
addedSize: 10,
|
||||
removedSize: 0,
|
||||
sizeDelta: 10,
|
||||
addedIds: [1],
|
||||
addedSelfSizes: [10],
|
||||
deletedIds: [],
|
||||
deletedSelfSizes: [],
|
||||
id: options.id ?? 1,
|
||||
selfSize: options.selfSize ?? 10,
|
||||
change: options.change ?? 'added',
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockDetailedClassDiff(
|
||||
options: Partial<HeapSnapshotDetailedClassDiff> = {},
|
||||
): HeapSnapshotDetailedClassDiff {
|
||||
return {
|
||||
classDiff: options.classDiff ?? createMockClassDiff(),
|
||||
objects: options.objects ?? [createMockDiffObject()],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -71,13 +71,12 @@ describe('HeapSnapshotManager', () => {
|
||||
|
||||
const lastDiff = classDiffs.at(-1);
|
||||
assert.ok(lastDiff);
|
||||
const detailedDiff = await manager.getDetailedClassDiff(
|
||||
const {classDiff} = await manager.getDetailedClassDiff(
|
||||
filePathA,
|
||||
filePathB,
|
||||
lastDiff.index,
|
||||
);
|
||||
assert.strictEqual(detailedDiff.index, lastDiff.index);
|
||||
assert.strictEqual(detailedDiff.className, lastDiff.className);
|
||||
assert.deepStrictEqual(classDiff, lastDiff);
|
||||
});
|
||||
|
||||
// Verifies that the caching mechanism in HeapSnapshotManager correctly
|
||||
|
||||
@@ -644,6 +644,8 @@ describe('memory', () => {
|
||||
currentFilePath: 'heap2.heapsnapshot',
|
||||
classIndex: 2,
|
||||
maxNameLength: 500,
|
||||
pageIdx: 1,
|
||||
pageSize: 5,
|
||||
},
|
||||
},
|
||||
response,
|
||||
@@ -659,7 +661,7 @@ describe('memory', () => {
|
||||
sinon.assert.calledOnceWithExactly(
|
||||
response.setHeapSnapshotDetailedClassDiff,
|
||||
detailedDiff,
|
||||
{maxNameLength: 500},
|
||||
{pageIdx: 1, pageSize: 5, maxNameLength: 500},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user