-
-
Notifications
You must be signed in to change notification settings - Fork 17
feat: add memory-spike plugin #33
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be here, right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| toString() { | ||
| return "RecordMemorySpikePlugin"; | ||
| } | ||
| } | ||
| exports.RecordMemorySpikePlugin = RecordMemorySpikePlugin; | ||
| 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"); | ||
|
|
||
| }); |
There was a problem hiding this comment.
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-gcThere was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?