Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add memory-spike plugin #33

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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.

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()`);
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
new Function(`gc()`);
new Function(`gc()`)();

I think you should call the function to it to throw, right?

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?


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");

});
Loading