From 212fcaf254352e8e682758044e4e18f93fb207ad Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:44:49 +0900 Subject: [PATCH] enhance(dev): tweak get-backend-memory --- .github/scripts/backend-memory-report.mts | 46 ++++++++++----------- packages/backend/scripts/measure-memory.mts | 8 ++-- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/.github/scripts/backend-memory-report.mts b/.github/scripts/backend-memory-report.mts index 9aae10d887..fc64cb6db5 100644 --- a/.github/scripts/backend-memory-report.mts +++ b/.github/scripts/backend-memory-report.mts @@ -47,8 +47,7 @@ const memoryReportPhases = [ const metrics = [ 'HeapUsed', 'Pss', - 'Private_Dirty', - 'VmRSS', + 'USS', 'External', ] as const; @@ -57,16 +56,27 @@ function formatMemoryMb(valueKiB: number | null | undefined) { return `${util.formatNumber(valueKiB / 1000)} MB`; } -function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { - return report.summary[phase].memoryUsage[metric]; +function formatMetricName(metric: typeof metrics[number]) { + return metric === 'Pss' ? 'PSS' : metric; } function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { - return sample.phases[phase].memoryUsage[metric]; + const memoryUsage = sample.phases[phase].memoryUsage; + if (metric !== 'USS') return memoryUsage[metric]; + return memoryUsage.Private_Clean + memoryUsage.Private_Dirty; +} + +function getSampleValues(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { + return report.samples.map(sample => getMemoryValueFromSample(sample, phase, metric)); +} + +function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { + if (metric !== 'USS') return report.summary[phase].memoryUsage[metric]; + return util.median(getSampleValues(report, phase, metric)); } function getSampleSpread(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { - const values = report.samples.map(sample => getMemoryValueFromSample(sample, phase, metric)); + const values = getSampleValues(report, phase, metric); if (values.length < 2) return null; const center = util.median(values); @@ -91,9 +101,9 @@ function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase: const headSpread = getSampleSpread(head, phase, metric); const summary = util.pairedDeltaSummary(base.samples, head.samples, (sample) => getMemoryValueFromSample(sample, phase, metric)); const percent = summary.median * 100 / baseValue; - const deltaMedian = summary == null ? '-' : `${formatDeltaMemory(summary.median)}
${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`; + const deltaMedian = `${formatDeltaMemory(summary.median)}
${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`; - lines.push(`| **${metric}** | ${formatMemoryMb(baseValue)}
± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)}
± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${summary?.mad == null ? '-' : formatMemoryMb(summary.mad)} | ${summary == null ? '-' : formatDeltaMemory(summary.min)} | ${summary == null ? '-' : formatDeltaMemory(summary.max)} |`); + lines.push(`| **${formatMetricName(metric)}** | ${formatMemoryMb(baseValue)}
± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)}
± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${formatMemoryMb(summary.mad)} | ${formatDeltaMemory(summary.min)} | ${formatDeltaMemory(summary.max)} |`); } return lines.join('\n'); @@ -384,27 +394,15 @@ if (jsFootprintSection != null) { lines.push(''); } -function getWarningMetric(base: MemoryReport, head: MemoryReport) { - for (const metric of ['Pss', 'Private_Dirty', 'VmRSS'] as const) { - if (getMemoryValue(base, 'afterGc', metric) != null && getMemoryValue(head, 'afterGc', metric) != null) { - return metric; - } - } - return null; -} - function getDiffPercent(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { const baseValue = getMemoryValue(base, phase, metric); const headValue = getMemoryValue(head, phase, metric); - if (baseValue == null || headValue == null || baseValue <= 0) return null; - return ((headValue - baseValue) * 100) / baseValue; } function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { const baseValue = getMemoryValue(base, phase, metric); const headValue = getMemoryValue(head, phase, metric); - if (baseValue == null || headValue == null) return false; const delta = headValue - baseValue; if (delta <= 0) return false; @@ -417,10 +415,10 @@ function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: type return delta > combinedSpread * 3; } -const warningMetric = getWarningMetric(base, head); -const warningDiffPercent = warningMetric == null ? null : getDiffPercent(base, head, 'afterGc', warningMetric); -if (warningMetric != null && warningDiffPercent != null && warningDiffPercent > 5 && isBeyondSampleNoise(base, head, 'afterGc', warningMetric)) { - lines.push(`⚠️ **Warning**: Memory usage (${warningMetric}) has increased by more than 5% and exceeds the observed sample noise. Please verify this is not an unintended change.`); +const warningMetric = 'Pss'; +const warningDiffPercent = getDiffPercent(base, head, 'afterGc', warningMetric); +if (warningDiffPercent > 5 && isBeyondSampleNoise(base, head, 'afterGc', warningMetric)) { + lines.push(`⚠️ **Warning**: Memory usage (${formatMetricName(warningMetric)}) has increased by more than 5% and exceeds the observed sample noise. Please verify this is not an unintended change.`); lines.push(''); } diff --git a/packages/backend/scripts/measure-memory.mts b/packages/backend/scripts/measure-memory.mts index 55aab61a84..a95eb3075c 100644 --- a/packages/backend/scripts/measure-memory.mts +++ b/packages/backend/scripts/measure-memory.mts @@ -57,14 +57,14 @@ type HeapSnapshotErrorMessage = { }; type HeapSnapshotResponseMessage = HeapSnapshotMessage | HeapSnapshotErrorMessage; -function parseMemoryFile(content: string, keys: KS, path: string, required: boolean): Record { +function parseMemoryFile(content: string, keys: KS, path: string): Record { const result = {} as Record; for (const _key of keys) { const key = _key as KS[number]; const match = content.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`)); if (match) { result[key] = parseInt(match[1], 10); - } else if (required) { + } else { throw new Error(`Failed to parse ${key} from ${path}`); } } @@ -78,13 +78,13 @@ function bytesToKiB(value: number) { async function getMemoryUsage(pid: number) { const path = `/proc/${pid}/status`; const status = await fs.readFile(path, 'utf-8'); - return parseMemoryFile(status, procStatusKeys, path, true); + return parseMemoryFile(status, procStatusKeys, path); } async function getSmapsRollupMemoryUsage(pid: number) { const path = `/proc/${pid}/smaps_rollup`; const smapsRollup = await fs.readFile(path, 'utf-8'); - return parseMemoryFile(smapsRollup, smapsRollupKeys, path, false); + return parseMemoryFile(smapsRollup, smapsRollupKeys, path); } function isRecord(value: unknown): value is Record {