Skip to content

Commit d3161c0

Browse files
committed
use size delta and correctly assume first snap
1 parent bbc2e9e commit d3161c0

File tree

8 files changed

+65
-48
lines changed

8 files changed

+65
-48
lines changed

internal/perf/src/heapsnapshot.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export interface HeapSnapshotDiff {
88
/**
99
* Diffs the provided v8 JavaScript {@link files heap snapshot files}
1010
* consecutively and filters the results to only include objects that have a positive
11-
* delta in both count and size in **every** snapshot, possibly indicating a leak.
11+
* size delta (grew in size) in **every** snapshot, possibly indicating a leak.
1212
*
1313
* Note that this is a heuristic and may not always indicate a leak, some objects may
1414
* legitimately grow in size or count over time.
@@ -28,6 +28,7 @@ export async function leakingObjectsInHeapSnapshotFiles(
2828
let baseSnap = await parseHeapSnapshot(
2929
createReadStream(snapshotFiles.shift()!),
3030
);
31+
let firstSnap = true;
3132
while (baseSnap) {
3233
const snapshotFile = snapshotFiles.shift()!;
3334
if (!snapshotFile) {
@@ -46,16 +47,15 @@ export async function leakingObjectsInHeapSnapshotFiles(
4647
)) {
4748
if (
4849
// size just kept growing
49-
diff.sizeDelta > 0 &&
50-
// count just kept growing
51-
diff.countDelta > 0
50+
diff.sizeDelta > 0
5251
) {
5352
growingDiff[diff.name] = diff;
5453
}
5554
}
5655

57-
if (!Object.keys(totalGrowingDiff).length) {
56+
if (firstSnap) {
5857
// this is the first snapshot, so we just take the diff as is
58+
firstSnap = false;
5959
Object.assign(totalGrowingDiff, growingDiff);
6060
continue;
6161
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

internal/perf/tests/heapsnapshot.test.ts

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,72 +10,89 @@ it.skipIf(
1010
// no need to test in bun (also, bun does not support increasing timeouts per test)
1111
globalThis.Bun,
1212
)(
13-
'should correctly calculate the leaking objects',
13+
'should correctly calculate no leaking objects',
1414
{
1515
// parsing snapshots can take a while, so we increase the timeout
1616
timeout: 30_000,
1717
},
1818
async () => {
19-
await using snap1 = await archivedFixtureFile(
19+
await using snaps = await archivedFixtureFiles([
2020
'http-server-under-load/1.heapsnapshot',
21-
);
22-
await using snap2 = await archivedFixtureFile(
2321
'http-server-under-load/2.heapsnapshot',
24-
);
25-
await using snap3 = await archivedFixtureFile(
2622
'http-server-under-load/3.heapsnapshot',
27-
);
28-
await using snap4 = await archivedFixtureFile(
2923
'http-server-under-load/4.heapsnapshot',
30-
);
31-
await expect(
32-
leakingObjectsInHeapSnapshotFiles([
33-
snap1.filepath,
34-
snap2.filepath,
35-
snap3.filepath,
36-
snap4.filepath,
37-
]),
38-
).resolves.toMatchInlineSnapshot(`
24+
]);
25+
await expect(leakingObjectsInHeapSnapshotFiles(snaps.filepaths)).resolves
26+
.toMatchInlineSnapshot(`
27+
{}
28+
`);
29+
},
30+
);
31+
32+
it.skipIf(
33+
// no need to test in bun (also, bun does not support increasing timeouts per test)
34+
globalThis.Bun,
35+
)(
36+
'should correctly detect randomly growing and freeing objects in size',
37+
{
38+
// parsing snapshots can take a while, so we increase the timeout
39+
timeout: 30_000,
40+
},
41+
async () => {
42+
await using snaps = await archivedFixtureFiles([
43+
'random-grow-and-free/1.heapsnapshot',
44+
'random-grow-and-free/2.heapsnapshot',
45+
'random-grow-and-free/3.heapsnapshot',
46+
'random-grow-and-free/4.heapsnapshot',
47+
'random-grow-and-free/5.heapsnapshot',
48+
'random-grow-and-free/6.heapsnapshot',
49+
]);
50+
await expect(leakingObjectsInHeapSnapshotFiles(snaps.filepaths)).resolves
51+
.toMatchInlineSnapshot(`
3952
{
4053
"(compiled code)": {
41-
"addedCount": 1727,
42-
"addedSize": 228096,
43-
"countDelta": 91,
54+
"addedCount": 24267,
55+
"addedSize": 4981944,
56+
"countDelta": -19747,
4457
"name": "(compiled code)",
45-
"removedCount": 1636,
46-
"removedSize": 163328,
47-
"sizeDelta": 64768,
58+
"removedCount": 44014,
59+
"removedSize": 2541944,
60+
"sizeDelta": 2440000,
4861
},
4962
}
5063
`);
5164
},
5265
);
5366

5467
/**
55-
* Unarchives the {@link archivedFile provided fixture file} for using in
56-
* tests and then removes it on disposal.
68+
* Unarchives the {@link archivedFiles provided fixture files} for using in
69+
* tests and then removes them on disposal.
5770
*
58-
* @param archivedFile - The name of the archived fixture file (without `.tar.gz`). Is the
71+
* @param archivedFiles - An array of file names of the archived fixture file (without `.tar.gz`). Is the
5972
* filename of the file inside the archive.
6073
*/
61-
async function archivedFixtureFile(archivedFile: string) {
62-
const filepath = path.join(__fixtures, archivedFile);
63-
const [, waitForExit] = await spawn(
64-
{
65-
cwd: __fixtures,
66-
},
67-
'tar',
68-
'-xz',
69-
'-f',
70-
archivedFile + '.tar.gz',
71-
'-C',
72-
path.dirname(filepath),
73-
);
74-
await waitForExit;
74+
async function archivedFixtureFiles(archivedFiles: string[]) {
75+
const filepaths: string[] = [];
76+
for (const archivedFile of archivedFiles) {
77+
const filepath = path.join(__fixtures, archivedFile);
78+
const [, waitForExit] = await spawn(
79+
{
80+
cwd: __fixtures,
81+
},
82+
'tar',
83+
'-xz',
84+
'-f',
85+
archivedFile + '.tar.gz',
86+
'-C',
87+
path.dirname(filepath),
88+
);
89+
await waitForExit;
90+
filepaths.push(filepath);
91+
}
7592
return {
76-
[Symbol.asyncDispose]() {
77-
return fs.unlink(filepath);
93+
async [Symbol.asyncDispose]() {
94+
await Promise.all(filepaths.map((filepath) => fs.unlink(filepath)));
7895
},
79-
filepath,
96+
filepaths,
8097
};
8198
}

0 commit comments

Comments
 (0)