From 12be1fe5b5002d195e4817c888ee052cbc5a1ea7 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 16 Jul 2026 20:23:01 +0900
Subject: [PATCH] [skip ci] chore(dev): tweak backend-memory-report
---
.github/scripts/backend-memory-report.mts | 277 ++--------------------
1 file changed, 16 insertions(+), 261 deletions(-)
diff --git a/.github/scripts/backend-memory-report.mts b/.github/scripts/backend-memory-report.mts
index 4a701655ec..85630f87b2 100644
--- a/.github/scripts/backend-memory-report.mts
+++ b/.github/scripts/backend-memory-report.mts
@@ -8,34 +8,7 @@ import * as util from './utility.mts';
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
import type { MemoryReport } from './measure-backend-memory-comparison.mts';
-const [baseFile, headFile, outputFile, baseJsFootprintFile, headJsFootprintFile] = process.argv.slice(2);
-
-type RuntimeLoadedJsFootprintReport = {
- phases: Record<'afterRequest', {
- totals: {
- loadedJsModules: number;
- loadedJsSourceBytes: number;
- loadedJsGzipBytes: number;
- astNodeCount: number;
- functionCount: number;
- classCount: number;
- stringLiteralBytes: number;
- externalPackageCount: number;
- nativeAddonPackageCount: number;
- };
- modules: {
- path: string;
- package: string;
- category: string;
- sourceBytes: number;
- gzipBytes: number;
- astNodeCount: number;
- functionCount: number;
- classCount: number;
- stringLiteralBytes: number;
- }[];
- }>;
-};
+const [baseFile, headFile, outputFile] = process.argv.slice(2);
const memoryReportPhases = [
{
@@ -44,7 +17,7 @@ const memoryReportPhases = [
},
] as const;
-const metrics = [
+const memoryMetrics = [
'HeapUsed',
'Pss',
'USS',
@@ -56,26 +29,26 @@ function formatMemoryMb(valueKiB: number | null | undefined) {
return `${util.formatNumber(valueKiB / 1000)} MB`;
}
-function formatMetricName(metric: typeof metrics[number]) {
+function formatMemoryMetricName(metric: typeof memoryMetrics[number]) {
return metric === 'Pss' ? 'PSS' : metric;
}
-function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
+function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
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]) {
+function getSampleValues(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
return report.samples.map(sample => getMemoryValueFromSample(sample, phase, metric));
}
-function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
+function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[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]) {
+function getSampleSpread(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
const values = getSampleValues(report, phase, metric);
if (values.length < 2) return null;
@@ -93,7 +66,7 @@ function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase:
return util.formatColoredDelta(deltaKiB, v => formatMemoryMb(v), 100); // 0.1 MB threshold
}
- for (const metric of metrics) {
+ for (const metric of memoryMetrics) {
const baseValue = getMemoryValue(base, phase, metric);
const headValue = getMemoryValue(head, phase, metric);
@@ -103,29 +76,12 @@ function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase:
const percent = summary.median * 100 / baseValue;
const deltaMedian = `${formatDeltaMemory(summary.median)}
${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
- lines.push(`| **${formatMetricName(metric)}** | ${formatMemoryMb(baseValue)}
± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)}
± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${formatMemoryMb(summary.mad)} | ${formatDeltaMemory(summary.min)} | ${formatDeltaMemory(summary.max)} |`);
+ lines.push(`| **${formatMemoryMetricName(metric)}** | ${formatMemoryMb(baseValue)}
± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)}
± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${formatMemoryMb(summary.mad)} | ${formatDeltaMemory(summary.min)} | ${formatDeltaMemory(summary.max)} |`);
}
return lines.join('\n');
}
-/*
-function measurementSummary(base, head) {
- const baseCount = base?.sampleCount;
- const headCount = head?.sampleCount;
- const strategy = base?.comparison?.strategy;
- if (baseCount == null || headCount == null) return null;
-
- if (strategy === 'interleaved-pairs') {
- const rounds = base?.comparison?.rounds ?? baseCount;
- const warmupRounds = base?.comparison?.warmupRounds ?? 0;
- return `_Measured as ${rounds} interleaved base/head pairs after ${warmupRounds} warmup pair(s). Values are medians; ± is median absolute deviation._`;
- }
-
- return `_Sample count: base ${baseCount}, head ${headCount}. Values are medians; ± is median absolute deviation._`;
-}
-*/
-
function renderHeapSnapshotSection(base: MemoryReport, head: MemoryReport) {
const baseHeapSnapshotReport = {
summary: base.summary.afterGc.heapSnapshot!,
@@ -165,204 +121,11 @@ function renderHeapSnapshotSection(base: MemoryReport, head: MemoryReport) {
return lines.join('\n');
}
-function getJsFootprintValue(report: RuntimeLoadedJsFootprintReport, phase: 'afterRequest', key: keyof RuntimeLoadedJsFootprintReport['phases'][typeof phase]['totals']) {
- const value = report.phases[phase].totals[key];
- return Number.isFinite(value) ? value : null;
-}
-
-function renderJsFootprintMetricTable(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
- const metricRows = [
- ['Loaded JS modules', 'loadedJsModules', util.formatNumber],
- ['Loaded JS source', 'loadedJsSourceBytes', util.formatBytes],
- //['Loaded JS gzip estimate', 'loadedJsGzipBytes', util.formatBytes],
- //['AST nodes', 'astNodeCount', util.formatNumber],
- //['Functions', 'functionCount', util.formatNumber],
- //['Classes', 'classCount', util.formatNumber],
- //['String literals', 'stringLiteralBytes', util.formatBytes],
- ['External packages loaded', 'externalPackageCount', util.formatNumber],
- ['Native addon packages', 'nativeAddonPackageCount', util.formatNumber],
- ] as const;
-
- const lines = [
- '| Metric | Base | Head | Δ | Δ (%) |',
- '| --- | ---: | ---: | ---: | ---: |',
- ];
-
- for (const [title, key, formatter] of metricRows) {
- const baseValue = getJsFootprintValue(base, 'afterRequest', key);
- const headValue = getJsFootprintValue(head, 'afterRequest', key);
- if (baseValue == null || headValue == null) continue;
-
- lines.push(`| **${title}** | ${formatter(baseValue)} | ${formatter(headValue)} | ${util.formatColoredDelta(headValue - baseValue, v => formatter(v))} | ${util.calcAndFormatDeltaPercent(baseValue, headValue).replaceAll('\\%', '\\\\%')} |`);
- }
-
- return lines.join('\n');
-}
-
-/*
-function renderJsFootprintPhaseTable(base, head) {
- const lines = [
- '| Phase | Base modules | Head modules | Δ modules | Base source | Head source | Δ source |',
- '| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
- ];
-
- for (const [phase, title] of [['startup', 'Startup'], ['afterRequest', 'After warmup requests']]) {
- const baseModules = getJsFootprintValue(base, phase, 'loadedJsModules');
- const headModules = getJsFootprintValue(head, phase, 'loadedJsModules');
- const baseSource = getJsFootprintValue(base, phase, 'loadedJsSourceBytes');
- const headSource = getJsFootprintValue(head, phase, 'loadedJsSourceBytes');
- if (baseModules == null || headModules == null || baseSource == null || headSource == null) continue;
-
- lines.push(`| ${title} | ${util.formatNumber(baseModules)} | ${util.formatNumber(headModules)} | ${formatPlainDelta(baseModules, headModules)} | ${util.formatBytes(baseSource)} | ${util.formatBytes(headSource)} | ${formatPlainDelta(baseSource, headSource, util.formatBytes)} |`);
- }
-
- return lines.join('\n');
-}
-*/
-
-function packageMap(report: RuntimeLoadedJsFootprintReport) {
- const map = new Map();
- for (const packageSummary of report.phases.afterRequest.packages) {
- if (packageSummary?.category !== 'external' || typeof packageSummary.name !== 'string') continue;
- map.set(packageSummary.name, packageSummary);
- }
- return map;
-}
-
-function packageDisplayName(packageSummary: { name: string; version?: string | null }) {
- if (packageSummary.version == null) return packageSummary.name;
- return `${packageSummary.name} ${packageSummary.version}`;
-}
-
-function renderNewExternalPackages(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
- const basePackages = packageMap(base);
- const headPackages = packageMap(head);
- const newPackages = [...headPackages.values()]
- .filter(packageSummary => !basePackages.has(packageSummary.name))
- .toSorted((a, b) => b.sourceBytes - a.sourceBytes)
- .slice(0, 10);
-
- if (newPackages.length === 0) return null;
-
- const lines = [
- '#### Newly Loaded External Packages',
- '',
- '| Package | Loaded JS | Modules | Notes |',
- '| --- | ---: | ---: | --- |',
- ];
-
- for (const packageSummary of newPackages) {
- lines.push(`| ${packageDisplayName(packageSummary)} | ${util.formatBytes(packageSummary.sourceBytes)} | ${util.formatNumber(packageSummary.modules)} | ${packageSummary.nativeAddon ? 'native addon' : ''} |`);
- }
-
- return lines.join('\n');
-}
-
-function renderLargestPackageIncreases(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
- const basePackages = packageMap(base);
- const headPackages = packageMap(head);
- const increases = [...headPackages.values()]
- .map(headPackage => {
- const basePackage = basePackages.get(headPackage.name);
- const baseSourceBytes = basePackage?.sourceBytes ?? 0;
- const baseModules = basePackage?.modules ?? 0;
- return {
- ...headPackage,
- baseSourceBytes,
- baseModules,
- sourceDiff: headPackage.sourceBytes - baseSourceBytes,
- moduleDiff: headPackage.modules - baseModules,
- };
- })
- .filter(packageSummary => packageSummary.sourceDiff > 0)
- .toSorted((a, b) => b.sourceDiff - a.sourceDiff)
- .slice(0, 10);
-
- if (increases.length === 0) return null;
-
- const lines = [
- '#### Largest Package Increases',
- '',
- '| Package | Base | Head | Δ | Modules Δ |',
- '| --- | ---: | ---: | ---: | ---: |',
- ];
-
- for (const packageSummary of increases) {
- lines.push(`| ${packageDisplayName(packageSummary)} | ${util.formatBytes(packageSummary.baseSourceBytes)} | ${util.formatBytes(packageSummary.sourceBytes)} | ${util.formatColoredDelta(packageSummary.sourceBytes - packageSummary.baseSourceBytes, v => util.formatBytes(v))} | ${util.formatColoredDelta(packageSummary.modules - packageSummary.baseModules, v => util.formatNumber(v))} |`);
- }
-
- return lines.join('\n');
-}
-
-function renderNewLoadedModules(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
- function moduleMap(report: RuntimeLoadedJsFootprintReport) {
- const map = new Map();
- for (const moduleSummary of report.phases.afterRequest.modules) {
- if (typeof moduleSummary.path !== 'string') continue;
- map.set(moduleSummary.path, moduleSummary);
- }
- return map;
- }
-
- const baseModules = moduleMap(base);
- const headModules = moduleMap(head);
- const newModules = [...headModules.values()]
- .filter(moduleSummary => !baseModules.has(moduleSummary.path))
- .toSorted((a, b) => b.sourceBytes - a.sourceBytes)
- .slice(0, 10);
-
- if (newModules.length === 0) return null;
-
- const lines = [
- '#### Largest Newly Loaded Modules',
- '',
- '| Module | Package | Loaded JS |',
- '| --- | --- | ---: |',
- ];
-
- for (const moduleSummary of newModules) {
- lines.push(`| \`${moduleSummary.path}\` | ${moduleSummary.package} | ${util.formatBytes(moduleSummary.sourceBytes)} |`);
- }
-
- return lines.join('\n');
-}
-
-function renderJsFootprintSection(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
- const lines = [
- '### Runtime Loaded JS Footprint',
- '',
- 'Click to show
',
- '',
- renderJsFootprintMetricTable(base, head),
- '',
- //'#### Load Phase Breakdown',
- //'',
- //renderJsFootprintPhaseTable(base, head),
- //'',
- ];
-
- for (const block of [
- renderNewExternalPackages(base, head),
- renderLargestPackageIncreases(base, head),
- renderNewLoadedModules(base, head),
- ]) {
- if (block == null) continue;
- lines.push(block);
- lines.push('');
- }
-
- lines.push(' ');
- lines.push('');
-
- return lines.join('\n');
-}
-
const base = JSON.parse(await readFile(baseFile, 'utf8')) as MemoryReport;
const head = JSON.parse(await readFile(headFile, 'utf8')) as MemoryReport;
-const baseJsFootprint = JSON.parse(await readFile(baseJsFootprintFile, 'utf8')) as RuntimeLoadedJsFootprintReport;
-const headJsFootprint = JSON.parse(await readFile(headJsFootprintFile, 'utf8')) as RuntimeLoadedJsFootprintReport;
+
const lines = [
- '## ⚙️ Backend Memory Usage Report',
+ '## ⚙️ Backend Diagnostics Report',
'',
];
@@ -373,7 +136,7 @@ const lines = [
//}
for (const phase of memoryReportPhases) {
- lines.push(`### ${phase.title}`);
+ lines.push(`### Memory: ${phase.title}`);
lines.push(renderMainTableForPhase(base, head, phase.key));
lines.push('');
}
@@ -386,22 +149,16 @@ if (heapSnapshotSection != null) {
const baseHeapSnapshotArtifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_BASE!.trim();
const headHeapSnapshotArtifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_HEAD!.trim();
-lines.push(`Download representative V8 heap snapshot [base](${baseHeapSnapshotArtifactUrl}) / [head](${headHeapSnapshotArtifactUrl})`);
+lines.push(`You can download the representative heap snapshot: [base](${baseHeapSnapshotArtifactUrl}) / [head](${headHeapSnapshotArtifactUrl})`);
lines.push('');
-const jsFootprintSection = renderJsFootprintSection(baseJsFootprint, headJsFootprint);
-if (jsFootprintSection != null) {
- lines.push(jsFootprintSection);
- lines.push('');
-}
-
-function getDiffPercent(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
+function getDiffPercent(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
const baseValue = getMemoryValue(base, phase, metric);
const headValue = getMemoryValue(head, phase, metric);
return ((headValue - baseValue) * 100) / baseValue;
}
-function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
+function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
const baseValue = getMemoryValue(base, phase, metric);
const headValue = getMemoryValue(head, phase, metric);
@@ -419,10 +176,8 @@ function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: type
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(`⚠️ **Warning**: Memory usage (${formatMemoryMetricName(warningMetric)}) has increased by more than 5% and exceeds the observed sample noise. Please verify this is not an unintended change.`);
lines.push('');
}
-//lines.push(`[See workflow logs for details](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`);
-
await writeFile(outputFile, `${lines.join('\n')}\n`);