From 57f92a87b51f5cb33516dd930230b570acfb4e1a Mon Sep 17 00:00:00 2001 From: ishabi Date: Mon, 28 Sep 2026 10:29:12 +0200 Subject: [PATCH 1/2] perf: deduplicate CPU profile name conversions --- benchmark/name-caching.js | 103 +++++++++++++++++++++++++++++ bindings/translate-time-profile.cc | 10 +-- 2 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 benchmark/name-caching.js diff --git a/benchmark/name-caching.js b/benchmark/name-caching.js new file mode 100644 index 00000000..3dbf0080 --- /dev/null +++ b/benchmark/name-caching.js @@ -0,0 +1,103 @@ +'use strict' + +const { Script } = require('vm') +const { isMainThread } = require('worker_threads') +const { TimeProfiler } = require('../out/src/time-profiler-bindings') + +const scriptCount = Number(process.env.SCRIPT_COUNT || '500') +const functionsPerScript = Number(process.env.FUNCTIONS_PER_SCRIPT || '8') +const rounds = Number(process.env.ROUNDS || '8') +const work = Number(process.env.WORK || '1000') +const lines = Number(process.env.LINES || '32') +const iterations = Number(process.env.ITERATIONS || '9') +const warmup = Number(process.env.WARMUP || '3') + +function compileScript (scriptIndex) { + const handlers = [] + const calls = [] + + for (let i = 0; i < functionsPerScript; i++) { + const name = `handler_${scriptIndex}_${i}` + const statements = Array.from({ length: lines }, (_, line) => + `for (let k = 0; k < ${work}; k++) total += Math.sqrt(k * n + ${i + line})` + ) + handlers.push(` + function ${name}(n) { + let total = 0 + ${statements.join('\n')} + return total + }`) + calls.push(`total += ${name}(n + ${i})`) + } + + return new Script(` + (() => { + ${handlers.join('\n')} + return function run_${scriptIndex}(n) { + let total = 0 + ${calls.join('\n')} + return total + } + })() + `, { filename: `/opt/service/dist/modules/module-${scriptIndex}.js` }) + .runInThisContext() +} + +function countNodes (node) { + let count = 1 + for (const child of node.children) count += countNodes(child) + return count +} + +function median (values) { + return [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)] +} + +const scripts = Array.from({ length: scriptCount }, (_, i) => compileScript(i)) +const stopMicros = [] +const nanosPerNode = [] +const nodeCounts = [] + +for (let iteration = 0; iteration < warmup + iterations; iteration++) { + const profiler = new TimeProfiler({ + intervalMicros: 50, + durationMillis: 60000, + lineNumbers: true, + withContexts: false, + workaroundV8Bug: false, + collectCpuTime: false, + collectAsyncId: false, + isMainThread, + useCPED: false + }) + profiler.start() + + let result = 0 + for (let round = 0; round < rounds; round++) { + for (const run of scripts) result += run(round + 1) + } + + const start = process.hrtime.bigint() + const profile = profiler.stop(false) + const elapsed = Number(process.hrtime.bigint() - start) + profiler.dispose() + + if (!Number.isFinite(result)) throw new Error('benchmark workload failed') + if (iteration < warmup) continue + + const nodes = countNodes(profile.topDownRoot) + stopMicros.push(elapsed / 1000) + nanosPerNode.push(elapsed / nodes) + nodeCounts.push(nodes) +} + +console.log(JSON.stringify({ + name: 'profile-name-caching', + scripts: scriptCount, + functionsPerScript, + lines, + iterations, + medianNodes: median(nodeCounts), + medianStopMicros: median(stopMicros), + medianNanosPerNode: median(nanosPerNode) +})) diff --git a/bindings/translate-time-profile.cc b/bindings/translate-time-profile.cc index 4a915a6f..53b300cf 100644 --- a/bindings/translate-time-profile.cc +++ b/bindings/translate-time-profile.cc @@ -344,6 +344,8 @@ class TimeProfileTranslator : ProfileTranslator { unsigned int hitLineCount = node->GetHitLineCount(); unsigned int hitCount = node->GetHitCount(); + auto name = node->GetFunctionName(); + auto scriptName = node->GetScriptResourceName(); auto scriptId = NewInteger(node->GetScriptId()); if (hitLineCount > 0) { std::vector entries(hitLineCount); @@ -352,8 +354,8 @@ class TimeProfileTranslator : ProfileTranslator { for (const v8::CpuProfileNode::LineTick entry : entries) { Set(children, index++, - CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), + CreateTimeNode(name, + scriptName, scriptId, NewInteger(entry.line), // V8 14+ (Node.js 25+) added column field to LineTick struct @@ -372,8 +374,8 @@ class TimeProfileTranslator : ProfileTranslator { children = NewArray(count + 1); Set(children, index++, - CreateTimeNode(node->GetFunctionName(), - node->GetScriptResourceName(), + CreateTimeNode(name, + scriptName, scriptId, NewInteger(node->GetLineNumber()), NewInteger(node->GetColumnNumber()), From 0119d10013bd14a4a68b187c8d21c39b9cd32810 Mon Sep 17 00:00:00 2001 From: ishabi Date: Mon, 28 Sep 2026 14:24:09 +0200 Subject: [PATCH 2/2] perf: reuse CPU profile metadata for child nodes --- benchmark/name-caching.js | 11 ++++++++--- bindings/translate-time-profile.cc | 14 +++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/benchmark/name-caching.js b/benchmark/name-caching.js index 3dbf0080..bb17eb9f 100644 --- a/benchmark/name-caching.js +++ b/benchmark/name-caching.js @@ -11,13 +11,14 @@ const work = Number(process.env.WORK || '1000') const lines = Number(process.env.LINES || '32') const iterations = Number(process.env.ITERATIONS || '9') const warmup = Number(process.env.WARMUP || '3') +const nonAsciiNames = process.env.NON_ASCII_NAMES === '1' function compileScript (scriptIndex) { const handlers = [] const calls = [] for (let i = 0; i < functionsPerScript; i++) { - const name = `handler_${scriptIndex}_${i}` + const name = `${nonAsciiNames ? '处理器' : 'handler'}_${scriptIndex}_${i}` const statements = Array.from({ length: lines }, (_, line) => `for (let k = 0; k < ${work}; k++) total += Math.sqrt(k * n + ${i + line})` ) @@ -33,13 +34,13 @@ function compileScript (scriptIndex) { return new Script(` (() => { ${handlers.join('\n')} - return function run_${scriptIndex}(n) { + return function ${nonAsciiNames ? '运行' : 'run'}_${scriptIndex}(n) { let total = 0 ${calls.join('\n')} return total } })() - `, { filename: `/opt/service/dist/modules/module-${scriptIndex}.js` }) + `, { filename: `/opt/${nonAsciiNames ? '服务' : 'service'}/dist/modules/module-${scriptIndex}.js` }) .runInThisContext() } @@ -96,7 +97,11 @@ console.log(JSON.stringify({ scripts: scriptCount, functionsPerScript, lines, + nonAsciiNames, + rounds, + work, iterations, + warmup, medianNodes: median(nodeCounts), medianStopMicros: median(stopMicros), medianNanosPerNode: median(nanosPerNode) diff --git a/bindings/translate-time-profile.cc b/bindings/translate-time-profile.cc index 53b300cf..fb997da2 100644 --- a/bindings/translate-time-profile.cc +++ b/bindings/translate-time-profile.cc @@ -389,17 +389,21 @@ class TimeProfileTranslator : ProfileTranslator { for (int32_t i = 0; i < count; i++) { Set(children, index++, - TranslateLineNumbersTimeProfileNode(node, node->GetChild(i))); + TranslateLineNumbersTimeProfileNode( + name, scriptName, scriptId, node->GetChild(i))); }; return children; } v8::Local TranslateLineNumbersTimeProfileNode( - const v8::CpuProfileNode* parent, const v8::CpuProfileNode* node) { - return CreateTimeNode(parent->GetFunctionName(), - parent->GetScriptResourceName(), - NewInteger(parent->GetScriptId()), + v8::Local name, + v8::Local scriptName, + v8::Local scriptId, + const v8::CpuProfileNode* node) { + return CreateTimeNode(name, + scriptName, + scriptId, NewInteger(node->GetLineNumber()), NewInteger(node->GetColumnNumber()), zero,