From 5ab352da1190e995edc68101d288a162269e7d47 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 17:41:51 +0900
Subject: [PATCH 01/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index a04ee187a5..6ea8a5b5af 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -443,12 +443,10 @@ function renderJsFootprintSection(base, head) {
'',
renderJsFootprintMetricTable(base, head),
'',
- '#### Load Phase Breakdown',
- '',
- renderJsFootprintPhaseTable(base, head),
- '',
- '',
- '',
+ //'#### Load Phase Breakdown',
+ //'',
+ //renderJsFootprintPhaseTable(base, head),
+ //'',
];
for (const block of [
@@ -461,6 +459,9 @@ function renderJsFootprintSection(base, head) {
lines.push('');
}
+ lines.push('');
+ lines.push('');
+
return lines.join('\n');
}
From 8dfa900729145b24578e3d0d65b50a1ba1fdf1ff Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 18:45:58 +0900
Subject: [PATCH 02/11] (test) enhance(dev): improve backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 154 +++++++++++++++
.../measure-backend-memory-comparison.mjs | 46 ++++-
.github/workflows/get-backend-memory.yml | 1 +
packages/backend/scripts/measure-memory.mjs | 184 ++++++++++++++++++
packages/backend/src/boot/entry.ts | 16 ++
5 files changed, 397 insertions(+), 4 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index 6ea8a5b5af..ab26e92233 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -26,6 +26,17 @@ const metrics = [
'External',
];
+const heapSnapshotCategories = [
+ 'Code',
+ 'Strings',
+ 'JS arrays',
+ 'Typed arrays',
+ 'System objects',
+ 'Other JS objects',
+ 'Other non-JS objects',
+ 'Total',
+];
+
function formatNumber(value) {
return numberFormatter.format(value);
}
@@ -273,6 +284,143 @@ function formatPlainDiffPercent(baseValue, headValue) {
return `${sign}${formatPercent(Math.abs((diff * 100) / baseValue))}`;
}
+function getHeapSnapshotCategoryValue(report, phase, category) {
+ const value = report?.[phase]?.heapSnapshot?.categories?.[category];
+ return Number.isFinite(value) ? value : null;
+}
+
+function getHeapSnapshotSampleValues(report, phase, category) {
+ if (!Array.isArray(report?.samples)) return [];
+
+ return report.samples
+ .map(sample => getHeapSnapshotCategoryValue(sample, phase, category))
+ .filter(value => Number.isFinite(value));
+}
+
+function getHeapSnapshotSampleSpread(report, phase, category) {
+ const values = getHeapSnapshotSampleValues(report, phase, category);
+ if (values.length < 2) return null;
+
+ const center = median(values);
+ return median(values.map(value => Math.abs(value - center)));
+}
+
+function formatDiffBytes(baseBytes, headBytes) {
+ const diff = headBytes - baseBytes;
+ if (diff === 0) return formatBytes(0);
+
+ const sign = diff > 0 ? '+' : '-';
+ return formatColoredDiff(`${sign}${formatBytes(Math.abs(diff))}`, diff);
+}
+
+function formatDiffBytesPercent(baseBytes, headBytes) {
+ const diff = headBytes - baseBytes;
+ if (diff === 0) return '0%';
+ if (baseBytes <= 0) return '-';
+
+ const sign = diff > 0 ? '+' : '-';
+ return formatColoredDiff(`${sign}${formatPercent(Math.abs((diff * 100) / baseBytes))}`, diff);
+}
+
+function getPairedHeapSnapshotDeltaValues(base, head, phase, category) {
+ const baseSamplesByRound = getSamplesByRound(base);
+ const headSamplesByRound = getSamplesByRound(head);
+ const values = [];
+
+ for (const [round, baseSample] of baseSamplesByRound) {
+ const headSample = headSamplesByRound.get(round);
+ if (headSample == null) continue;
+
+ const baseValue = getHeapSnapshotCategoryValue(baseSample, phase, category);
+ const headValue = getHeapSnapshotCategoryValue(headSample, phase, category);
+ if (baseValue == null || headValue == null) continue;
+
+ values.push(headValue - baseValue);
+ }
+
+ return values;
+}
+
+function formatDeltaBytes(diffBytes) {
+ if (diffBytes === 0) return formatBytes(0);
+
+ const sign = diffBytes > 0 ? '+' : '-';
+ return formatColoredDiff(`${sign}${formatBytes(Math.abs(diffBytes))}`, diffBytes);
+}
+
+function pairedHeapSnapshotDeltaSummary(base, head, phase, category) {
+ const values = getPairedHeapSnapshotDeltaValues(base, head, phase, category);
+ if (values.length === 0) return null;
+
+ return {
+ median: median(values),
+ mad: mad(values),
+ min: Math.min(...values),
+ max: Math.max(...values),
+ samples: values.length,
+ };
+}
+
+function renderHeapSnapshotTable(base, head, phase) {
+ const lines = [
+ '| Category | Base | Head | Δ | Δ (%) |',
+ '| --- | ---: | ---: | ---: | ---: |',
+ ];
+
+ for (const category of heapSnapshotCategories) {
+ const baseValue = getHeapSnapshotCategoryValue(base, phase, category);
+ const headValue = getHeapSnapshotCategoryValue(head, phase, category);
+ if (baseValue == null || headValue == null) continue;
+
+ const baseSpread = getHeapSnapshotSampleSpread(base, phase, category);
+ const headSpread = getHeapSnapshotSampleSpread(head, phase, category);
+
+ lines.push(`| ${category} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${formatDiffBytes(baseValue, headValue)} | ${formatDiffBytesPercent(baseValue, headValue)} |`);
+ }
+
+ if (lines.length === 2) return null;
+ return lines.join('\n');
+}
+
+function renderHeapSnapshotPairedDeltaTable(base, head, phase) {
+ const lines = [
+ '| Category | Δ median | Δ MAD | Δ min | Δ max |',
+ '| --- | ---: | ---: | ---: | ---: |',
+ ];
+
+ for (const category of heapSnapshotCategories) {
+ const summary = pairedHeapSnapshotDeltaSummary(base, head, phase, category);
+ if (summary == null) continue;
+
+ lines.push(`| ${category} | ${formatDeltaBytes(summary.median)} | ${summary.mad == null ? '-' : formatBytes(summary.mad)} | ${formatDeltaBytes(summary.min)} | ${formatDeltaBytes(summary.max)} |`);
+ }
+
+ if (lines.length === 2) return null;
+ return lines.join('\n');
+}
+
+function renderHeapSnapshotSection(base, head) {
+ const table = renderHeapSnapshotTable(base, head, 'afterRequest');
+ if (table == null) return null;
+
+ const lines = [
+ '### V8 Heap Snapshot Statistics',
+ '',
+ table,
+ '',
+ ];
+
+ const pairedDeltaTable = renderHeapSnapshotPairedDeltaTable(base, head, 'afterRequest');
+ if (pairedDeltaTable != null) {
+ lines.push('#### Paired Delta Summary');
+ lines.push('');
+ lines.push(pairedDeltaTable);
+ lines.push('');
+ }
+
+ return lines.join('\n');
+}
+
function getJsFootprintValue(report, phase, key) {
const value = report?.[phase]?.totals?.[key];
return Number.isFinite(value) ? value : null;
@@ -494,6 +642,12 @@ for (const phase of phases) {
}
}
+const heapSnapshotSection = renderHeapSnapshotSection(base, head);
+if (heapSnapshotSection != null) {
+ lines.push(heapSnapshotSection);
+ lines.push('');
+}
+
const jsFootprintSection = renderJsFootprintSection(baseJsFootprint, headJsFootprint);
if (jsFootprintSection != null) {
lines.push(jsFootprintSection);
diff --git a/.github/scripts/measure-backend-memory-comparison.mjs b/.github/scripts/measure-backend-memory-comparison.mjs
index da543570ac..25b8942572 100644
--- a/.github/scripts/measure-backend-memory-comparison.mjs
+++ b/.github/scripts/measure-backend-memory-comparison.mjs
@@ -9,6 +9,16 @@ import { writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
const phases = ['beforeGc', 'afterGc', 'afterRequest'];
+const heapSnapshotCategories = [
+ 'Code',
+ 'Strings',
+ 'JS arrays',
+ 'Typed arrays',
+ 'System objects',
+ 'Other JS objects',
+ 'Other non-JS objects',
+ 'Total',
+];
const [baseDirArg, headDirArg, baseOutputArg, headOutputArg] = process.argv.slice(2);
@@ -121,6 +131,31 @@ function summarizeSamples(samples) {
if (values.length > 0) summary[phase][key] = median(values);
}
+
+ const heapSnapshotCategoryValues = {};
+ for (const category of heapSnapshotCategories) {
+ const values = samples
+ .map(sample => sample[phase]?.heapSnapshot?.categories?.[category])
+ .filter(value => Number.isFinite(value));
+
+ if (values.length > 0) heapSnapshotCategoryValues[category] = median(values);
+ }
+
+ const heapSnapshotNodeCountValues = {};
+ for (const category of heapSnapshotCategories) {
+ const values = samples
+ .map(sample => sample[phase]?.heapSnapshot?.nodeCounts?.[category])
+ .filter(value => Number.isFinite(value));
+
+ if (values.length > 0) heapSnapshotNodeCountValues[category] = median(values);
+ }
+
+ if (Object.keys(heapSnapshotCategoryValues).length > 0) {
+ summary[phase].heapSnapshot = {
+ categories: heapSnapshotCategoryValues,
+ nodeCounts: heapSnapshotNodeCountValues,
+ };
+ }
}
return summary;
@@ -138,12 +173,15 @@ async function measureRepo(label, repoDir, round, orderIndex) {
});
process.stderr.write(`[${label}] Measuring memory\n`);
+ const measureEnv = {
+ ...process.env,
+ MK_MEMORY_SAMPLE_COUNT: '1',
+ };
+ if (round <= 0) measureEnv.MK_MEMORY_HEAP_SNAPSHOT = '0';
+
const stdout = await run('node', ['packages/backend/scripts/measure-memory.mjs'], {
cwd: repoDir,
- env: {
- ...process.env,
- MK_MEMORY_SAMPLE_COUNT: '1',
- },
+ env: measureEnv,
});
const report = JSON.parse(stdout);
diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/get-backend-memory.yml
index 9f5af707c7..c21c516721 100644
--- a/.github/workflows/get-backend-memory.yml
+++ b/.github/workflows/get-backend-memory.yml
@@ -93,6 +93,7 @@ jobs:
env:
MK_MEMORY_COMPARE_ROUNDS: 5
MK_MEMORY_COMPARE_WARMUP_ROUNDS: 1
+ MK_MEMORY_HEAP_SNAPSHOT: 1
run: node head/.github/scripts/measure-backend-memory-comparison.mjs base head memory-base.json memory-head.json
- name: Measure backend loaded JS footprint
run: |
diff --git a/packages/backend/scripts/measure-memory.mjs b/packages/backend/scripts/measure-memory.mjs
index beefcd7ed0..8d2c1e1e77 100644
--- a/packages/backend/scripts/measure-memory.mjs
+++ b/packages/backend/scripts/measure-memory.mjs
@@ -14,6 +14,7 @@ import { fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
+import { tmpdir } from 'node:os';
import * as http from 'node:http';
import * as fs from 'node:fs/promises';
@@ -30,11 +31,21 @@ function readIntegerEnv(name, defaultValue, min) {
return value;
}
+function readBooleanEnv(name, defaultValue) {
+ const rawValue = process.env[name];
+ if (rawValue == null || rawValue === '') return defaultValue;
+ if (rawValue === '1' || rawValue === 'true') return true;
+ if (rawValue === '0' || rawValue === 'false') return false;
+ throw new Error(`${name} must be one of: 1, 0, true, false`);
+}
+
const SAMPLE_COUNT = readIntegerEnv('MK_MEMORY_SAMPLE_COUNT', 3, 1); // Number of samples to measure
const STARTUP_TIMEOUT = readIntegerEnv('MK_MEMORY_STARTUP_TIMEOUT_MS', 120000, 1); // Timeout for server startup
const MEMORY_SETTLE_TIME = readIntegerEnv('MK_MEMORY_SETTLE_TIME_MS', 10000, 0); // Wait after startup for memory to settle
const IPC_TIMEOUT = readIntegerEnv('MK_MEMORY_IPC_TIMEOUT_MS', 30000, 1); // Timeout for IPC responses
const REQUEST_COUNT = readIntegerEnv('MK_MEMORY_REQUEST_COUNT', 10, 0);
+const HEAP_SNAPSHOT = readBooleanEnv('MK_MEMORY_HEAP_SNAPSHOT', false);
+const HEAP_SNAPSHOT_TIMEOUT = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_TIMEOUT_MS', 120000, 1);
const procStatusKeys = {
VmPeak: 0,
@@ -74,6 +85,45 @@ const memoryKeys = {
const phases = ['beforeGc', 'afterGc', 'afterRequest'];
+const heapSnapshotCategories = [
+ 'Code',
+ 'Strings',
+ 'JS arrays',
+ 'Typed arrays',
+ 'System objects',
+ 'Other JS objects',
+ 'Other non-JS objects',
+ 'Total',
+];
+
+const typedArrayNames = new Set([
+ 'ArrayBuffer',
+ 'SharedArrayBuffer',
+ 'DataView',
+ 'Int8Array',
+ 'Uint8Array',
+ 'Uint8ClampedArray',
+ 'Int16Array',
+ 'Uint16Array',
+ 'Int32Array',
+ 'Uint32Array',
+ 'Float16Array',
+ 'Float32Array',
+ 'Float64Array',
+ 'BigInt64Array',
+ 'BigUint64Array',
+ 'system / JSArrayBufferData',
+]);
+
+const otherJsNodeTypes = new Set([
+ 'object',
+ 'closure',
+ 'regexp',
+ 'number',
+ 'symbol',
+ 'bigint',
+]);
+
function parseMemoryFile(content, keys, path, required) {
const result = {};
for (const key of Object.keys(keys)) {
@@ -91,6 +141,76 @@ function bytesToKiB(value) {
return Math.round(value / 1024);
}
+function createEmptyHeapSnapshotCategoryMap() {
+ return Object.fromEntries(heapSnapshotCategories.map(category => [category, 0]));
+}
+
+function isTypedArrayNode(type, name) {
+ return typedArrayNames.has(name) ||
+ (type === 'native' && (name.includes('ArrayBuffer') || name.includes('TypedArray')));
+}
+
+function isSystemNode(type, name) {
+ return type === 'hidden' ||
+ type === 'synthetic' ||
+ type === 'object shape' ||
+ name.startsWith('system /') ||
+ name.startsWith('(system ');
+}
+
+function classifyHeapSnapshotNode(type, name) {
+ if (type === 'code') return 'Code';
+ if (type === 'string' || type === 'concatenated string' || type === 'sliced string') return 'Strings';
+ if (isTypedArrayNode(type, name)) return 'Typed arrays';
+ if (type === 'array' || (type === 'object' && name === 'Array')) return 'JS arrays';
+ if (isSystemNode(type, name)) return 'System objects';
+ if (otherJsNodeTypes.has(type)) return 'Other JS objects';
+ return 'Other non-JS objects';
+}
+
+function analyzeHeapSnapshot(snapshot) {
+ const meta = snapshot?.snapshot?.meta;
+ const nodes = snapshot?.nodes;
+ const strings = snapshot?.strings;
+ if (meta == null || !Array.isArray(nodes) || !Array.isArray(strings)) {
+ throw new Error('Invalid heap snapshot format');
+ }
+
+ const nodeFields = meta.node_fields;
+ if (!Array.isArray(nodeFields)) throw new Error('Invalid heap snapshot node fields');
+
+ const typeOffset = nodeFields.indexOf('type');
+ const nameOffset = nodeFields.indexOf('name');
+ const selfSizeOffset = nodeFields.indexOf('self_size');
+ if (typeOffset < 0 || nameOffset < 0 || selfSizeOffset < 0) {
+ throw new Error('Heap snapshot is missing required node fields');
+ }
+
+ const nodeTypeNames = meta.node_types?.[typeOffset];
+ if (!Array.isArray(nodeTypeNames)) throw new Error('Invalid heap snapshot node types');
+
+ const fieldCount = nodeFields.length;
+ const categories = createEmptyHeapSnapshotCategoryMap();
+ const nodeCounts = createEmptyHeapSnapshotCategoryMap();
+
+ for (let offset = 0; offset < nodes.length; offset += fieldCount) {
+ const type = nodeTypeNames[nodes[offset + typeOffset]] ?? 'unknown';
+ const name = strings[nodes[offset + nameOffset]] ?? '';
+ const selfSize = nodes[offset + selfSizeOffset] ?? 0;
+ const category = classifyHeapSnapshotNode(type, name);
+
+ categories[category] += selfSize;
+ categories.Total += selfSize;
+ nodeCounts[category]++;
+ nodeCounts.Total++;
+ }
+
+ return {
+ categories,
+ nodeCounts,
+ };
+}
+
async function getMemoryUsage(pid) {
const path = `/proc/${pid}/status`;
const status = await fs.readFile(path, 'utf-8');
@@ -150,6 +270,39 @@ async function getRuntimeMemoryUsage(serverProcess) {
};
}
+async function getHeapSnapshotStatistics(serverProcess) {
+ if (!HEAP_SNAPSHOT) return null;
+
+ const snapshotPath = join(tmpdir(), `misskey-backend-heap-${process.pid}-${serverProcess.pid}-${Date.now()}.heapsnapshot`);
+ const response = waitForMessage(
+ serverProcess,
+ message => message != null && typeof message === 'object' && (message.type === 'heap snapshot' || message.type === 'heap snapshot error'),
+ 'heap snapshot',
+ HEAP_SNAPSHOT_TIMEOUT,
+ );
+
+ serverProcess.send({
+ type: 'heap snapshot',
+ path: snapshotPath,
+ });
+
+ const message = await response;
+ if (message.type === 'heap snapshot error') {
+ throw new Error(`Failed to write heap snapshot: ${message.message}`);
+ }
+
+ const writtenPath = typeof message.path === 'string' ? message.path : snapshotPath;
+
+ try {
+ const snapshot = JSON.parse(await fs.readFile(writtenPath, 'utf-8'));
+ return analyzeHeapSnapshot(snapshot);
+ } finally {
+ await fs.unlink(writtenPath).catch(err => {
+ process.stderr.write(`Failed to delete heap snapshot ${writtenPath}: ${err.message}\n`);
+ });
+ }
+}
+
async function getAllMemoryUsage(serverProcess) {
const pid = serverProcess.pid;
return {
@@ -180,6 +333,31 @@ function summarizeResults(results) {
summary[phase][key] = median(values);
}
}
+
+ const heapSnapshotCategoryValues = {};
+ for (const category of heapSnapshotCategories) {
+ const values = results
+ .map(result => result[phase]?.heapSnapshot?.categories?.[category])
+ .filter(value => Number.isFinite(value));
+
+ if (values.length > 0) heapSnapshotCategoryValues[category] = median(values);
+ }
+
+ const heapSnapshotNodeCountValues = {};
+ for (const category of heapSnapshotCategories) {
+ const values = results
+ .map(result => result[phase]?.heapSnapshot?.nodeCounts?.[category])
+ .filter(value => Number.isFinite(value));
+
+ if (values.length > 0) heapSnapshotNodeCountValues[category] = median(values);
+ }
+
+ if (Object.keys(heapSnapshotCategoryValues).length > 0) {
+ summary[phase].heapSnapshot = {
+ categories: heapSnapshotCategoryValues,
+ nodeCounts: heapSnapshotNodeCountValues,
+ };
+ }
}
return summary;
@@ -290,6 +468,8 @@ async function measureMemory() {
await triggerGc();
const afterRequest = await getAllMemoryUsage(serverProcess);
+ const heapSnapshot = await getHeapSnapshotStatistics(serverProcess);
+ if (heapSnapshot != null) afterRequest.heapSnapshot = heapSnapshot;
// Stop the server
serverProcess.kill('SIGTERM');
@@ -339,6 +519,10 @@ async function main() {
memorySettleTimeMs: MEMORY_SETTLE_TIME,
ipcTimeoutMs: IPC_TIMEOUT,
requestCount: REQUEST_COUNT,
+ heapSnapshot: {
+ enabled: HEAP_SNAPSHOT,
+ timeoutMs: HEAP_SNAPSHOT_TIMEOUT,
+ },
},
...summary,
samples: results,
diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts
index b5f258e9ee..9fcb4d73ca 100644
--- a/packages/backend/src/boot/entry.ts
+++ b/packages/backend/src/boot/entry.ts
@@ -9,6 +9,7 @@
import cluster from 'node:cluster';
import { EventEmitter } from 'node:events';
+import { writeHeapSnapshot } from 'node:v8';
import chalk from 'chalk';
import Xev from 'xev';
import Logger from '@/logger.js';
@@ -106,6 +107,21 @@ process.on('message', msg => {
value: process.memoryUsage(),
});
}
+ } else if (msg != null && typeof msg === 'object' && 'type' in msg && msg.type === 'heap snapshot' && 'path' in msg && typeof msg.path === 'string') {
+ if (process.send != null) {
+ try {
+ const path = writeHeapSnapshot(msg.path);
+ process.send({
+ type: 'heap snapshot',
+ path,
+ });
+ } catch (err) {
+ process.send({
+ type: 'heap snapshot error',
+ message: err instanceof Error ? err.message : String(err),
+ });
+ }
+ }
}
});
From 83319d1cc2564e1df262cd7afafacf98964f111c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 19:20:11 +0900
Subject: [PATCH 03/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 110 +++++-----------------
1 file changed, 22 insertions(+), 88 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index ab26e92233..4a4ad039d8 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -69,23 +69,6 @@ function formatColoredDiff(text, diff) {
return `$\\color{${color}}{\\text{${formatMathText(text).replaceAll('\\%', '\\\\%')}}}$`;
}
-function formatDiff(baseKiB, headKiB) {
- const diff = headKiB - baseKiB;
- if (diff === 0) return formatMemory(0);
-
- const sign = diff > 0 ? '+' : '-';
- return formatColoredDiff(`${sign}${formatMemory(Math.abs(diff))}`, diff);
-}
-
-function formatDiffPercent(baseKiB, headKiB) {
- const diff = headKiB - baseKiB;
- if (diff === 0) return '0%';
- if (baseKiB <= 0) return '-';
-
- const sign = diff > 0 ? '+' : '-';
- return formatColoredDiff(`${sign}${formatPercent(Math.abs((diff * 100) / baseKiB))}`, diff);
-}
-
function getMemoryValue(report, phase, metric) {
const value = report?.[phase]?.[metric];
return Number.isFinite(value) ? value : null;
@@ -159,6 +142,14 @@ function formatDeltaMemory(diffKiB) {
return formatColoredDiff(`${sign}${formatMemory(Math.abs(diffKiB))}`, diffKiB);
}
+function formatDeltaMemoryWithPercent(diffKiB, baseKiB) {
+ if (diffKiB === 0) return `${formatMemory(0)} (0%)`;
+
+ const sign = diffKiB > 0 ? '+' : '-';
+ const percent = baseKiB > 0 ? ` (${sign}${formatPercent(Math.abs((diffKiB * 100) / baseKiB))})` : '';
+ return formatColoredDiff(`${sign}${formatMemory(Math.abs(diffKiB))}${percent}`, diffKiB);
+}
+
function pairedDeltaSummary(base, head, phase, metric) {
const values = getPairedDeltaValues(base, head, phase, metric);
if (values.length === 0) return null;
@@ -174,8 +165,8 @@ function pairedDeltaSummary(base, head, phase, metric) {
function renderTable(base, head, phase) {
const lines = [
- '| Metric | Base | Head | Δ | Δ (%) |',
- '| --- | ---: | ---: | ---: | ---: |',
+ '| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
+ '| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
for (const metric of metrics) {
@@ -185,27 +176,11 @@ function renderTable(base, head, phase) {
const baseSpread = getSampleSpread(base, phase, metric);
const headSpread = getSampleSpread(head, phase, metric);
-
- lines.push(`| ${metric} | ${formatMemory(baseValue)}
± ${formatMemory(baseSpread)} | ${formatMemory(headValue)}
± ${formatMemory(headSpread)} | ${formatDiff(baseValue, headValue)} | ${formatDiffPercent(baseValue, headValue)} |`);
- }
-
- return lines.join('\n');
-}
-
-function renderPairedDeltaTable(base, head, phase) {
- const lines = [
- '| Metric | Δ median | Δ MAD | Δ min | Δ max |',
- '| --- | ---: | ---: | ---: | ---: |',
- ];
-
- for (const metric of metrics) {
const summary = pairedDeltaSummary(base, head, phase, metric);
- if (summary == null) continue;
- lines.push(`| ${metric} | ${formatDeltaMemory(summary.median)} | ${summary.mad == null ? '-' : formatMemory(summary.mad)} | ${formatDeltaMemory(summary.min)} | ${formatDeltaMemory(summary.max)} |`);
+ lines.push(`| ${metric} | ${formatMemory(baseValue)}
± ${formatMemory(baseSpread)} | ${formatMemory(headValue)}
± ${formatMemory(headSpread)} | ${summary == null ? '-' : formatDeltaMemoryWithPercent(summary.median, baseValue)} | ${summary?.mad == null ? '-' : formatMemory(summary.mad)} | ${summary == null ? '-' : formatDeltaMemory(summary.min)} | ${summary == null ? '-' : formatDeltaMemory(summary.max)} |`);
}
- if (lines.length === 2) return null;
return lines.join('\n');
}
@@ -305,23 +280,6 @@ function getHeapSnapshotSampleSpread(report, phase, category) {
return median(values.map(value => Math.abs(value - center)));
}
-function formatDiffBytes(baseBytes, headBytes) {
- const diff = headBytes - baseBytes;
- if (diff === 0) return formatBytes(0);
-
- const sign = diff > 0 ? '+' : '-';
- return formatColoredDiff(`${sign}${formatBytes(Math.abs(diff))}`, diff);
-}
-
-function formatDiffBytesPercent(baseBytes, headBytes) {
- const diff = headBytes - baseBytes;
- if (diff === 0) return '0%';
- if (baseBytes <= 0) return '-';
-
- const sign = diff > 0 ? '+' : '-';
- return formatColoredDiff(`${sign}${formatPercent(Math.abs((diff * 100) / baseBytes))}`, diff);
-}
-
function getPairedHeapSnapshotDeltaValues(base, head, phase, category) {
const baseSamplesByRound = getSamplesByRound(base);
const headSamplesByRound = getSamplesByRound(head);
@@ -348,6 +306,14 @@ function formatDeltaBytes(diffBytes) {
return formatColoredDiff(`${sign}${formatBytes(Math.abs(diffBytes))}`, diffBytes);
}
+function formatDeltaBytesWithPercent(diffBytes, baseBytes) {
+ if (diffBytes === 0) return `${formatBytes(0)} (0%)`;
+
+ const sign = diffBytes > 0 ? '+' : '-';
+ const percent = baseBytes > 0 ? ` (${sign}${formatPercent(Math.abs((diffBytes * 100) / baseBytes))})` : '';
+ return formatColoredDiff(`${sign}${formatBytes(Math.abs(diffBytes))}${percent}`, diffBytes);
+}
+
function pairedHeapSnapshotDeltaSummary(base, head, phase, category) {
const values = getPairedHeapSnapshotDeltaValues(base, head, phase, category);
if (values.length === 0) return null;
@@ -363,8 +329,8 @@ function pairedHeapSnapshotDeltaSummary(base, head, phase, category) {
function renderHeapSnapshotTable(base, head, phase) {
const lines = [
- '| Category | Base | Head | Δ | Δ (%) |',
- '| --- | ---: | ---: | ---: | ---: |',
+ '| Category | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
+ '| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
for (const category of heapSnapshotCategories) {
@@ -374,25 +340,9 @@ function renderHeapSnapshotTable(base, head, phase) {
const baseSpread = getHeapSnapshotSampleSpread(base, phase, category);
const headSpread = getHeapSnapshotSampleSpread(head, phase, category);
-
- lines.push(`| ${category} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${formatDiffBytes(baseValue, headValue)} | ${formatDiffBytesPercent(baseValue, headValue)} |`);
- }
-
- if (lines.length === 2) return null;
- return lines.join('\n');
-}
-
-function renderHeapSnapshotPairedDeltaTable(base, head, phase) {
- const lines = [
- '| Category | Δ median | Δ MAD | Δ min | Δ max |',
- '| --- | ---: | ---: | ---: | ---: |',
- ];
-
- for (const category of heapSnapshotCategories) {
const summary = pairedHeapSnapshotDeltaSummary(base, head, phase, category);
- if (summary == null) continue;
- lines.push(`| ${category} | ${formatDeltaBytes(summary.median)} | ${summary.mad == null ? '-' : formatBytes(summary.mad)} | ${formatDeltaBytes(summary.min)} | ${formatDeltaBytes(summary.max)} |`);
+ lines.push(`| ${category} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${summary == null ? '-' : formatDeltaBytesWithPercent(summary.median, baseValue)} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
}
if (lines.length === 2) return null;
@@ -410,14 +360,6 @@ function renderHeapSnapshotSection(base, head) {
'',
];
- const pairedDeltaTable = renderHeapSnapshotPairedDeltaTable(base, head, 'afterRequest');
- if (pairedDeltaTable != null) {
- lines.push('#### Paired Delta Summary');
- lines.push('');
- lines.push(pairedDeltaTable);
- lines.push('');
- }
-
return lines.join('\n');
}
@@ -632,14 +574,6 @@ for (const phase of phases) {
lines.push(`### ${phase.title}`);
lines.push(renderTable(base, head, phase.key));
lines.push('');
-
- const pairedDeltaTable = renderPairedDeltaTable(base, head, phase.key);
- if (pairedDeltaTable != null) {
- lines.push('#### Paired Delta Summary');
- lines.push('');
- lines.push(pairedDeltaTable);
- lines.push('');
- }
}
const heapSnapshotSection = renderHeapSnapshotSection(base, head);
From 351f878e2c1fa8e96c6b81bdfaa665a50e449dba Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 19:36:55 +0900
Subject: [PATCH 04/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 39 +++++++++++------------
1 file changed, 18 insertions(+), 21 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index 4a4ad039d8..bb30e98da4 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -27,6 +27,7 @@ const metrics = [
];
const heapSnapshotCategories = [
+ 'Total',
'Code',
'Strings',
'JS arrays',
@@ -34,7 +35,6 @@ const heapSnapshotCategories = [
'System objects',
'Other JS objects',
'Other non-JS objects',
- 'Total',
];
function formatNumber(value) {
@@ -56,6 +56,14 @@ function formatPercent(value) {
return `${formatNumber(value)}%`;
}
+function formatDeltaPercent(diff, baseValue) {
+ if (diff === 0) return '0%';
+ if (baseValue <= 0) return '-';
+
+ const sign = diff > 0 ? '+' : '-';
+ return formatColoredDiff(`${sign}${formatPercent(Math.abs((diff * 100) / baseValue))}`, diff);
+}
+
function formatMathText(text) {
return text
.replaceAll('\\', '\\\\')
@@ -142,14 +150,6 @@ function formatDeltaMemory(diffKiB) {
return formatColoredDiff(`${sign}${formatMemory(Math.abs(diffKiB))}`, diffKiB);
}
-function formatDeltaMemoryWithPercent(diffKiB, baseKiB) {
- if (diffKiB === 0) return `${formatMemory(0)} (0%)`;
-
- const sign = diffKiB > 0 ? '+' : '-';
- const percent = baseKiB > 0 ? ` (${sign}${formatPercent(Math.abs((diffKiB * 100) / baseKiB))})` : '';
- return formatColoredDiff(`${sign}${formatMemory(Math.abs(diffKiB))}${percent}`, diffKiB);
-}
-
function pairedDeltaSummary(base, head, phase, metric) {
const values = getPairedDeltaValues(base, head, phase, metric);
if (values.length === 0) return null;
@@ -177,8 +177,9 @@ function renderTable(base, head, phase) {
const baseSpread = getSampleSpread(base, phase, metric);
const headSpread = getSampleSpread(head, phase, metric);
const summary = pairedDeltaSummary(base, head, phase, metric);
+ const deltaMedian = summary == null ? '-' : `${formatDeltaMemory(summary.median)}
${formatDeltaPercent(summary.median, baseValue)}`;
- lines.push(`| ${metric} | ${formatMemory(baseValue)}
± ${formatMemory(baseSpread)} | ${formatMemory(headValue)}
± ${formatMemory(headSpread)} | ${summary == null ? '-' : formatDeltaMemoryWithPercent(summary.median, baseValue)} | ${summary?.mad == null ? '-' : formatMemory(summary.mad)} | ${summary == null ? '-' : formatDeltaMemory(summary.min)} | ${summary == null ? '-' : formatDeltaMemory(summary.max)} |`);
+ lines.push(`| **${metric}** | ${formatMemory(baseValue)}
± ${formatMemory(baseSpread)} | ${formatMemory(headValue)}
± ${formatMemory(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatMemory(summary.mad)} | ${summary == null ? '-' : formatDeltaMemory(summary.min)} | ${summary == null ? '-' : formatDeltaMemory(summary.max)} |`);
}
return lines.join('\n');
@@ -306,14 +307,6 @@ function formatDeltaBytes(diffBytes) {
return formatColoredDiff(`${sign}${formatBytes(Math.abs(diffBytes))}`, diffBytes);
}
-function formatDeltaBytesWithPercent(diffBytes, baseBytes) {
- if (diffBytes === 0) return `${formatBytes(0)} (0%)`;
-
- const sign = diffBytes > 0 ? '+' : '-';
- const percent = baseBytes > 0 ? ` (${sign}${formatPercent(Math.abs((diffBytes * 100) / baseBytes))})` : '';
- return formatColoredDiff(`${sign}${formatBytes(Math.abs(diffBytes))}${percent}`, diffBytes);
-}
-
function pairedHeapSnapshotDeltaSummary(base, head, phase, category) {
const values = getPairedHeapSnapshotDeltaValues(base, head, phase, category);
if (values.length === 0) return null;
@@ -329,7 +322,7 @@ function pairedHeapSnapshotDeltaSummary(base, head, phase, category) {
function renderHeapSnapshotTable(base, head, phase) {
const lines = [
- '| Category | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
+ '| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
@@ -341,8 +334,12 @@ function renderHeapSnapshotTable(base, head, phase) {
const baseSpread = getHeapSnapshotSampleSpread(base, phase, category);
const headSpread = getHeapSnapshotSampleSpread(head, phase, category);
const summary = pairedHeapSnapshotDeltaSummary(base, head, phase, category);
+ const deltaMedian = summary == null ? '-' : `${formatDeltaBytes(summary.median)}
${formatDeltaPercent(summary.median, baseValue)}`;
- lines.push(`| ${category} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${summary == null ? '-' : formatDeltaBytesWithPercent(summary.median, baseValue)} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
+ lines.push(`| **${category}** | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
+ if (category === 'Total') {
+ lines.push('| | | | | | | |');
+ }
}
if (lines.length === 2) return null;
@@ -391,7 +388,7 @@ function renderJsFootprintMetricTable(base, head) {
const headValue = getJsFootprintValue(head, 'afterRequest', key);
if (baseValue == null || headValue == null) continue;
- lines.push(`| ${title} | ${formatter(baseValue)} | ${formatter(headValue)} | ${formatPlainDiff(baseValue, headValue, formatter)} | ${formatPlainDiffPercent(baseValue, headValue)} |`);
+ lines.push(`| **${title}** | ${formatter(baseValue)} | ${formatter(headValue)} | ${formatPlainDiff(baseValue, headValue, formatter)} | ${formatPlainDiffPercent(baseValue, headValue)} |`);
}
return lines.join('\n');
From 7b2790e46df6a023d2ab965971a21fab5b088bc9 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 20:13:35 +0900
Subject: [PATCH 05/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index bb30e98da4..0d2ba26cba 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -8,7 +8,7 @@ if (baseFile == null || headFile == null || outputFile == null) {
}
const numberFormatter = new Intl.NumberFormat('en-US', {
- maximumFractionDigits: 2,
+ maximumFractionDigits: 1,
});
const phases = [
@@ -265,6 +265,14 @@ function getHeapSnapshotCategoryValue(report, phase, category) {
return Number.isFinite(value) ? value : null;
}
+function formatHeapSnapshotCategoryLabel(category, baseValue, headValue, baseTotal, headTotal) {
+ if (category === 'Total' || baseTotal == null || headTotal == null || baseTotal <= 0 || headTotal <= 0) return `**${category}**`;
+
+ const basePercent = formatPercent((baseValue * 100) / baseTotal);
+ const headPercent = formatPercent((headValue * 100) / headTotal);
+ return `**${category}**
${basePercent} → ${headPercent}`;
+}
+
function getHeapSnapshotSampleValues(report, phase, category) {
if (!Array.isArray(report?.samples)) return [];
@@ -325,6 +333,8 @@ function renderHeapSnapshotTable(base, head, phase) {
'| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
];
+ const baseTotal = getHeapSnapshotCategoryValue(base, phase, 'Total');
+ const headTotal = getHeapSnapshotCategoryValue(head, phase, 'Total');
for (const category of heapSnapshotCategories) {
const baseValue = getHeapSnapshotCategoryValue(base, phase, category);
@@ -335,8 +345,9 @@ function renderHeapSnapshotTable(base, head, phase) {
const headSpread = getHeapSnapshotSampleSpread(head, phase, category);
const summary = pairedHeapSnapshotDeltaSummary(base, head, phase, category);
const deltaMedian = summary == null ? '-' : `${formatDeltaBytes(summary.median)}
${formatDeltaPercent(summary.median, baseValue)}`;
+ const categoryLabel = formatHeapSnapshotCategoryLabel(category, baseValue, headValue, baseTotal, headTotal);
- lines.push(`| **${category}** | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
+ lines.push(`| ${categoryLabel} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
if (category === 'Total') {
lines.push('| | | | | | | |');
}
From fdc2f79855bcc5d48ec39cedd8558cf5494f895c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 21:11:19 +0900
Subject: [PATCH 06/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 87 ++++++++++++++++++++++-
1 file changed, 86 insertions(+), 1 deletion(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index 0d2ba26cba..88a352f30c 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -37,6 +37,28 @@ const heapSnapshotCategories = [
'Other non-JS objects',
];
+const heapSnapshotCategoriesColors = {
+ 'Total': 'gray',
+ 'Code': 'orange',
+ 'Strings': 'red',
+ 'JS arrays': 'cyan',
+ 'Typed arrays': 'green',
+ 'System objects': 'yellow',
+ 'Other JS objects': 'violet',
+ 'Other non-JS objects': 'pink',
+};
+
+const heapSnapshotCategoriesColorsHex = {
+ 'Total': '#888888',
+ 'Code': '#f28e2c',
+ 'Strings': '#e15759',
+ 'JS arrays': '#76b7b2',
+ 'Typed arrays': '#59a14f',
+ 'System objects': '#edc949',
+ 'Other JS objects': '#af7aa1',
+ 'Other non-JS objects': '#ff9da7',
+};
+
function formatNumber(value) {
return numberFormatter.format(value);
}
@@ -265,6 +287,60 @@ function getHeapSnapshotCategoryValue(report, phase, category) {
return Number.isFinite(value) ? value : null;
}
+function escapeCsvValue(value) {
+ return `"${String(value).replaceAll('"', '""')}"`;
+}
+
+function renderHeapSnapshotSankey(report, phase, title) {
+ const total = getHeapSnapshotCategoryValue(report, phase, 'Total');
+ if (total == null || total <= 0) return null;
+
+ const categories = heapSnapshotCategories
+ .filter(category => category !== 'Total')
+ .map(category => {
+ const value = getHeapSnapshotCategoryValue(report, phase, category);
+ if (value == null || value <= 0) return null;
+ return {
+ category,
+ value,
+ };
+ })
+ .filter(value => value != null);
+
+ if (categories.length === 0) return null;
+
+ const nodeColors = {
+ [title]: heapSnapshotCategoriesColorsHex.Total,
+ };
+ for (const { category } of categories) {
+ nodeColors[category] = heapSnapshotCategoriesColorsHex[category];
+ }
+
+ const lines = [
+ `${title} heap snapshot composition
`,
+ '',
+ '```mermaid',
+ `%%{init: ${JSON.stringify({
+ sankey: {
+ linkColor: 'target',
+ nodeAlignment: 'left',
+ nodeColors,
+ },
+ })}}%%`,
+ 'sankey',
+ ];
+
+ for (const { category, value } of categories) {
+ lines.push(`${escapeCsvValue(title)},${escapeCsvValue(category)},${value}`);
+ }
+
+ lines.push('```');
+ lines.push('');
+ lines.push(' ');
+
+ return lines.join('\n');
+}
+
function formatHeapSnapshotCategoryLabel(category, baseValue, headValue, baseTotal, headTotal) {
if (category === 'Total' || baseTotal == null || headTotal == null || baseTotal <= 0 || headTotal <= 0) return `**${category}**`;
@@ -347,7 +423,7 @@ function renderHeapSnapshotTable(base, head, phase) {
const deltaMedian = summary == null ? '-' : `${formatDeltaBytes(summary.median)}
${formatDeltaPercent(summary.median, baseValue)}`;
const categoryLabel = formatHeapSnapshotCategoryLabel(category, baseValue, headValue, baseTotal, headTotal);
- lines.push(`| ${categoryLabel} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
+ lines.push(`| $\\color{${heapSnapshotCategoriesColors[category]}}{\\rule{8pt}{8pt}}$ ${categoryLabel} | ${formatBytes(baseValue)}
± ${baseSpread == null ? '-' : formatBytes(baseSpread)} | ${formatBytes(headValue)}
± ${headSpread == null ? '-' : formatBytes(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatBytes(summary.mad)} | ${summary == null ? '-' : formatDeltaBytes(summary.min)} | ${summary == null ? '-' : formatDeltaBytes(summary.max)} |`);
if (category === 'Total') {
lines.push('| | | | | | | |');
}
@@ -368,6 +444,15 @@ function renderHeapSnapshotSection(base, head) {
'',
];
+ for (const graph of [
+ renderHeapSnapshotSankey(base, 'afterRequest', 'Base'),
+ renderHeapSnapshotSankey(head, 'afterRequest', 'Head'),
+ ]) {
+ if (graph == null) continue;
+ lines.push(graph);
+ lines.push('');
+ }
+
return lines.join('\n');
}
From d0081035fc9b1d983c020c2ed7d40d4883f80a97 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 21:28:05 +0900
Subject: [PATCH 07/11] chore(dev): tweak frontend-js-size
---
.github/scripts/frontend-js-size.mjs | 55 +++++++++++++++++++++++++++-
1 file changed, 54 insertions(+), 1 deletion(-)
diff --git a/.github/scripts/frontend-js-size.mjs b/.github/scripts/frontend-js-size.mjs
index ddfc66e51f..f234a41fdb 100644
--- a/.github/scripts/frontend-js-size.mjs
+++ b/.github/scripts/frontend-js-size.mjs
@@ -530,12 +530,61 @@ function renderFrontendBundleReport(before, after) {
return lines.join('\n');
}
+const visualizerTreemapLimit = 20;
+
+function mermaidTreemapLabel(value) {
+ const label = String(value)
+ .replaceAll('\\', '/')
+ .replaceAll('"', "'")
+ .replaceAll('`', "'")
+ .replaceAll('\r', ' ')
+ .replaceAll('\n', ' ')
+ .trim();
+ return label === '' ? '(unknown)' : label;
+}
+
+function renderVisualizerTreemap(label, report) {
+ const rows = report.hotModules
+ .filter((row) => row.renderedLength > 0)
+ .slice(0, visualizerTreemapLimit);
+ const topRendered = rows.reduce((sum, row) => sum + row.renderedLength, 0);
+ const otherRendered = Math.max(0, report.metrics.renderedLength - topRendered);
+ const lines = [
+ '```mermaid',
+ 'treemap-beta',
+ `"${mermaidTreemapLabel(label)}"`,
+ ];
+
+ for (const row of rows) {
+ lines.push(` "${mermaidTreemapLabel(row.id)}": ${Math.round(row.renderedLength)}`);
+ }
+ if (otherRendered > 0) {
+ lines.push(` "Other": ${Math.round(otherRendered)}`);
+ }
+
+ lines.push('```');
+ return lines.join('\n');
+}
+
+function renderVisualizerTreemapDetails(label, report, open = false) {
+ return [
+ ``,
+ `${label} rendered size treemap (top ${visualizerTreemapLimit} + Other)
`,
+ '',
+ renderVisualizerTreemap(label, report),
+ '',
+ ' ',
+ ].join('\n');
+}
+
const args = process.argv.slice(2);
const [beforeDir, afterDir, beforeStatsFile, afterStatsFile, outFile] = args;
const before = await collectReport(beforeDir);
const after = await collectReport(afterDir);
const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8'));
const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8'));
+const beforeVisualizerReport = collectVisualizerReport(beforeStats);
+const afterVisualizerReport = collectVisualizerReport(afterStats);
const visualizerArtifactLink = `[Download detailed HTML](${process.env.FRONTEND_BUNDLE_REPORT_ARTIFACT_URL})`;
const body = [
@@ -547,7 +596,11 @@ const body = [
'',
'## Bundle Stats',
'',
- renderFrontendBundleReport(collectVisualizerReport(beforeStats), collectVisualizerReport(afterStats)),
+ renderFrontendBundleReport(beforeVisualizerReport, afterVisualizerReport),
+ '',
+ renderVisualizerTreemapDetails('Before', beforeVisualizerReport),
+ '',
+ renderVisualizerTreemapDetails('After', afterVisualizerReport, true),
'',
visualizerArtifactLink,
].join('\n');
From 079ec865e0cff451d6aad3cd48a35d0adb421bf8 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 24 Jun 2026 21:39:53 +0900
Subject: [PATCH 08/11] chore(dev): tweak frontend-js-size
---
.github/scripts/frontend-js-size.mjs | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/.github/scripts/frontend-js-size.mjs b/.github/scripts/frontend-js-size.mjs
index f234a41fdb..ffd01d9c56 100644
--- a/.github/scripts/frontend-js-size.mjs
+++ b/.github/scripts/frontend-js-size.mjs
@@ -530,7 +530,7 @@ function renderFrontendBundleReport(before, after) {
return lines.join('\n');
}
-const visualizerTreemapLimit = 20;
+const visualizerTreemapLimit = 30;
function mermaidTreemapLabel(value) {
const label = String(value)
@@ -543,6 +543,13 @@ function mermaidTreemapLabel(value) {
return label === '' ? '(unknown)' : label;
}
+function mermaidTreemapModuleLabel(id) {
+ const normalizedId = String(id).replaceAll('\\', '/');
+ const filePath = normalizedId.split(/[?#]/, 1)[0];
+ const fileName = path.posix.basename(filePath);
+ return mermaidTreemapLabel(fileName || normalizedId);
+}
+
function renderVisualizerTreemap(label, report) {
const rows = report.hotModules
.filter((row) => row.renderedLength > 0)
@@ -556,7 +563,7 @@ function renderVisualizerTreemap(label, report) {
];
for (const row of rows) {
- lines.push(` "${mermaidTreemapLabel(row.id)}": ${Math.round(row.renderedLength)}`);
+ lines.push(` "${mermaidTreemapModuleLabel(row.id)}": ${Math.round(row.renderedLength)}`);
}
if (otherRendered > 0) {
lines.push(` "Other": ${Math.round(otherRendered)}`);
From 453f38b6b6b8e9faf5741cadf418b2a6923f31c4 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 25 Jun 2026 09:02:40 +0900
Subject: [PATCH 09/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 41 +++++-
.../measure-backend-memory-comparison.mjs | 50 ++++++++
packages/backend/scripts/measure-memory.mjs | 120 ++++++++++++++++++
3 files changed, 205 insertions(+), 6 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index 88a352f30c..3a9b2fdddb 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -287,10 +287,25 @@ function getHeapSnapshotCategoryValue(report, phase, category) {
return Number.isFinite(value) ? value : null;
}
+function getHeapSnapshotBreakdownEntries(report, phase, category) {
+ const breakdown = report?.[phase]?.heapSnapshot?.breakdowns?.[category];
+ if (breakdown == null || typeof breakdown !== 'object') return [];
+
+ return Object.entries(breakdown)
+ .filter(([, value]) => Number.isFinite(value) && value > 0)
+ .toSorted((a, b) => b[1] - a[1]);
+}
+
function escapeCsvValue(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
+function formatSankeyValue(value) {
+ const rounded = Math.round(value * 100) / 100;
+ if (Number.isInteger(rounded)) return String(rounded);
+ return rounded.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
+}
+
function renderHeapSnapshotSankey(report, phase, title) {
const total = getHeapSnapshotCategoryValue(report, phase, 'Total');
if (total == null || total <= 0) return null;
@@ -300,9 +315,13 @@ function renderHeapSnapshotSankey(report, phase, title) {
.map(category => {
const value = getHeapSnapshotCategoryValue(report, phase, category);
if (value == null || value <= 0) return null;
+ const breakdownEntries = getHeapSnapshotBreakdownEntries(report, phase, category);
+ const breakdownTotal = breakdownEntries.reduce((sum, [, childValue]) => sum + childValue, 0);
return {
category,
value,
+ breakdownTotal,
+ breakdownEntries,
};
})
.filter(value => value != null);
@@ -312,8 +331,13 @@ function renderHeapSnapshotSankey(report, phase, title) {
const nodeColors = {
[title]: heapSnapshotCategoriesColorsHex.Total,
};
- for (const { category } of categories) {
- nodeColors[category] = heapSnapshotCategoriesColorsHex[category];
+ for (const { category, breakdownEntries } of categories) {
+ const categoryColor = heapSnapshotCategoriesColorsHex[category] ?? heapSnapshotCategoriesColorsHex.Total;
+ nodeColors[category] = categoryColor;
+
+ for (const [childName] of breakdownEntries) {
+ nodeColors[`${category}: ${childName}`] = categoryColor;
+ }
}
const lines = [
@@ -327,11 +351,16 @@ function renderHeapSnapshotSankey(report, phase, title) {
nodeColors,
},
})}}%%`,
- 'sankey',
+ 'sankey-beta',
];
- for (const { category, value } of categories) {
- lines.push(`${escapeCsvValue(title)},${escapeCsvValue(category)},${value}`);
+ for (const { category, value, breakdownTotal, breakdownEntries } of categories) {
+ lines.push(`${escapeCsvValue(title)},${escapeCsvValue(category)},${formatSankeyValue(value)}`);
+
+ for (const [childName, childValue] of breakdownEntries) {
+ const normalizedValue = breakdownTotal > 0 ? childValue * value / breakdownTotal : childValue;
+ lines.push(`${escapeCsvValue(category)},${escapeCsvValue(`${category}: ${childName}`)},${formatSankeyValue(normalizedValue)}`);
+ }
}
lines.push('```');
@@ -653,7 +682,7 @@ const head = JSON.parse(await readFile(headFile, 'utf8'));
const baseJsFootprint = baseJsFootprintFile == null ? null : JSON.parse(await readFile(baseJsFootprintFile, 'utf8'));
const headJsFootprint = headJsFootprintFile == null ? null : JSON.parse(await readFile(headJsFootprintFile, 'utf8'));
const lines = [
- '## Backend Memory Usage Report',
+ '## ⚙️ Backend Memory Usage Report',
'',
];
diff --git a/.github/scripts/measure-backend-memory-comparison.mjs b/.github/scripts/measure-backend-memory-comparison.mjs
index 25b8942572..3857499bdd 100644
--- a/.github/scripts/measure-backend-memory-comparison.mjs
+++ b/.github/scripts/measure-backend-memory-comparison.mjs
@@ -37,6 +37,8 @@ function readIntegerEnv(name, defaultValue, min) {
return value;
}
+const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', 6, 1);
+
function commandName(command) {
if (process.platform !== 'win32') return command;
if (command === 'pnpm') return 'pnpm.cmd';
@@ -111,6 +113,51 @@ function median(values) {
return Math.round((sorted[center - 1] + sorted[center]) / 2);
}
+function summarizeHeapSnapshotBreakdowns(samples, phase) {
+ const breakdowns = {};
+
+ for (const category of heapSnapshotCategories) {
+ if (category === 'Total') continue;
+
+ const childKeys = new Set();
+ for (const sample of samples) {
+ for (const childKey of Object.keys(sample[phase]?.heapSnapshot?.breakdowns?.[category] ?? {})) {
+ childKeys.add(childKey);
+ }
+ }
+
+ const categoryBreakdown = {};
+ for (const childKey of childKeys) {
+ const values = samples
+ .map(sample => sample[phase]?.heapSnapshot?.breakdowns?.[category]?.[childKey])
+ .filter(value => Number.isFinite(value));
+
+ if (values.length > 0) categoryBreakdown[childKey] = median(values);
+ }
+
+ if (Object.keys(categoryBreakdown).length > 0) {
+ breakdowns[category] = collapseHeapSnapshotBreakdown(categoryBreakdown);
+ }
+ }
+
+ return breakdowns;
+}
+
+function collapseHeapSnapshotBreakdown(breakdown) {
+ const entries = Object.entries(breakdown)
+ .filter(([, value]) => value > 0)
+ .toSorted((a, b) => b[1] - a[1]);
+
+ const topEntries = entries.slice(0, HEAP_SNAPSHOT_BREAKDOWN_TOP_N);
+ const otherValue = entries
+ .slice(HEAP_SNAPSHOT_BREAKDOWN_TOP_N)
+ .reduce((sum, [, value]) => sum + value, 0);
+
+ const collapsed = Object.fromEntries(topEntries);
+ if (otherValue > 0) collapsed.Other = otherValue;
+ return collapsed;
+}
+
function summarizeSamples(samples) {
const summary = {};
@@ -151,9 +198,12 @@ function summarizeSamples(samples) {
}
if (Object.keys(heapSnapshotCategoryValues).length > 0) {
+ const heapSnapshotBreakdowns = summarizeHeapSnapshotBreakdowns(samples, phase);
+
summary[phase].heapSnapshot = {
categories: heapSnapshotCategoryValues,
nodeCounts: heapSnapshotNodeCountValues,
+ ...(Object.keys(heapSnapshotBreakdowns).length > 0 ? { breakdowns: heapSnapshotBreakdowns } : {}),
};
}
}
diff --git a/packages/backend/scripts/measure-memory.mjs b/packages/backend/scripts/measure-memory.mjs
index 8d2c1e1e77..b3a7056e1a 100644
--- a/packages/backend/scripts/measure-memory.mjs
+++ b/packages/backend/scripts/measure-memory.mjs
@@ -46,6 +46,7 @@ const IPC_TIMEOUT = readIntegerEnv('MK_MEMORY_IPC_TIMEOUT_MS', 30000, 1); // Tim
const REQUEST_COUNT = readIntegerEnv('MK_MEMORY_REQUEST_COUNT', 10, 0);
const HEAP_SNAPSHOT = readBooleanEnv('MK_MEMORY_HEAP_SNAPSHOT', false);
const HEAP_SNAPSHOT_TIMEOUT = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_TIMEOUT_MS', 120000, 1);
+const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', 6, 1);
const procStatusKeys = {
VmPeak: 0,
@@ -168,6 +169,84 @@ function classifyHeapSnapshotNode(type, name) {
return 'Other non-JS objects';
}
+function addValue(map, key, value) {
+ map[key] = (map[key] ?? 0) + value;
+}
+
+function sanitizeHeapSnapshotBreakdownLabel(value, fallback = 'unknown') {
+ const label = String(value ?? '').replace(/\s+/g, ' ').trim();
+ if (label === '') return fallback;
+ if (label.length <= 80) return label;
+ return `${label.slice(0, 77)}...`;
+}
+
+function classifyHeapSnapshotBreakdown(category, type, name) {
+ if (category === 'Strings') return type;
+
+ if (category === 'JS arrays') {
+ if (type === 'array') return 'array nodes';
+ if (type === 'object' && name === 'Array') return 'Array objects';
+ return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`);
+ }
+
+ if (category === 'Typed arrays') {
+ if (name === 'system / JSArrayBufferData') return 'ArrayBuffer data';
+ if (name === 'Uint8Array') return 'Uint8Array / Buffer';
+ if (typedArrayNames.has(name)) return name;
+ if (type === 'native' && name.includes('ArrayBuffer')) return 'native ArrayBuffer';
+ if (type === 'native' && name.includes('TypedArray')) return 'native TypedArray';
+ return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`);
+ }
+
+ if (category === 'System objects') {
+ if (name.startsWith('system /')) return sanitizeHeapSnapshotBreakdownLabel(name);
+ if (name.startsWith('(system ')) return sanitizeHeapSnapshotBreakdownLabel(name);
+ return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`, type);
+ }
+
+ if (category === 'Other JS objects') {
+ if (type === 'object') return sanitizeHeapSnapshotBreakdownLabel(`object: ${name}`, 'object: unknown');
+ return type;
+ }
+
+ if (category === 'Other non-JS objects') {
+ if (type === 'native') return sanitizeHeapSnapshotBreakdownLabel(`native: ${name}`, 'native: unknown');
+ return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`, type);
+ }
+
+ if (category === 'Code') {
+ const lowerName = name.toLowerCase();
+ if (lowerName.includes('bytecode')) return 'bytecode';
+ if (lowerName.includes('builtin')) return 'builtins';
+ if (lowerName.includes('regexp')) return 'regexp code';
+ if (lowerName.includes('stub')) return 'stubs';
+ return sanitizeHeapSnapshotBreakdownLabel(`code: ${name}`, 'code: unknown');
+ }
+
+ return sanitizeHeapSnapshotBreakdownLabel(`${type}: ${name}`, type);
+}
+
+function collapseHeapSnapshotBreakdown(breakdowns) {
+ const collapsed = {};
+
+ for (const [category, children] of Object.entries(breakdowns)) {
+ const entries = Object.entries(children)
+ .filter(([, value]) => value > 0)
+ .toSorted((a, b) => b[1] - a[1]);
+
+ const topEntries = entries.slice(0, HEAP_SNAPSHOT_BREAKDOWN_TOP_N);
+ const otherValue = entries
+ .slice(HEAP_SNAPSHOT_BREAKDOWN_TOP_N)
+ .reduce((sum, [, value]) => sum + value, 0);
+
+ const categoryBreakdown = Object.fromEntries(topEntries);
+ if (otherValue > 0) categoryBreakdown.Other = otherValue;
+ if (Object.keys(categoryBreakdown).length > 0) collapsed[category] = categoryBreakdown;
+ }
+
+ return collapsed;
+}
+
function analyzeHeapSnapshot(snapshot) {
const meta = snapshot?.snapshot?.meta;
const nodes = snapshot?.nodes;
@@ -192,6 +271,11 @@ function analyzeHeapSnapshot(snapshot) {
const fieldCount = nodeFields.length;
const categories = createEmptyHeapSnapshotCategoryMap();
const nodeCounts = createEmptyHeapSnapshotCategoryMap();
+ const breakdowns = Object.fromEntries(
+ heapSnapshotCategories
+ .filter(category => category !== 'Total')
+ .map(category => [category, {}]),
+ );
for (let offset = 0; offset < nodes.length; offset += fieldCount) {
const type = nodeTypeNames[nodes[offset + typeOffset]] ?? 'unknown';
@@ -203,11 +287,13 @@ function analyzeHeapSnapshot(snapshot) {
categories.Total += selfSize;
nodeCounts[category]++;
nodeCounts.Total++;
+ addValue(breakdowns[category], classifyHeapSnapshotBreakdown(category, type, name), selfSize);
}
return {
categories,
nodeCounts,
+ breakdowns: collapseHeapSnapshotBreakdown(breakdowns),
};
}
@@ -319,6 +405,36 @@ function median(values) {
return Math.round((sorted[center - 1] + sorted[center]) / 2);
}
+function summarizeHeapSnapshotBreakdowns(results, phase) {
+ const breakdowns = {};
+
+ for (const category of heapSnapshotCategories) {
+ if (category === 'Total') continue;
+
+ const childKeys = new Set();
+ for (const result of results) {
+ for (const childKey of Object.keys(result[phase]?.heapSnapshot?.breakdowns?.[category] ?? {})) {
+ childKeys.add(childKey);
+ }
+ }
+
+ const categoryBreakdown = {};
+ for (const childKey of childKeys) {
+ const values = results
+ .map(result => result[phase]?.heapSnapshot?.breakdowns?.[category]?.[childKey])
+ .filter(value => Number.isFinite(value));
+
+ if (values.length > 0) categoryBreakdown[childKey] = median(values);
+ }
+
+ if (Object.keys(categoryBreakdown).length > 0) {
+ breakdowns[category] = collapseHeapSnapshotBreakdown({ [category]: categoryBreakdown })[category] ?? categoryBreakdown;
+ }
+ }
+
+ return breakdowns;
+}
+
function summarizeResults(results) {
const summary = {};
@@ -353,9 +469,12 @@ function summarizeResults(results) {
}
if (Object.keys(heapSnapshotCategoryValues).length > 0) {
+ const heapSnapshotBreakdowns = summarizeHeapSnapshotBreakdowns(results, phase);
+
summary[phase].heapSnapshot = {
categories: heapSnapshotCategoryValues,
nodeCounts: heapSnapshotNodeCountValues,
+ ...(Object.keys(heapSnapshotBreakdowns).length > 0 ? { breakdowns: heapSnapshotBreakdowns } : {}),
};
}
}
@@ -522,6 +641,7 @@ async function main() {
heapSnapshot: {
enabled: HEAP_SNAPSHOT,
timeoutMs: HEAP_SNAPSHOT_TIMEOUT,
+ breakdownTopN: HEAP_SNAPSHOT_BREAKDOWN_TOP_N,
},
},
...summary,
From 0904855001b2f2d8fa8cfd764c1c03e7c175feaa Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 25 Jun 2026 10:14:08 +0900
Subject: [PATCH 10/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 50 +++++++++++++++++------
1 file changed, 38 insertions(+), 12 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index 3a9b2fdddb..b6d48a5bef 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -296,16 +296,24 @@ function getHeapSnapshotBreakdownEntries(report, phase, category) {
.toSorted((a, b) => b[1] - a[1]);
}
+const heapSnapshotSankeyChildMinRatio = 0.3;
+const heapSnapshotSankeyParentMinPercent = 10;
+
function escapeCsvValue(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
-function formatSankeyValue(value) {
+function formatSankeyPercentValue(value) {
const rounded = Math.round(value * 100) / 100;
+ if (rounded === 0 && value > 0) return '0.01';
if (Number.isInteger(rounded)) return String(rounded);
return rounded.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
}
+function formatHeapSnapshotSankeyChildLabel(label) {
+ return String(label).replace(/^[^:]+:\s*/, '');
+}
+
function renderHeapSnapshotSankey(report, phase, title) {
const total = getHeapSnapshotCategoryValue(report, phase, 'Total');
if (total == null || total <= 0) return null;
@@ -317,11 +325,30 @@ function renderHeapSnapshotSankey(report, phase, title) {
if (value == null || value <= 0) return null;
const breakdownEntries = getHeapSnapshotBreakdownEntries(report, phase, category);
const breakdownTotal = breakdownEntries.reduce((sum, [, childValue]) => sum + childValue, 0);
+ const percent = (value * 100) / total;
+ const childEntries = [];
+ let otherPercent = 0;
+
+ if (breakdownTotal > 0 && percent > heapSnapshotSankeyParentMinPercent) {
+ for (const [childName, childValue] of breakdownEntries) {
+ const childRatio = childValue / breakdownTotal;
+ const childPercent = percent * childRatio;
+ if (childRatio >= heapSnapshotSankeyChildMinRatio) {
+ childEntries.push([formatHeapSnapshotSankeyChildLabel(childName), childPercent]);
+ } else {
+ otherPercent += childPercent;
+ }
+ }
+
+ if (childEntries.length > 0 && otherPercent > 0) {
+ childEntries.push(['Other', otherPercent]);
+ }
+ }
+
return {
category,
- value,
- breakdownTotal,
- breakdownEntries,
+ percent,
+ childEntries,
};
})
.filter(value => value != null);
@@ -331,12 +358,12 @@ function renderHeapSnapshotSankey(report, phase, title) {
const nodeColors = {
[title]: heapSnapshotCategoriesColorsHex.Total,
};
- for (const { category, breakdownEntries } of categories) {
+ for (const { category, childEntries } of categories) {
const categoryColor = heapSnapshotCategoriesColorsHex[category] ?? heapSnapshotCategoriesColorsHex.Total;
nodeColors[category] = categoryColor;
- for (const [childName] of breakdownEntries) {
- nodeColors[`${category}: ${childName}`] = categoryColor;
+ for (const [childName] of childEntries) {
+ nodeColors[childName] = categoryColor;
}
}
@@ -354,12 +381,11 @@ function renderHeapSnapshotSankey(report, phase, title) {
'sankey-beta',
];
- for (const { category, value, breakdownTotal, breakdownEntries } of categories) {
- lines.push(`${escapeCsvValue(title)},${escapeCsvValue(category)},${formatSankeyValue(value)}`);
+ for (const { category, percent, childEntries } of categories) {
+ lines.push(`${escapeCsvValue(title)},${escapeCsvValue(category)},${formatSankeyPercentValue(percent)}`);
- for (const [childName, childValue] of breakdownEntries) {
- const normalizedValue = breakdownTotal > 0 ? childValue * value / breakdownTotal : childValue;
- lines.push(`${escapeCsvValue(category)},${escapeCsvValue(`${category}: ${childName}`)},${formatSankeyValue(normalizedValue)}`);
+ for (const [childName, childPercent] of childEntries) {
+ lines.push(`${escapeCsvValue(category)},${escapeCsvValue(childName)},${formatSankeyPercentValue(childPercent)}`);
}
}
From 2116213dc89552673b6662be5249504364148b6b Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 25 Jun 2026 10:40:50 +0900
Subject: [PATCH 11/11] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mjs | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs
index b6d48a5bef..2aeaf24733 100644
--- a/.github/scripts/backend-memory-report.mjs
+++ b/.github/scripts/backend-memory-report.mjs
@@ -373,9 +373,15 @@ function renderHeapSnapshotSankey(report, phase, title) {
'```mermaid',
`%%{init: ${JSON.stringify({
sankey: {
+ showValues: false,
linkColor: 'target',
- nodeAlignment: 'left',
- nodeColors,
+ labelStyle: 'outlined',
+ nodeAlignment: 'center',
+ nodePadding: 10,
+ nodeColors: {
+ ...nodeColors,
+ 'Other': '#888888',
+ },
},
})}}%%`,
'sankey-beta',