From 8f2c3a434f8ad26d2f469353742a20c772d8d863 Mon Sep 17 00:00:00 2001 From: naugtur Date: Wed, 25 Dec 2024 23:31:58 +0100 Subject: [PATCH 1/2] feat: add memory-spike plugin --- doc/Plugins.md | 4 ++ lib/index.js | 2 + lib/plugins.js | 5 ++ lib/plugins/memory-spike.js | 109 ++++++++++++++++++++++++++++++++++++ package.json | 2 +- test/plugin-memspike.js | 48 ++++++++++++++++ 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 lib/plugins/memory-spike.js create mode 100644 test/plugin-memspike.js diff --git a/doc/Plugins.md b/doc/Plugins.md index 4a9c63c..547b75c 100644 --- a/doc/Plugins.md +++ b/doc/Plugins.md @@ -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. diff --git a/lib/index.js b/lib/index.js index 4ae5843..a0a90ad 100644 --- a/lib/index.js +++ b/lib/index.js @@ -15,6 +15,7 @@ const { V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, + RecordMemorySpikePlugin, } = require('./plugins'); const { validateFunction, @@ -205,6 +206,7 @@ module.exports = { V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, + RecordMemorySpikePlugin, chartReport, textReport, htmlReport, diff --git a/lib/plugins.js b/lib/plugins.js index b18a631..2990d22 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -12,6 +12,10 @@ const { MemoryPlugin, } = require('./plugins/memory'); +const { + RecordMemorySpikePlugin +} = require('./plugins/memory-spike'); + const { validateFunction, validateArray, @@ -53,5 +57,6 @@ module.exports = { V8NeverOptimizePlugin, V8GetOptimizationStatus, V8OptimizeOnNextCallPlugin, + RecordMemorySpikePlugin, validatePlugins, }; diff --git a/lib/plugins/memory-spike.js b/lib/plugins/memory-spike.js new file mode 100644 index 0000000..71d8b49 --- /dev/null +++ b/lib/plugins/memory-spike.js @@ -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); + + } + + toString() { + return "RecordMemorySpikePlugin"; + } +} +exports.RecordMemorySpikePlugin = RecordMemorySpikePlugin; diff --git a/package.json b/package.json index fad2318..a1ed1ee 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/plugin-memspike.js b/test/plugin-memspike.js new file mode 100644 index 0000000..5f40945 --- /dev/null +++ b/test/plugin-memspike.js @@ -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"); + +}); From 397da5f8774e30e408faa9671deda3537a9f1f1e Mon Sep 17 00:00:00 2001 From: Zbyszek Tenerowicz Date: Sat, 11 Jan 2025 11:01:30 +0100 Subject: [PATCH 2/2] fix: gc check Co-authored-by: Rafael Gonzaga --- lib/plugins/memory-spike.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugins/memory-spike.js b/lib/plugins/memory-spike.js index 71d8b49..9f6dde6 100644 --- a/lib/plugins/memory-spike.js +++ b/lib/plugins/memory-spike.js @@ -64,7 +64,7 @@ class RecordMemorySpikePlugin { #spikeSamples = {}; isSupported() { try { - new Function(`gc()`); + new Function(`gc()`)(); return true; } catch (e) { return false;