Skip to content

Commit

Permalink
feat(memory-enricher): added support to report memory heap statistics
Browse files Browse the repository at this point in the history
  • Loading branch information
H4ad committed Nov 26, 2023
1 parent 6733730 commit 441b3ad
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 18 deletions.
15 changes: 15 additions & 0 deletions examples/create-uint32array/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { Suite } = require('../../lib');

const suite = new Suite();

suite
.add(`new Uint32Array(1024)`, function () {
return new Uint32Array(1024);
})
.add(`new Uint32Array(1024 * 1024)`, function () {
return new Uint32Array(1024 * 1024);
})
.add(`new Uint32Array(1024 * 1024 * 10)`, function () {
return new Uint32Array(1024 * 1024 * 10);
})
.run();
45 changes: 45 additions & 0 deletions examples/create-uint32array/node.managed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { Suite } = require('../../lib');

const suite = new Suite();

suite
.add(`new Uint32Array(1024)`, function (timer) {
const assert = require('node:assert');

let r;

timer.start();
for (let i = 0; i < timer.count; i++) {
r = new Uint32Array(1024);
}
timer.end(timer.count);

assert.ok(r);
})
.add(`new Uint32Array(1024 * 1024)`, function (timer) {
const assert = require('node:assert');

let r;

timer.start();
for (let i = 0; i < timer.count; i++) {
r = new Uint32Array(1024 * 1024);
}
timer.end(timer.count);

assert.ok(r);
})
.add(`new Uint32Array(1024 * 1024 * 10)`, function (timer) {
const assert = require('node:assert');

let r;

timer.start();
for (let i = 0; i < timer.count; i++) {
r = new Uint32Array(1024 * 1024 * 10);
}
timer.end(timer.count);

assert.ok(r);
})
.run();
8 changes: 8 additions & 0 deletions examples/empty/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { Suite } = require('../../lib');

const suite = new Suite();

suite
.add(`empty`, function () {})
.add(`empty async`, async function () {})
.run();
64 changes: 53 additions & 11 deletions lib/clock.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,35 +106,72 @@ class ManagedTimer {
this.#iterations = iterations;
}

[kUnmanagedTimerResult]() {
[kUnmanagedTimerResult](context) {
if (this.#start === undefined)
throw new Error('You forgot to call .start()');

if (this.#end === undefined)
throw new Error('You forgot to call .end(count)');

return [Number(this.#end - this.#start), this.#iterations];
return [Number(this.#end - this.#start), this.#iterations, context];
}
}

function createRunUnmanagedBenchmark(awaitOrEmpty) {
return `
function createRunUnmanagedBenchmark(bench, awaitOrEmpty,) {
let code = `
let i = 0;
let context = {};
`;

const varNames = { managed: true, globalThisVar: 'globalThis', contextVar: 'context' };

for (const enrich of bench.enrichers) {
code += enrich.beforeClockTemplate(varNames);
}

code += `
const startedAt = timer.now();
for (let i = 0; i < count; i++)
for (; i < count; i++)
${awaitOrEmpty}bench.fn();
const duration = Number(timer.now() - startedAt);
return [duration, count];
`;

for (const enricher of bench.enrichers) {
code += enricher.afterClockTemplate(varNames);
}

code += 'return [duration, count, context];';

return code;
}

function createRunManagedBenchmark(awaitOrEmpty) {
return `
function createRunManagedBenchmark(bench, awaitOrEmpty) {
let code = `
let i = 0;
let context = {};
`;

const varNames = { managed: true, globalThisVar: 'globalThis', contextVar: 'context' };

for (const enrich of bench.enrichers) {
code += enrich.beforeClockTemplate(varNames);
}

code += `
${awaitOrEmpty}bench.fn(timer);
return timer[kUnmanagedTimerResult]();
`;
const result = timer[kUnmanagedTimerResult](context);
`;

for (const enricher of bench.enrichers) {
code += enricher.afterClockTemplate(varNames);
}

code += 'return result;';

return code;
}

const AsyncFunction = async function () {
Expand All @@ -151,14 +188,15 @@ function createRunner(bench, recommendedCount) {
}

const compiledFnStringFactory = hasArg ? createRunManagedBenchmark : createRunUnmanagedBenchmark;
const compiledFnString = compiledFnStringFactory(isAsync ? 'await ' : '');
const compiledFnString = compiledFnStringFactory(bench, isAsync ? 'await ' : '');
const createFnPrototype = isAsync ? AsyncFunction : SyncFunction;
const compiledFn = createFnPrototype('bench', 'timer', 'count', 'kUnmanagedTimerResult', compiledFnString);

const selectedTimer = hasArg ? new ManagedTimer(recommendedCount) : timer;

const runner = FunctionPrototypeBind(compiledFn, globalThis, bench, selectedTimer, recommendedCount, kUnmanagedTimerResult);

debugBench(`Compiled Code: ${ compiledFnString }`);
debugBench(`Created compiled benchmark, hasArg=${hasArg}, isAsync=${isAsync}, recommendedCount=${recommendedCount}`);

return runner;
Expand All @@ -173,6 +211,10 @@ async function clockBenchmark(bench, recommendedCount) {

debugBench(`Took ${timer.format(result[0])} to execute ${result[1]} iterations`);

for (const enricher of bench.enrichers) {
enricher.onCompleteClock(result);
}

return result;
}

Expand Down
95 changes: 95 additions & 0 deletions lib/enrichers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const { kStatisticalHistogramRecord, StatisticalHistogram, kStatisticalHistogramFinish } = require("./histogram");
const { MathRound, NumberPrototypeToFixed } = require('./primordials');

function formatBytes(bytes) {
if (bytes < 1024)
return `${ MathRound(bytes) }B`;

const kbytes = bytes / 1024;
if (kbytes < 1024)
return `${ NumberPrototypeToFixed(kbytes, 2) }Kb`;

const mbytes = kbytes / 1024;
if (mbytes < 1024)
return `${ NumberPrototypeToFixed(mbytes, 2) }MB`;

const gbytes = mbytes / 1024;
return `${ NumberPrototypeToFixed(gbytes, 2) }GB`;
}

class MemoryEnricher {
static MEMORY_BEFORE_RUN = 'memoryBeforRun';
static MEMORY_AFTER_RUN = 'memoryAfterRun';

/**
* @type {StatisticalHistogram}
*/
#heapUsedHistogram;

constructor() {
this.reset();
}

static isSupported() {
return typeof globalThis.gc === 'function';
}

reset() {
this.#heapUsedHistogram = new StatisticalHistogram();
}

beforeClockTemplate({ managed, globalThisVar, contextVar }) {
if (managed) {
process.emitWarning('The memory statistics can be inaccurate since it will include the tear-up and teardown of your benchmark.')
}

let code = '';

code += `${ contextVar }.${ MemoryEnricher.MEMORY_BEFORE_RUN } = 0;\n`;
code += `${ contextVar }.${ MemoryEnricher.MEMORY_AFTER_RUN } = 0;\n`;
code += `${ globalThisVar }.gc();\n`;
code += `${ contextVar }.${ MemoryEnricher.MEMORY_BEFORE_RUN } = ${ globalThisVar }.process.memoryUsage();\n`;

return code;
}

afterClockTemplate({ globalThisVar, contextVar }) {
return `${ contextVar }.${ MemoryEnricher.MEMORY_AFTER_RUN } = ${ globalThisVar }.process.memoryUsage();\n`;
}

onCompleteClock(result) {
const realIterations = result[1];
const context = result[2];

const heapUsed = context[MemoryEnricher.MEMORY_AFTER_RUN].heapUsed - context[MemoryEnricher.MEMORY_BEFORE_RUN].heapUsed;
const externalUsed = context[MemoryEnricher.MEMORY_AFTER_RUN].external - context[MemoryEnricher.MEMORY_BEFORE_RUN].external;

const memoryAllocated = (heapUsed + externalUsed) / realIterations;

// below 0, we just coerce to be zero
this.#heapUsedHistogram[kStatisticalHistogramRecord](Math.max(0, memoryAllocated));
}

onCompleteBenchmark() {
this.#heapUsedHistogram[kStatisticalHistogramFinish]();
}

toString() {
return `heap usage=${ formatBytes(this.#heapUsedHistogram.mean) } (${ formatBytes(this.#heapUsedHistogram.min) } ... ${ formatBytes(this.#heapUsedHistogram.max) })`;
}

getResult() {
return {
proto: null,
type: 'MemoryEnricher',
histogram: this.#heapUsedHistogram,
};
}
}

const supportedEnrichers = [MemoryEnricher].filter(enricher => enricher.isSupported());

module.exports = {
MemoryEnricher,
supportedEnrichers,
};
19 changes: 16 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ const { ArrayPrototypePush } = require('./primordials');
const { getInitialIterations, maxTime, minTime, runBenchmark } = require('./lifecycle');
const { debugBench, timer } = require('./clock');
const { validateFunction, validateNumber, validateObject, validateString } = require('./validators');
const { supportedEnrichers } = require('./enrichers');

class Benchmark {
#name;
#fn;
#minTime;
#maxTime;
#enrichers;

constructor(name, fn, minTime, maxTime) {
constructor(name, fn, minTime, maxTime, enrichers) {
this.#name = name;
this.#fn = fn;
this.#minTime = minTime;
this.#maxTime = maxTime;
this.#enrichers = enrichers.map(Enricher => new Enricher());
}

get name() {
Expand All @@ -32,6 +35,10 @@ class Benchmark {
get maxTime() {
return this.#maxTime;
}

get enrichers() {
return this.#enrichers;
}
}

class Suite {
Expand Down Expand Up @@ -76,11 +83,12 @@ class Suite {

const benchMinTime = benchOptions?.minTime ?? minTime;
const benchMaxTime = benchOptions?.maxTime ?? maxTime;
const benchEnrichers = benchOptions?.enrichers ?? supportedEnrichers;

validateNumber(benchMinTime, 'options.minTime', timer.resolution * 1e3);
validateNumber(benchMaxTime, 'options.maxTime', benchMinTime);

const benchmark = new Benchmark(name, benchFn, benchMinTime, benchMaxTime);
const benchmark = new Benchmark(name, benchFn, benchMinTime, benchMaxTime, benchEnrichers);

ArrayPrototypePush(this.#benchmarks, benchmark);

Expand All @@ -101,9 +109,14 @@ class Suite {

for (let i = 0; i < this.#benchmarks.length; i++) {
const benchmark = this.#benchmarks[i];

for (const enricher of benchmark.enrichers) {
enricher.reset();
}

const initialIteration = initialIterations[i];

debugBench(`Starting ${benchmark.name} with minTime=${benchmark.minTime}, maxTime=${benchmark.maxTime}`);
debugBench(`Starting ${ benchmark.name } with minTime=${ benchmark.minTime }, maxTime=${ benchmark.maxTime }`);

const result = await runBenchmark(benchmark, initialIteration);
results[i] = result;
Expand Down
9 changes: 7 additions & 2 deletions lib/lifecycle.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { MathMax, MathMin, MathRound, NumberMAX_SAFE_INTEGER } = require('./primordials');
const { clockBenchmark, debugBench, MIN_RESOLUTION, timer } = require('./clock');
const { clockBenchmark, debugBench, hasGcAvailable, MIN_RESOLUTION, timer } = require('./clock');
const { kStatisticalHistogramFinish, kStatisticalHistogramRecord, StatisticalHistogram } = require('./histogram');

// 0.05 - Arbitrary number used in some benchmark tools
Expand Down Expand Up @@ -43,7 +43,7 @@ async function runBenchmark(bench, initialIterations) {

while (benchTimeSpent < maxDuration) {
startClock = timer.now();
const { 0: duration, 1: realIterations } = await clockBenchmark(bench, initialIterations);
const { 0: duration, 1: realIterations, 2: context } = await clockBenchmark(bench, initialIterations);
benchTimeSpent += Number(timer.now() - startClock);

iterations += realIterations;
Expand All @@ -65,13 +65,18 @@ async function runBenchmark(bench, initialIterations) {

histogram[kStatisticalHistogramFinish]();

for (const enricher of bench.enrichers) {
enricher.onCompleteBenchmark();
}

const opsSec = iterations / (timeSpent / timer.scale);

return {
__proto__: null,
opsSec,
iterations,
histogram,
enrichers: bench.enrichers.map(enricher => enricher.getResult()),
};
}

Expand Down
8 changes: 6 additions & 2 deletions lib/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ function reportConsoleBench(bench, result) {

process.stdout.write(bench.name);
process.stdout.write(' x ');
process.stdout.write(`${formatter.format(opsSecReported)} ops/sec +/- `);
process.stdout.write(`${ formatter.format(opsSecReported) } ops/sec +/- `);
process.stdout.write(formatter.format(
NumberPrototypeToFixed(result.histogram.cv, 2)),
);
process.stdout.write(`% (${result.histogram.samples.length} runs sampled) `);
process.stdout.write(`% (${ result.histogram.samples.length } runs sampled) `);

for (const enrich of bench.enrichers) {
process.stdout.write(`${ enrich.toString() } `);
}

process.stdout.write('min..max=(');
process.stdout.write(timer.format(result.histogram.min));
Expand Down

0 comments on commit 441b3ad

Please sign in to comment.