Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/Plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,7 @@ optimization.

The `V8GetOptimizationStatus` plugin collects the V8 engine's optimization
status for a given function after it has been benchmarked.

### Class: `RecordMemorySpikePlugin`

A plugin to record memory allocation spikes in your benchmark's run. It should help you understand the speed vs memory tradeoffs you're making.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should mention folks that are going to use it will need to pass --expose-gc

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also mention a possible performance inconsistency when this plugin is enabled due to GC blocking op.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm only calling gc in oncomplete, which I assumed is not part of the span that is being measured. is that incorrect?

2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
V8NeverOptimizePlugin,
V8GetOptimizationStatus,
V8OptimizeOnNextCallPlugin,
RecordMemorySpikePlugin,
} = require('./plugins');
const {
validateFunction,
Expand Down Expand Up @@ -205,6 +206,7 @@ module.exports = {
V8NeverOptimizePlugin,
V8GetOptimizationStatus,
V8OptimizeOnNextCallPlugin,
RecordMemorySpikePlugin,
chartReport,
textReport,
htmlReport,
Expand Down
5 changes: 5 additions & 0 deletions lib/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const {
MemoryPlugin,
} = require('./plugins/memory');

const {
RecordMemorySpikePlugin
} = require('./plugins/memory-spike');

const {
validateFunction,
validateArray,
Expand Down Expand Up @@ -53,5 +57,6 @@ module.exports = {
V8NeverOptimizePlugin,
V8GetOptimizationStatus,
V8OptimizeOnNextCallPlugin,
RecordMemorySpikePlugin,
validatePlugins,
};
109 changes: 109 additions & 0 deletions lib/plugins/memory-spike.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const v8 = require("node:v8");

const translateHeapStats = (stats = []) => {
const result = {};
for (const { space_name, space_used_size } of stats) {
result[space_name] = space_used_size;
}
return result;
};

const updateMaxEachKey = (current, update) => {
for (const key in current) {
current[key] = Math.max(current[key], update[key]);
}
};

const diffEachKey = (a, b, divBy = 1) => {
const result = {};
for (const key in a) {
result[key] = (b[key] - a[key]) / divBy;
}
return result;
};

const avgEachKey = (items) => {
const result = {};
for (const item of items) {
for (const key in item) {
result[key] = (result[key] || 0) + item[key];
}
}
for (const key in result) {
result[key] /= items.length;
}

return result;
};

const toHumanReadable = (obj) => {
const result = {};
for (const key in obj) {
if (obj[key] > 0) result[key] = `+${(obj[key] / 1024).toFixed(4)} KB`;
}
return result;
};

globalThis.__recordMemorySpike = (frequency = 2) => {
const initial = translateHeapStats(v8.getHeapSpaceStatistics());
const result = { ...initial };
const collect = () =>
updateMaxEachKey(result, translateHeapStats(v8.getHeapSpaceStatistics()));
const interval = setInterval(collect, frequency);
return {
collect,
getResult: () => {
clearInterval(interval);
collect();
return [initial, result];
},
};
};

class RecordMemorySpikePlugin {
#spikeSamples = {};
isSupported() {
try {
new Function(`gc()`)();
return true;
} catch (e) {
return false;
}
}

beforeClockTemplate() {
return [`const __mem_spike__ = __recordMemorySpike();`];
}
afterClockTemplate({ context, bench }) {
return [
`;
${context}.benchName=${bench}.name;
${context}.memSpike = __mem_spike__.getResult();
`,
];
}

onCompleteBenchmark([_time, iterations, results]) {
gc();
const [initial, result] = results.memSpike;
const diff = diffEachKey(initial, result, iterations);
if (!this.#spikeSamples[results.benchName]) {
this.#spikeSamples[results.benchName] = [];
}
this.#spikeSamples[results.benchName].push(diff);
}

getResult(name) {
return toHumanReadable(avgEachKey(this.#spikeSamples[name]));
}

getReport() {
process._rawDebug('grp',arguments);

}
Comment on lines +100 to +103
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to brainstorm what to produce as the report - or is producing results enough? If we get it right, the report might be nice tho.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called by the reporters, and it should return a string in case you want to show it there. See on textReporter


toString() {
return "RecordMemorySpikePlugin";
}
}
exports.RecordMemorySpikePlugin = RecordMemorySpikePlugin;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"main": "lib/index.js",
"scripts": {
"test": "node --test --allow-natives-syntax"
"test": "node --test --allow-natives-syntax --expose-gc"
},
"repository": {
"type": "git",
Expand Down
48 changes: 48 additions & 0 deletions test/plugin-memspike.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @ts-check
const { RecordMemorySpikePlugin, Suite
} = require('../lib/index');

const { test } = require("node:test");
const assert = require("node:assert");
const { setTimeout } = require("node:timers/promises");

const wasteMemoryForAWhile = async () => {
const a = Buffer.alloc(1024 * 1024, "a");
await setTimeout(5);
a.at(1); // prevent optimization
};
function noop() {}

test("RecordMemorySpikePlugin", async (t) => {
const bench = new Suite({
reporter: noop,
plugins: [new RecordMemorySpikePlugin()],
});
bench
.add("sequence", async () => {
for (let i = 0; i < 20; i++) {
await wasteMemoryForAWhile();
}
})
.add("concurent", async () => {
await Promise.all(
Array.from({ length: 20 }, () => wasteMemoryForAWhile()),
);
});

const [bench1, bench2] = await bench.run();
console.dir(
{
bench1,
bench2,
},
{ depth: 100 },
);

const { plugins: [{ result: result1 }] } = bench1;
const { plugins: [{ result: result2 }] } = bench2;
const parseResult = (str) => parseFloat(str.replace(/[^\d.-]/g, ''));
assert.ok(parseResult(result1.new_space) > parseResult(result2.new_space), "Sequence new_space should be larger than concurrent new_space");
assert.ok(parseResult(result1.old_space) > parseResult(result2.old_space), "Sequence old_space should be larger than concurrent old_space");

});