-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
eng: add assertHeap method for memory assertions (#198334)
This adds an `assertHeap` function that can be used in tests. It takes a heap snapshot, and asserts the state of classes in memory. This works in Node and the Electron sandbox, but is a no-op in the browser. Snapshots are process asynchronously and will report failures at the end of the suite. This method should be used sparingly (e.g. once at the end of a suite to ensure nothing leaked before), as gathering a heap snapshot is fairly slow, at least until V8 11.5.130 (https://v8.dev/blog/speeding-up-v8-heap-snapshots). When used, the function will ensure the test has a minimum timeout duration of 20s to avoid immediate failures. It takes options containing a mapping of class names, and assertion functions to run on the number of retained instances of that class. For example: ```ts assertSnapshot({ classes: { ShouldNeverLeak: count => assert.strictEqual(count, 0), SomeSingleton: count => assert(count <= 1), } }); ``` Closes #191920
- Loading branch information
1 parent
8cac42e
commit a0b5488
Showing
7 changed files
with
181 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
|
||
declare const __analyzeSnapshotInTests: (currentTest: string, classes: readonly string[]) => Promise<({ done: Promise<number[]>; file: string })>; | ||
|
||
let currentTest: Mocha.Test | undefined; | ||
|
||
const snapshotsToAssert: ({ counts: Promise<number[]>; file: string; test: string; opts: ISnapshotAssertOptions })[] = []; | ||
|
||
setup(function () { | ||
currentTest = this.currentTest; | ||
}); | ||
|
||
suiteTeardown(async () => { | ||
await Promise.all(snapshotsToAssert.map(async snap => { | ||
const counts = await snap.counts; | ||
|
||
const asserts = Object.entries(snap.opts.classes); | ||
if (asserts.length !== counts.length) { | ||
throw new Error(`expected class counts to equal assertions length for ${snap.test}`); | ||
} | ||
|
||
for (const [i, [name, doAssert]] of asserts.entries()) { | ||
try { | ||
doAssert(counts[i]); | ||
} catch (e) { | ||
throw new Error(`Unexpected number of ${name} instances (${counts[i]}) after "${snap.test}":\n\n${e.message}\n\nSnapshot saved at: ${snap.file}`); | ||
} | ||
} | ||
})); | ||
|
||
snapshotsToAssert.length = 0; | ||
}); | ||
|
||
export interface ISnapshotAssertOptions { | ||
classes: Record<string, (count: number) => void>; | ||
} | ||
|
||
const snapshotMinTime = 20_000; | ||
|
||
/** | ||
* Takes a heap snapshot, and asserts the state of classes in memory. This | ||
* works in Node and the Electron sandbox, but is a no-op in the browser. | ||
* Snapshots are process asynchronously and will report failures at the end of | ||
* the suite. | ||
* | ||
* This method should be used sparingly (e.g. once at the end of a suite to | ||
* ensure nothing leaked before), as gathering a heap snapshot is fairly | ||
* slow, at least until V8 11.5.130 (https://v8.dev/blog/speeding-up-v8-heap-snapshots). | ||
* | ||
* Takes options containing a mapping of class names, and assertion functions | ||
* to run on the number of retained instances of that class. For example: | ||
* | ||
* ```ts | ||
* assertSnapshot({ | ||
* classes: { | ||
* ShouldNeverLeak: count => assert.strictEqual(count, 0), | ||
* SomeSingleton: count => assert(count <= 1), | ||
* } | ||
*}); | ||
* ``` | ||
*/ | ||
export async function assertHeap(opts: ISnapshotAssertOptions) { | ||
if (!currentTest) { | ||
throw new Error('assertSnapshot can only be used when a test is running'); | ||
} | ||
|
||
// snapshotting can take a moment, ensure the test timeout is decently long | ||
// so it doesn't immediately fail. | ||
if (currentTest.timeout() < snapshotMinTime) { | ||
currentTest.timeout(snapshotMinTime); | ||
} | ||
|
||
if (typeof __analyzeSnapshotInTests === 'undefined') { | ||
return; // running in browser, no-op | ||
} | ||
|
||
const { done, file } = await __analyzeSnapshotInTests(currentTest.fullTitle(), Object.keys(opts.classes)); | ||
snapshotsToAssert.push({ counts: done, file, test: currentTest.fullTitle(), opts }); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All rights reserved. | ||
* Licensed under the MIT License. See License.txt in the project root for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
//@ts-check | ||
|
||
// note: we use a fork here since we can't make a worker from the renderer process | ||
|
||
const { fork } = require('child_process'); | ||
const workerData = process.env.SNAPSHOT_WORKER_DATA; | ||
const fs = require('fs'); | ||
const { pathToFileURL } = require('url'); | ||
|
||
if (!workerData) { | ||
const { join } = require('path'); | ||
const { tmpdir } = require('os'); | ||
|
||
exports.takeSnapshotAndCountClasses = async (/** @type string */currentTest, /** @type string[] */ classes) => { | ||
const cleanTitle = currentTest.replace(/[^\w]+/g, '-'); | ||
const file = join(tmpdir(), `vscode-test-snap-${cleanTitle}.heapsnapshot`); | ||
|
||
if (typeof process.takeHeapSnapshot !== 'function') { | ||
// node.js: | ||
const inspector = require('inspector'); | ||
const session = new inspector.Session(); | ||
session.connect(); | ||
|
||
const fd = fs.openSync(file, 'w'); | ||
await new Promise((resolve, reject) => { | ||
session.on('HeapProfiler.addHeapSnapshotChunk', (m) => { | ||
fs.writeSync(fd, m.params.chunk); | ||
}); | ||
|
||
session.post('HeapProfiler.takeHeapSnapshot', null, (err) => { | ||
session.disconnect(); | ||
fs.closeSync(fd); | ||
if (err) { | ||
reject(err); | ||
} else { | ||
resolve(); | ||
} | ||
}); | ||
}); | ||
} else { | ||
// electron exposes this nice method for us: | ||
process.takeHeapSnapshot(file); | ||
} | ||
|
||
const worker = fork(__filename, { | ||
env: { | ||
...process.env, | ||
SNAPSHOT_WORKER_DATA: JSON.stringify({ | ||
path: file, | ||
classes, | ||
}) | ||
} | ||
}); | ||
|
||
const promise = new Promise((resolve, reject) => { | ||
worker.on('message', (/** @type any */msg) => { | ||
if ('err' in msg) { | ||
reject(new Error(msg.err)); | ||
} else { | ||
resolve(msg.counts); | ||
} | ||
worker.kill(); | ||
}); | ||
}); | ||
|
||
return { done: promise, file: pathToFileURL(file) }; | ||
}; | ||
} else { | ||
const { path, classes } = JSON.parse(workerData); | ||
const { decode_bytes } = require('@vscode/v8-heap-parser'); | ||
|
||
fs.promises.readFile(path) | ||
.then(buf => decode_bytes(buf)) | ||
.then(graph => graph.get_class_counts(classes)) | ||
.then( | ||
counts => process.send({ counts: Array.from(counts) }), | ||
err => process.send({ err: String(err.stack || err) }) | ||
); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters