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] 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,