mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: https://github.com/nodejs/node/pull/66536 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Bryan English <bryan@bryanenglish.com>
417 lines
14 KiB
JavaScript
417 lines
14 KiB
JavaScript
// Flags: --experimental-bench --no-warnings
|
|
'use strict';
|
|
|
|
const common = require('../common');
|
|
const { spawnSyncAndAssert } = require('../common/child_process');
|
|
const assert = require('assert');
|
|
const { createRunner } = require('node:bench');
|
|
const { setImmediate } = require('timers/promises');
|
|
|
|
function recordSample(b) {
|
|
b.record({
|
|
__proto__: null,
|
|
operations: 1,
|
|
duration_ns: 1n,
|
|
});
|
|
}
|
|
|
|
async function testReadableBackpressure() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const sampleCount = 64;
|
|
let calls = 0;
|
|
const completion = runner.bench('bounded stream', {
|
|
samples: sampleCount,
|
|
}, (b) => {
|
|
calls++;
|
|
recordSample(b);
|
|
});
|
|
const stream = runner.run({ warmup: 0 });
|
|
const iterator = stream[Symbol.asyncIterator]();
|
|
const first = await iterator.next();
|
|
|
|
assert.strictEqual(first.value.type, 'bench:plan');
|
|
await setImmediate();
|
|
assert(calls < sampleCount);
|
|
assert(stream.readableLength <= stream.readableHighWaterMark);
|
|
|
|
const records = [first.value];
|
|
for (;;) {
|
|
const next = await iterator.next();
|
|
if (next.done) break;
|
|
records.push(next.value);
|
|
}
|
|
|
|
const result = await completion;
|
|
assert.strictEqual(calls, sampleCount);
|
|
assert.strictEqual(result.samples.length, sampleCount);
|
|
assert.strictEqual(records[1].type, 'bench:start');
|
|
assert.strictEqual(records.length, sampleCount + 4);
|
|
}
|
|
|
|
async function testPlanBackpressure() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const benchmarkCount = 32;
|
|
const completions = [];
|
|
let calls = 0;
|
|
for (let i = 0; i < benchmarkCount; i++) {
|
|
completions.push(runner.bench(`planned ${i}`, { samples: 1 }, (b) => {
|
|
calls++;
|
|
recordSample(b);
|
|
}));
|
|
}
|
|
const stream = runner.run({ warmup: 0 });
|
|
const iterator = stream[Symbol.asyncIterator]();
|
|
const first = await iterator.next();
|
|
|
|
assert.strictEqual(first.value.type, 'bench:plan');
|
|
await setImmediate();
|
|
assert.strictEqual(calls, 0);
|
|
assert(stream.readableLength <= stream.readableHighWaterMark);
|
|
const records = [first.value];
|
|
for (;;) {
|
|
const next = await iterator.next();
|
|
if (next.done) break;
|
|
records.push(next.value);
|
|
}
|
|
|
|
await Promise.all(completions);
|
|
assert.strictEqual(calls, benchmarkCount);
|
|
assert.strictEqual(
|
|
records.slice(0, benchmarkCount).every(({ type }) => type === 'bench:plan'),
|
|
true,
|
|
);
|
|
}
|
|
|
|
async function testDestroyWhileBlocked() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const completions = [];
|
|
for (let i = 0; i < 32; i++) {
|
|
completions.push(runner.bench(`destroyed ${i}`, {
|
|
samples: 1,
|
|
}, recordSample));
|
|
}
|
|
const stream = runner.run({ warmup: 0 });
|
|
const unblocked = stream.waitForDrain();
|
|
const iterator = stream[Symbol.asyncIterator]();
|
|
await iterator.next();
|
|
await unblocked;
|
|
for (let i = 0; i < 100 &&
|
|
stream.readableLength < stream.readableHighWaterMark; i++) {
|
|
await setImmediate();
|
|
}
|
|
assert.strictEqual(stream.readableLength, stream.readableHighWaterMark);
|
|
|
|
const draining = stream.waitForDrain();
|
|
const closed = new Promise((resolve) => stream.once('close', resolve));
|
|
stream.destroy();
|
|
await assert.rejects(draining, { code: 'ERR_INVALID_STATE' });
|
|
await assert.rejects(stream.waitForDrain(), { code: 'ERR_INVALID_STATE' });
|
|
await closed;
|
|
await Promise.all(completions);
|
|
}
|
|
|
|
async function testNamedEventsWithoutReading() {
|
|
const runner = createRunner();
|
|
const sampleCount = 64;
|
|
let calls = 0;
|
|
const completion = runner.bench('named events', {
|
|
samples: sampleCount,
|
|
}, (b) => {
|
|
calls++;
|
|
recordSample(b);
|
|
});
|
|
const stream = runner.run({ warmup: 0 });
|
|
const summary = await new Promise((resolve) => {
|
|
stream.once('bench:summary', resolve);
|
|
});
|
|
const result = await completion;
|
|
|
|
assert.strictEqual(calls, sampleCount);
|
|
assert.strictEqual(result.samples.length, sampleCount);
|
|
assert.strictEqual(summary.success, true);
|
|
assert.strictEqual(stream.readableLength, sampleCount + 4);
|
|
assert(stream.readableLength > stream.readableHighWaterMark);
|
|
stream.destroy();
|
|
}
|
|
|
|
async function testCancellationCompletesBenchmarks() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const first = runner.bench('cancelled stream', { samples: 64 }, recordSample);
|
|
const second = runner.bench('continues headlessly', {
|
|
samples: 1,
|
|
}, recordSample);
|
|
const stream = runner.run({ warmup: 0 });
|
|
const iterator = stream[Symbol.asyncIterator]();
|
|
|
|
await iterator.next();
|
|
await iterator.return();
|
|
const results = await Promise.all([first, second]);
|
|
assert.strictEqual(results[0].samples.length, 64);
|
|
assert.strictEqual(results[1].samples.length, 1);
|
|
}
|
|
|
|
async function testDeliveryDoesNotConsumeTimeout() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
// The timeout only has to cover the benchmark's own work, which is 32 samples
|
|
// that do nothing but record a fixed value. Keep it generous so that a loaded
|
|
// machine cannot exhaust it on its own, and keep the consumer stalled for
|
|
// longer than the timeout so that the benchmark can only complete when the
|
|
// time spent delivering records is excluded from the timeout.
|
|
const timeout = common.platformTimeout(500);
|
|
const completion = runner.bench('slow consumer', {
|
|
samples: 32,
|
|
timeout,
|
|
}, recordSample);
|
|
const stream = runner.run({ warmup: 0 });
|
|
const iterator = stream[Symbol.asyncIterator]();
|
|
|
|
await iterator.next();
|
|
await setImmediate();
|
|
for (;;) {
|
|
const next = await iterator.next();
|
|
if (next.done) break;
|
|
}
|
|
|
|
const result = await completion;
|
|
assert.strictEqual(result.error, undefined);
|
|
assert.strictEqual(result.samples.length, 32);
|
|
}
|
|
|
|
async function testReportingFailureSettlesBenchmarks() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const failure = new Error('record listener failed');
|
|
const first = runner.bench('reported', { samples: 1 }, recordSample);
|
|
const second = runner.bench('settled', { samples: 1 }, recordSample);
|
|
const stream = runner.run({ warmup: 0 });
|
|
stream.once('bench:complete', common.mustCall(() => {
|
|
throw failure;
|
|
}));
|
|
stream.resume();
|
|
|
|
const results = await Promise.all([first, second]);
|
|
assert.strictEqual(results[0].error, undefined);
|
|
assert.strictEqual(results[1].error, failure);
|
|
await setImmediate();
|
|
}
|
|
|
|
async function testSummaryListenerFailure() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const failure = new Error('summary listener failed');
|
|
const completion = runner.bench('summary failure', {
|
|
samples: 1,
|
|
}, recordSample);
|
|
const stream = runner.run({ warmup: 0 });
|
|
const diagnostics = [];
|
|
stream.on('bench:diagnostic', (diagnostic) => {
|
|
diagnostics.push(diagnostic);
|
|
});
|
|
stream.once('bench:summary', common.mustCall(() => {
|
|
throw failure;
|
|
}));
|
|
const ended = new Promise((resolve) => stream.once('end', resolve));
|
|
stream.resume();
|
|
|
|
const result = await completion;
|
|
await ended;
|
|
assert.strictEqual(result.error, undefined);
|
|
assert.strictEqual(diagnostics.length, 1);
|
|
assert.strictEqual(diagnostics[0].error.message, failure.message);
|
|
}
|
|
|
|
async function testRecordOwnership() {
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
const expectedError = new Error('expected failure');
|
|
expectedError.code = 'ERR_EXPECTED';
|
|
expectedError.cause = expectedError;
|
|
expectedError.uncloneable = new WeakMap();
|
|
expectedError.context = {
|
|
note: 'preserved',
|
|
callback() {},
|
|
get accessor() { return 'value'; },
|
|
};
|
|
expectedError.customContext = {
|
|
__proto__: {},
|
|
note: 'preserved',
|
|
};
|
|
expectedError.arrayPayload = [new WeakMap()];
|
|
const innerError = new Error('inner failure');
|
|
innerError.code = 'ERR_INNER';
|
|
const aggregate = new AggregateError([innerError], 'aggregate failure');
|
|
aggregate.code = 'ERR_AGGREGATE';
|
|
const causedError = new Error('caused failure', { cause: aggregate });
|
|
causedError.references = new Map([['self', causedError]]);
|
|
causedError.members = new Set([causedError]);
|
|
const measured = runner.bench('owned result', {
|
|
params: { kind: 'original' },
|
|
samples: 1,
|
|
}, (b) => {
|
|
b.record({
|
|
__proto__: null,
|
|
operations: 1,
|
|
duration_ns: 1n,
|
|
detail: { value: 'original' },
|
|
});
|
|
});
|
|
const failed = runner.bench('owned error', { samples: 1 }, () => {
|
|
throw expectedError;
|
|
});
|
|
const caused = runner.bench('owned cause', { samples: 1 }, () => {
|
|
throw causedError;
|
|
});
|
|
const throwingNameError = new Error('throwing name');
|
|
Object.defineProperty(throwingNameError, 'name', {
|
|
configurable: true,
|
|
get() { throw new Error('name getter'); },
|
|
});
|
|
const throwingName = runner.bench('throwing name', {
|
|
samples: 1,
|
|
}, () => {
|
|
throw throwingNameError;
|
|
});
|
|
const thrownValue = new WeakMap();
|
|
const uncloneable = runner.bench('uncloneable error', {
|
|
samples: 1,
|
|
}, () => {
|
|
throw thrownValue;
|
|
});
|
|
const proxyError = new Proxy({}, {
|
|
getPrototypeOf() {
|
|
throw new Error('prototype trap');
|
|
},
|
|
});
|
|
const trapped = runner.bench('trapping error', { samples: 1 }, () => {
|
|
throw proxyError;
|
|
});
|
|
const afterTrap = runner.bench('after trapping error', {
|
|
samples: 1,
|
|
}, recordSample);
|
|
const stream = runner.run({ warmup: 0 });
|
|
let eventSample;
|
|
let eventComplete;
|
|
let eventError;
|
|
let eventSummary;
|
|
|
|
stream.on('bench:sample', (sample) => {
|
|
if (sample.name !== 'owned result') return;
|
|
eventSample = sample;
|
|
sample.name = 'changed by event';
|
|
sample.detail.value = 'changed by event';
|
|
});
|
|
stream.on('bench:complete', (result) => {
|
|
if (result.name === 'owned result') {
|
|
eventComplete = result;
|
|
result.params.kind = 'changed by event';
|
|
result.samples[0].detail.value = 'changed by event';
|
|
} else if (result.name === 'owned error') {
|
|
eventError = result.error;
|
|
result.error.code = 'ERR_CHANGED';
|
|
}
|
|
});
|
|
stream.on('bench:summary', (summary) => {
|
|
eventSummary = summary;
|
|
summary.counts.total = 100;
|
|
});
|
|
|
|
const records = await stream.toArray();
|
|
const measuredResult = await measured;
|
|
const failedResult = await failed;
|
|
await caused;
|
|
const throwingNameResult = await throwingName;
|
|
const uncloneableResult = await uncloneable;
|
|
const trappedResult = await trapped;
|
|
const afterTrapResult = await afterTrap;
|
|
const streamSample = records.find(
|
|
({ type }) => type === 'bench:sample').data;
|
|
const streamResults = records.filter(
|
|
({ type }) => type === 'bench:complete').map(({ data }) => data);
|
|
const streamMeasured = streamResults.find(
|
|
({ name }) => name === 'owned result');
|
|
const streamFailed = streamResults.find(
|
|
({ name }) => name === 'owned error');
|
|
const streamCaused = streamResults.find(
|
|
({ name }) => name === 'owned cause');
|
|
const streamThrowingName = streamResults.find(
|
|
({ name }) => name === 'throwing name');
|
|
const streamSummary = records.find(
|
|
({ type }) => type === 'bench:summary').data;
|
|
|
|
assert.notStrictEqual(eventSample, streamSample);
|
|
assert.notStrictEqual(eventComplete, streamMeasured);
|
|
assert.notStrictEqual(streamMeasured, measuredResult);
|
|
assert.strictEqual(streamSample.name, 'owned result');
|
|
assert.strictEqual(streamSample.detail.value, 'original');
|
|
assert.strictEqual(streamMeasured.params.kind, 'original');
|
|
assert.strictEqual(streamMeasured.samples[0].detail.value, 'original');
|
|
assert.strictEqual(measuredResult.params.kind, 'original');
|
|
assert.strictEqual(measuredResult.samples[0].detail.value, 'original');
|
|
|
|
streamMeasured.samples[0].detail.value = 'changed by stream';
|
|
assert.strictEqual(measuredResult.samples[0].detail.value, 'original');
|
|
assert.notStrictEqual(eventError, streamFailed.error);
|
|
assert.notStrictEqual(streamFailed.error, expectedError);
|
|
assert.strictEqual(streamFailed.error.code, 'ERR_EXPECTED');
|
|
assert.strictEqual(streamFailed.error.cause, streamFailed.error);
|
|
assert.strictEqual(streamFailed.error.context.note, 'preserved');
|
|
assert.strictEqual(streamFailed.error.context.callback, undefined);
|
|
assert.strictEqual(streamFailed.error.context.accessor, undefined);
|
|
assert.strictEqual(streamFailed.error.customContext.note, 'preserved');
|
|
assert.deepStrictEqual(streamFailed.error.arrayPayload, [undefined]);
|
|
assert.strictEqual(failedResult.error, expectedError);
|
|
assert.strictEqual(failedResult.error.code, 'ERR_EXPECTED');
|
|
assert.strictEqual(failedResult.error.cause, failedResult.error);
|
|
assert.strictEqual(uncloneableResult.error, thrownValue);
|
|
assert.strictEqual(trappedResult.error, proxyError);
|
|
assert.strictEqual(afterTrapResult.error, undefined);
|
|
assert.strictEqual(throwingNameResult.error, throwingNameError);
|
|
assert.strictEqual(streamThrowingName.error.name, 'Error');
|
|
assert.strictEqual(streamThrowingName.error.message, 'throwing name');
|
|
assert(streamCaused.error.cause instanceof AggregateError);
|
|
assert.strictEqual(streamCaused.error.cause.name, 'AggregateError');
|
|
assert.strictEqual(streamCaused.error.cause.code, 'ERR_AGGREGATE');
|
|
assert.strictEqual(streamCaused.error.cause.errors[0].code, 'ERR_INNER');
|
|
assert.strictEqual(
|
|
streamCaused.error.references.get('self'), streamCaused.error);
|
|
assert.strictEqual(streamCaused.error.members.has(streamCaused.error), true);
|
|
assert.notStrictEqual(eventSummary, streamSummary);
|
|
assert.strictEqual(streamSummary.counts.total, 7);
|
|
}
|
|
|
|
function testOperatorsWithoutStreamModule() {
|
|
// Readable operators such as map() and toArray() are attached when
|
|
// node:stream is loaded. node:assert and ../common load node:stream, so
|
|
// check the operators in a child process that loads only node:bench.
|
|
const script = `
|
|
const { createRunner } = require('node:bench');
|
|
const runner = createRunner({ yieldBetweenSamples: false });
|
|
runner.bench('operators', { samples: 1 }, (b) => {
|
|
b.record({ operations: 1, duration_ns: 1n });
|
|
});
|
|
runner.run({ warmup: 0 })
|
|
.map((record) => record.type)
|
|
.toArray()
|
|
.then((types) => console.log(types.includes('bench:complete')));
|
|
`;
|
|
spawnSyncAndAssert(process.execPath, [
|
|
'--experimental-bench',
|
|
'--no-warnings',
|
|
'-e',
|
|
script,
|
|
], {
|
|
stdout: 'true',
|
|
trim: true,
|
|
});
|
|
}
|
|
|
|
(async () => {
|
|
await testReadableBackpressure();
|
|
await testPlanBackpressure();
|
|
await testDestroyWhileBlocked();
|
|
await testNamedEventsWithoutReading();
|
|
await testCancellationCompletesBenchmarks();
|
|
await testDeliveryDoesNotConsumeTimeout();
|
|
await testReportingFailureSettlesBenchmarks();
|
|
await testSummaryListenerFailure();
|
|
await testRecordOwnership();
|
|
testOperatorsWithoutStreamModule();
|
|
})().then(common.mustCall());
|