mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
0b6b2a4dc7
PR-URL: https://github.com/nodejs/node/pull/54355 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
193 lines
7.3 KiB
JavaScript
193 lines
7.3 KiB
JavaScript
'use strict';
|
|
|
|
const wait = require('timers/promises').setTimeout;
|
|
const assert = require('assert');
|
|
const common = require('../common');
|
|
const gcTrackerMap = new WeakMap();
|
|
const gcTrackerTag = 'NODE_TEST_COMMON_GC_TRACKER';
|
|
|
|
/**
|
|
* Installs a garbage collection listener for the specified object.
|
|
* Uses async_hooks for GC tracking, which may affect test functionality.
|
|
* A full setImmediate() invocation passes between a global.gc() call and the listener being invoked.
|
|
* @param {object} obj - The target object to track for garbage collection.
|
|
* @param {object} gcListener - The listener object containing the ongc callback.
|
|
* @param {Function} gcListener.ongc - The function to call when the target object is garbage collected.
|
|
*/
|
|
function onGC(obj, gcListener) {
|
|
const async_hooks = require('async_hooks');
|
|
|
|
const onGcAsyncHook = async_hooks.createHook({
|
|
init: common.mustCallAtLeast(function(id, type) {
|
|
if (this.trackedId === undefined) {
|
|
assert.strictEqual(type, gcTrackerTag);
|
|
this.trackedId = id;
|
|
}
|
|
}),
|
|
destroy(id) {
|
|
assert.notStrictEqual(this.trackedId, -1);
|
|
if (id === this.trackedId) {
|
|
this.gcListener.ongc();
|
|
onGcAsyncHook.disable();
|
|
}
|
|
},
|
|
}).enable();
|
|
onGcAsyncHook.gcListener = gcListener;
|
|
|
|
gcTrackerMap.set(obj, new async_hooks.AsyncResource(gcTrackerTag));
|
|
obj = null;
|
|
}
|
|
|
|
/**
|
|
* Repeatedly triggers garbage collection until a specified condition is met or a maximum number of attempts is reached.
|
|
* @param {string|Function} [name] - Optional name, used in the rejection message if the condition is not met.
|
|
* @param {Function} condition - A function that returns true when the desired condition is met.
|
|
* @returns {Promise} A promise that resolves when the condition is met, or rejects after 10 failed attempts.
|
|
*/
|
|
function gcUntil(name, condition) {
|
|
if (typeof name === 'function') {
|
|
condition = name;
|
|
name = undefined;
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
let count = 0;
|
|
function gcAndCheck() {
|
|
setImmediate(() => {
|
|
count++;
|
|
global.gc();
|
|
if (condition()) {
|
|
resolve();
|
|
} else if (count < 10) {
|
|
gcAndCheck();
|
|
} else {
|
|
reject(name === undefined ? undefined : 'Test ' + name + ' failed');
|
|
}
|
|
});
|
|
}
|
|
gcAndCheck();
|
|
});
|
|
}
|
|
|
|
// This function can be used to check if an object factor leaks or not,
|
|
// but it needs to be used with care:
|
|
// 1. The test should be set up with an ideally small
|
|
// --max-old-space-size or --max-heap-size, which combined with
|
|
// the maxCount parameter can reproduce a leak of the objects
|
|
// created by fn().
|
|
// 2. This works under the assumption that if *none* of the objects
|
|
// created by fn() can be garbage-collected, the test would crash due
|
|
// to OOM.
|
|
// 3. If *any* of the objects created by fn() can be garbage-collected,
|
|
// it is considered leak-free. The FinalizationRegistry is used to
|
|
// terminate the test early once we detect any of the object is
|
|
// garbage-collected to make the test less prone to false positives.
|
|
// This may be especially important for memory management relying on
|
|
// emphemeron GC which can be inefficient to deal with extremely fast
|
|
// heap growth.
|
|
// Note that this can still produce false positives. When the test using
|
|
// this function still crashes due to OOM, inspect the heap to confirm
|
|
// if a leak is present (e.g. using heap snapshots).
|
|
// The generateSnapshotAt parameter can be used to specify a count
|
|
// interval to create the heap snapshot which may enforce a more thorough GC.
|
|
// This can be tried for code paths that require it for the GC to catch up
|
|
// with heap growth. However this type of forced GC can be in conflict with
|
|
// other logic in V8 such as bytecode aging, and it can slow down the test
|
|
// significantly, so it should be used scarcely and only as a last resort.
|
|
async function checkIfCollectable(
|
|
fn, maxCount = 4096, generateSnapshotAt = Infinity, logEvery = 128) {
|
|
let anyFinalized = false;
|
|
let count = 0;
|
|
|
|
const f = new FinalizationRegistry(() => {
|
|
anyFinalized = true;
|
|
});
|
|
|
|
async function createObject() {
|
|
const obj = await fn();
|
|
f.register(obj);
|
|
if (count++ < maxCount && !anyFinalized) {
|
|
setImmediate(createObject, 1);
|
|
}
|
|
// This can force a more thorough GC, but can slow the test down
|
|
// significantly in a big heap. Use it with care.
|
|
if (count % generateSnapshotAt === 0) {
|
|
// XXX(joyeecheung): This itself can consume a bit of JS heap memory,
|
|
// but the other alternative writeHeapSnapshot can run into disk space
|
|
// not enough problems in the CI & be slower depending on file system.
|
|
// Just do this for now as long as it works and only invent some
|
|
// internal voodoo when we absolutely have no other choice.
|
|
require('v8').getHeapSnapshot().pause().read();
|
|
console.log(`Generated heap snapshot at ${count}`);
|
|
}
|
|
if (count % logEvery === 0) {
|
|
console.log(`Created ${count} objects`);
|
|
}
|
|
if (anyFinalized) {
|
|
console.log(`Found finalized object at ${count}, stop testing`);
|
|
}
|
|
}
|
|
|
|
createObject();
|
|
}
|
|
|
|
// Repeat an operation and give GC some breathing room at every iteration.
|
|
async function runAndBreathe(fn, repeat, waitTime = 20) {
|
|
for (let i = 0; i < repeat; i++) {
|
|
await fn();
|
|
await wait(waitTime);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This requires --expose-internals.
|
|
* This function can be used to check if an object factory leaks or not by
|
|
* iterating over the heap and count objects with the specified class
|
|
* (which is checked by looking up the prototype chain).
|
|
* @param {(i: number) => number} fn The factory receiving iteration count
|
|
* and returning number of objects created. The return value should be
|
|
* precise otherwise false negatives can be produced.
|
|
* @param {Function} ctor The constructor of the objects being counted.
|
|
* @param {number} count Number of iterations that this check should be done
|
|
* @param {number} waitTime Optional breathing time for GC.
|
|
*/
|
|
async function checkIfCollectableByCounting(fn, ctor, count, waitTime = 20) {
|
|
const { queryObjects } = require('v8');
|
|
const { name } = ctor;
|
|
const initialCount = queryObjects(ctor, { format: 'count' });
|
|
console.log(`Initial count of ${name}: ${initialCount}`);
|
|
let totalCreated = 0;
|
|
for (let i = 0; i < count; ++i) {
|
|
const created = await fn(i);
|
|
totalCreated += created;
|
|
console.log(`#${i}: created ${created} ${name}, total ${totalCreated}`);
|
|
await wait(waitTime); // give GC some breathing room.
|
|
const currentCount = queryObjects(ctor, { format: 'count' });
|
|
const collected = totalCreated - (currentCount - initialCount);
|
|
console.log(`#${i}: counted ${currentCount} ${name}, collected ${collected}`);
|
|
if (collected > 0) {
|
|
console.log(`Detected ${collected} collected ${name}, finish early`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await wait(waitTime); // give GC some breathing room.
|
|
const currentCount = queryObjects(ctor, { format: 'count' });
|
|
const collected = totalCreated - (currentCount - initialCount);
|
|
console.log(`Last count: counted ${currentCount} ${name}, collected ${collected}`);
|
|
// Some objects with the prototype can be collected.
|
|
if (collected > 0) {
|
|
console.log(`Detected ${collected} collected ${name}`);
|
|
return;
|
|
}
|
|
|
|
throw new Error(`${name} cannot be collected`);
|
|
}
|
|
|
|
module.exports = {
|
|
checkIfCollectable,
|
|
runAndBreathe,
|
|
checkIfCollectableByCounting,
|
|
onGC,
|
|
gcUntil,
|
|
};
|