From 6e4380f11d2f7d7377616c6ce2574a0365828fb4 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:37:47 +0900 Subject: [PATCH 1/3] enhance(dev): tweak report-backend-memory --- .github/scripts/backend-memory-report.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/scripts/backend-memory-report.mjs b/.github/scripts/backend-memory-report.mjs index 1eff258bb2..924a11dd57 100644 --- a/.github/scripts/backend-memory-report.mjs +++ b/.github/scripts/backend-memory-report.mjs @@ -20,7 +20,6 @@ const phases = [ const metrics = [ 'HeapUsed', - 'HeapTotal', 'Pss', 'Private_Dirty', 'VmRSS', From 544c4227f75b01f925d3df8ef1422af763a500fd Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:12:57 +0900 Subject: [PATCH 2/3] chore(dev): refactor --- .github/scripts/frontend-js-size.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/frontend-js-size.mjs b/.github/scripts/frontend-js-size.mjs index e43266daea..eaf6f414bc 100644 --- a/.github/scripts/frontend-js-size.mjs +++ b/.github/scripts/frontend-js-size.mjs @@ -49,7 +49,7 @@ function formatMathText(text) { .replaceAll('\\', '\\\\') .replaceAll('{', '\\{') .replaceAll('}', '\\}') - .replaceAll('%', '\\\\%'); + .replaceAll('%', '\\%'); } function formatDiff(diff) { @@ -58,7 +58,7 @@ function formatDiff(diff) { const sign = diff > 0 ? '+' : '-'; const text = `${sign}${formatBytes(Math.abs(diff))}`; const color = diff > 0 ? 'orange' : 'green'; - return `$\\color{${color}}{\\text{${formatMathText(text)}}}$`; + return `$\\color{${color}}{\\text{${formatMathText(text).replaceAll('\\%', '\\\\%')}}}$`; } function formatDiffPercent(beforeSize, afterSize) { @@ -67,7 +67,7 @@ function formatDiffPercent(beforeSize, afterSize) { if (diff === 0) return `0%`; const percent = Math.round(diff / beforeSize * 100); const color = diff > 0 ? 'orange' : 'green'; - return `$\\color{${color}}{\\text{${formatMathText(percent.toString() + '%')}}}$`; + return `$\\color{${color}}{\\text{${formatMathText(percent.toString() + '%').replaceAll('\\%', '\\\\%')}}}$`; } function escapeCell(value) { From 8186742c0f3e6958e1e026a06f60bffa2242284b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:48:25 +0900 Subject: [PATCH 3/3] refactor(dev): unify frontend-bundle-visualizer-report.mjs and frontend-js-size.mjs --- .../frontend-bundle-visualizer-report.mjs | 276 ----------- .github/scripts/frontend-js-size.mjs | 431 ++++++++++++++---- .../frontend-bundle-report-comment.yml | 12 +- .github/workflows/frontend-bundle-report.yml | 7 +- 4 files changed, 333 insertions(+), 393 deletions(-) delete mode 100644 .github/scripts/frontend-bundle-visualizer-report.mjs diff --git a/.github/scripts/frontend-bundle-visualizer-report.mjs b/.github/scripts/frontend-bundle-visualizer-report.mjs deleted file mode 100644 index 366bf9aa53..0000000000 --- a/.github/scripts/frontend-bundle-visualizer-report.mjs +++ /dev/null @@ -1,276 +0,0 @@ -import { readFile, writeFile } from 'node:fs/promises'; - -const [beforeFile, afterFile, outputFile] = process.argv.slice(2); - -if (beforeFile == null || afterFile == null || outputFile == null) { - console.error('Usage: node .github/scripts/frontend-bundle-visualizer-report.mjs '); - process.exit(1); -} - -const byteFormatter = new Intl.NumberFormat('en-US'); -const numberFormatter = new Intl.NumberFormat('en-US'); - -function formatBytes(value) { - if (!Number.isFinite(value) || value <= 0) return '0 B'; - - const units = ['B', 'KiB', 'MiB', 'GiB']; - let unitIndex = 0; - let size = value; - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex += 1; - } - - const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1; - return `${byteFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`; -} - -function formatNumber(value) { - return numberFormatter.format(value); -} - -function formatPercent(value) { - return `${Math.round(value)}%`; -} - -function sharePercent(value, total) { - if (total === 0) return '0%'; - return formatPercent((value / total) * 100); -} - -function formatMathText(text) { - return text - .replaceAll('\\', '\\\\') - .replaceAll('{', '\\{') - .replaceAll('}', '\\}') - .replaceAll('%', '\\%'); -} - -function formatColoredDiff(text, diff) { - const color = diff > 0 ? 'orange' : 'green'; - return `$\\color{${color}}{\\text{${formatMathText(text)}}}$`; -} - -function formatDiff(before, after, formatter) { - const diff = after - before; - if (diff === 0) return formatter(0); - - const sign = diff > 0 ? '+' : '-'; - return formatColoredDiff(`${sign}${formatter(Math.abs(diff))}`, diff); -} - -function formatDiffPercent(before, after) { - if (before === 0 && after === 0) return '0%'; - if (before === 0) return '-'; - - const diff = after - before; - if (diff === 0) return '0%'; - - const sign = diff > 0 ? '+' : '-'; - return formatColoredDiff(`${sign}${formatPercent(Math.abs(diff / before) * 100)}`, diff); -} - -function tableCell(value) { - return String(value).replaceAll('|', '\\|').replaceAll('\r', ' ').replaceAll('\n', ' '); -} - -function code(value) { - const sanitized = String(value).replaceAll('\r', ' ').replaceAll('\n', ' '); - const backtickRuns = sanitized.match(/`+/g) ?? []; - const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1)); - const fence = '`'.repeat(fenceLength); - const padding = sanitized.startsWith('`') || sanitized.endsWith('`') ? ' ' : ''; - - return `${fence}${padding}${sanitized}${padding}${fence}`; -} - -function tableCode(value) { - return tableCell(code(value)); -} - -function collectReport(data) { - const nodeParts = data.nodeParts ?? {}; - const nodeMetas = Object.values(data.nodeMetas ?? {}); - const moduleRows = []; - const bundleMap = new Map(); - - for (const meta of nodeMetas) { - const row = { - id: meta.id, - bundles: 0, - renderedLength: 0, - gzipLength: 0, - brotliLength: 0, - importedByCount: meta.importedBy?.length ?? 0, - importedCount: meta.imported?.length ?? 0, - }; - - for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) { - const part = nodeParts[partUid]; - if (part == null) continue; - - row.bundles += 1; - row.renderedLength += part.renderedLength; - row.gzipLength += part.gzipLength; - row.brotliLength += part.brotliLength; - - const bundle = bundleMap.get(bundleId) ?? { - id: bundleId, - modules: 0, - renderedLength: 0, - gzipLength: 0, - brotliLength: 0, - }; - bundle.modules += 1; - bundle.renderedLength += part.renderedLength; - bundle.gzipLength += part.gzipLength; - bundle.brotliLength += part.brotliLength; - bundleMap.set(bundleId, bundle); - } - - if (row.bundles > 0) { - moduleRows.push(row); - } - } - - let staticImports = 0; - let dynamicImports = 0; - for (const meta of nodeMetas) { - for (const imported of meta.imported ?? []) { - if (imported.dynamic) { - dynamicImports += 1; - } else { - staticImports += 1; - } - } - } - - const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength); - const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength); - const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0); - const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0); - const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0); - - return { - options: data.options ?? {}, - summary: { - bundles: bundleRows.length, - modules: moduleRows.length, - entries: nodeMetas.filter((meta) => meta.isEntry).length, - externals: nodeMetas.filter((meta) => meta.isExternal).length, - staticImports, - dynamicImports, - }, - metrics: { - renderedLength: totalRendered, - gzipLength: totalGzip, - brotliLength: totalBrotli, - }, - hotModules, - }; -} - -function renderSummaryTable(before, after) { - const summary = [ - 'bundles', - 'modules', - 'entries', - //'externals', - 'staticImports', - 'dynamicImports', - ]; - - const metrics = [ - 'renderedLength', - 'gzipLength', - 'brotliLength', - ]; - - return [ - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ``, - ...summary.map((key) => ``), - ...metrics.map((key) => ``), - ``, - ``, - ``, - ...summary.map((key) => ``), - ...metrics.map((key) => ``), - ``, - ``, - ``, - ``, - ...summary.map((key) => ``), - ...metrics.map((key) => ``), - ``, - ``, - ``, - ...summary.map((key) => ``), - ...metrics.map((key) => ``), - ``, - ``, - `
BundlesModulesEntriesImportsSize
StaticDynamicRenderedGzipBrotli
Before${formatNumber(before.summary[key])}${formatBytes(before.metrics[key])}
After${formatNumber(after.summary[key])}${formatBytes(after.metrics[key])}
Δ${formatDiff(before.summary[key], after.summary[key], formatNumber)}${formatDiff(before.metrics[key], after.metrics[key], formatBytes)}
Δ (%)${formatDiffPercent(before.summary[key], after.summary[key])}${formatDiffPercent(before.metrics[key], after.metrics[key])}
`, - ]; -} - -const beforeData = JSON.parse(await readFile(beforeFile, 'utf8')); -const afterData = JSON.parse(await readFile(afterFile, 'utf8')); -const before = collectReport(beforeData); -const after = collectReport(afterData); -const lines = [ - '## Frontend Bundle Report', - '', - ...renderSummaryTable(before, after), - '', - '
', - 'Top 10', - '', -]; - -for (const row of after.hotModules.slice(0, 10)) { - lines.push(`- ${code(row.id)}: ${sharePercent(row.renderedLength, after.metrics.renderedLength)} (${formatBytes(row.renderedLength)})`); -} - -lines.push( - '', - '
', -); - -lines.push( - '', - '
', - 'Hot Modules (Self Size)', - '', - '| Module | Bundles | Rendered | Share | Gzip | Brotli | Imports | Imported By |', - '|---|---:|---:|---:|---:|---:|---:|---:|', -); - -for (const row of after.hotModules.slice(0, 15)) { - lines.push(`| ${tableCode(row.id)} | ${row.bundles} | ${formatBytes(row.renderedLength)} | ${sharePercent(row.renderedLength, after.metrics.renderedLength)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${row.importedCount} | ${row.importedByCount} |`); -} - -lines.push( - '', - '
', -); - -await writeFile(outputFile, `${lines.join('\n')}\n`); diff --git a/.github/scripts/frontend-js-size.mjs b/.github/scripts/frontend-js-size.mjs index eaf6f414bc..ce006b4d02 100644 --- a/.github/scripts/frontend-js-size.mjs +++ b/.github/scripts/frontend-js-size.mjs @@ -1,8 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import { promises as fs } from 'node:fs'; import path from 'node:path'; const marker = ''; const locale = process.env.FRONTEND_JS_SIZE_LOCALE || 'ja-JP'; +const byteFormatter = new Intl.NumberFormat('en-US'); +const numberFormatter = new Intl.NumberFormat('en-US'); function normalizePath(filePath) { return filePath.split(path.sep).join('/'); @@ -33,18 +40,26 @@ async function* walk(dir) { } } +function formatNumber(value) { + return numberFormatter.format(value); +} + function formatBytes(size) { - if (size == null) return '-'; - if (size < 1024) return `${size} B`; - if (size < 1024 * 1024) return `${stripTrailingZeros((size / 1024).toFixed(1))} KiB`; - return `${stripTrailingZeros((size / 1024 / 1024).toFixed(2))} MiB`; + if (!Number.isFinite(value) || value <= 0) return '0 B'; + + const units = ['B', 'KiB', 'MiB', 'GiB']; + let unitIndex = 0; + let size = value; + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex += 1; + } + + const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1; + return `${byteFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`; } -function stripTrailingZeros(value) { - return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1'); -} - -function formatMathText(text) { +function escapeLatex(text) { return text .replaceAll('\\', '\\\\') .replaceAll('{', '\\{') @@ -52,28 +67,60 @@ function formatMathText(text) { .replaceAll('%', '\\%'); } -function formatDiff(diff) { - if (diff == null) return '-'; - if (diff === 0) return '0 B'; - const sign = diff > 0 ? '+' : '-'; - const text = `${sign}${formatBytes(Math.abs(diff))}`; +function formatColoredDiff(text, diff) { + if (diff === 0) return text; const color = diff > 0 ? 'orange' : 'green'; - return `$\\color{${color}}{\\text{${formatMathText(text).replaceAll('\\%', '\\\\%')}}}$`; + const sign = diff > 0 ? '+' : '-'; + return `$\\color{${color}}{\\text{${sign}${escapeLatex(text).replaceAll('\\%', '\\\\%')}}}$`; } -function formatDiffPercent(beforeSize, afterSize) { - if (beforeSize == null || beforeSize === 0 || afterSize == null || afterSize === 0) return '-'; - const diff = afterSize - beforeSize; +function formatNumberDiff(before, after) { + if (before == null || after == null) return '-'; + const diff = after - before; + return formatColoredDiff(formatNumber(Math.abs(diff)), diff); +} + +function formatBytesDiff(diff) { + if (diff == null) return '-'; + if (diff === 0) return '0 B'; + return formatColoredDiff(formatBytes(Math.abs(diff)), diff); +} + +function formatDiffPercent(before, after) { + if (before == null || before === 0 || after == null || after === 0) return '-'; + const diff = after - before; if (diff === 0) return `0%`; - const percent = Math.round(diff / beforeSize * 100); - const color = diff > 0 ? 'orange' : 'green'; - return `$\\color{${color}}{\\text{${formatMathText(percent.toString() + '%').replaceAll('\\%', '\\\\%')}}}$`; + const percent = Math.round(diff / before * 100); + return formatColoredDiff(`${percent}%`, diff); +} + +function sharePercent(value, total) { + if (total === 0) return '0%'; + return Math.round((value / total) * 100) + '%'; } function escapeCell(value) { return String(value).replaceAll('|', '\\|').replaceAll('\n', '
'); } +function tableCell(value) { + return String(value).replaceAll('|', '\\|').replaceAll('\r', ' ').replaceAll('\n', ' '); +} + +function code(value) { + const sanitized = String(value).replaceAll('\r', ' ').replaceAll('\n', ' '); + const backtickRuns = sanitized.match(/`+/g) ?? []; + const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1)); + const fence = '`'.repeat(fenceLength); + const padding = sanitized.startsWith('`') || sanitized.endsWith('`') ? ' ' : ''; + + return `${fence}${padding}${sanitized}${padding}${fence}`; +} + +function tableCode(value) { + return tableCell(code(value)); +} + function entryDisplayName(entry) { if (!entry) return ''; return entry.displayName || entry.file; @@ -174,28 +221,165 @@ async function collectReport(repoDir) { }; } -function commonKeys(before, after) { +function collectVisualizerReport(data) { + const nodeParts = data.nodeParts ?? {}; + const nodeMetas = Object.values(data.nodeMetas ?? {}); + const moduleRows = []; + const bundleMap = new Map(); + + for (const meta of nodeMetas) { + const row = { + id: meta.id, + bundles: 0, + renderedLength: 0, + gzipLength: 0, + brotliLength: 0, + importedByCount: meta.importedBy?.length ?? 0, + importedCount: meta.imported?.length ?? 0, + }; + + for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) { + const part = nodeParts[partUid]; + if (part == null) continue; + + row.bundles += 1; + row.renderedLength += part.renderedLength; + row.gzipLength += part.gzipLength; + row.brotliLength += part.brotliLength; + + const bundle = bundleMap.get(bundleId) ?? { + id: bundleId, + modules: 0, + renderedLength: 0, + gzipLength: 0, + brotliLength: 0, + }; + bundle.modules += 1; + bundle.renderedLength += part.renderedLength; + bundle.gzipLength += part.gzipLength; + bundle.brotliLength += part.brotliLength; + bundleMap.set(bundleId, bundle); + } + + if (row.bundles > 0) { + moduleRows.push(row); + } + } + + let staticImports = 0; + let dynamicImports = 0; + for (const meta of nodeMetas) { + for (const imported of meta.imported ?? []) { + if (imported.dynamic) { + dynamicImports += 1; + } else { + staticImports += 1; + } + } + } + + const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength); + const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength); + const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0); + const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0); + const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0); + + return { + options: data.options ?? {}, + summary: { + bundles: bundleRows.length, + modules: moduleRows.length, + entries: nodeMetas.filter((meta) => meta.isEntry).length, + externals: nodeMetas.filter((meta) => meta.isExternal).length, + staticImports, + dynamicImports, + }, + metrics: { + renderedLength: totalRendered, + gzipLength: totalGzip, + brotliLength: totalBrotli, + }, + hotModules, + }; +} + +function renderVisualizerSummaryTable(before, after) { + const summary = [ + 'bundles', + 'modules', + 'entries', + //'externals', + 'staticImports', + 'dynamicImports', + ]; + + const metrics = [ + 'renderedLength', + 'gzipLength', + 'brotliLength', + ]; + + return [ + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ``, + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + ``, + ``, + ``, + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + ``, + ``, + ``, + ``, + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + ``, + ``, + ``, + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + ``, + ``, + `
BundlesModulesEntriesImportsSize
StaticDynamicRenderedGzipBrotli
Before${formatNumber(before.summary[key])}${formatBytes(before.metrics[key])}
After${formatNumber(after.summary[key])}${formatBytes(after.metrics[key])}
Δ${formatNumberDiff(before.summary[key], after.summary[key])}${formatBytesDiff(before.metrics[key], after.metrics[key])}
Δ (%)${formatDiffPercent(before.summary[key], after.summary[key])}${formatDiffPercent(before.metrics[key], after.metrics[key])}
`, + ]; +} + +function commonChunkKeys(before, after) { return Object.keys(before.chunks) .filter((key) => after.chunks[key] != null); } -function addedKeys(before, after) { +function addedChunkKeys(before, after) { return Object.keys(after.chunks) .filter((key) => before.chunks[key] == null); } -function removedKeys(before, after) { +function removedChunkKeys(before, after) { return Object.keys(before.chunks) .filter((key) => after.chunks[key] == null); } -function rowChangeType(beforeEntry, afterEntry, beforeSize, afterSize) { - if (beforeEntry == null) return 'added'; - if (afterEntry == null) return 'removed'; - if (beforeSize !== afterSize) return 'updated'; - return 'unchanged'; -} - function getChunkComparisonRows(keys, before, after) { return keys.map((key) => { const beforeEntry = before.chunks[key]; @@ -208,13 +392,13 @@ function getChunkComparisonRows(keys, before, after) { chunkFile: beforeEntry?.file ?? afterEntry?.file, beforeSize, afterSize, - changeType: rowChangeType(beforeEntry, afterEntry, beforeSize, afterSize), + changeType: beforeEntry == null ? 'added' : afterEntry == null ? 'removed' : beforeSize !== afterSize ? 'updated' : 'unchanged', sortSize: Math.max(beforeSize, afterSize), }; }); } -function summarizeChanges(rows) { +function summarizeChunkChanges(rows) { return { updated: rows.filter((row) => row.changeType === 'updated').length, added: rows.filter((row) => row.changeType === 'added').length, @@ -222,18 +406,18 @@ function summarizeChanges(rows) { }; } -function formatChangeSummary(label, summary) { +function formatChunkChangeSummary(label, summary) { return `${label} (${summary.updated} updated, ${summary.added} added, ${summary.removed} removed)`; } -function compareComparisonRows(a, b) { +function compareChunkComparisonRows(a, b) { return Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize) || (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize) || b.sortSize - a.sortSize || a.name.localeCompare(b.name); } -function markdownTable(rows, total) { +function chunkMarkdownTable(rows, total) { if (rows.length === 0) return '_No data_'; const lines = [ @@ -241,91 +425,138 @@ function markdownTable(rows, total) { '| --- | ---: | ---: | ---: | ---: |', ]; if (total != null) { - lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${formatDiff(total.afterSize - total.beforeSize)} | ${formatDiffPercent(total.beforeSize, total.afterSize)} |`); + lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${formatBytesDiff(total.afterSize - total.beforeSize)} | ${formatDiffPercent(total.beforeSize, total.afterSize)} |`); lines.push('| | | | | |'); } for (const row of rows) { if (row.changeType === 'added') { - lines.push(`|
\`${escapeCell(row.name)}\` \`${escapeCell(row.chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | $\\color{orange}{\\text{(+)}}$ |`); + lines.push(`|
\`${escapeCell(row.name)}\` \`${escapeCell(row.chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatBytesDiff(row.afterSize - row.beforeSize)} | $\\color{orange}{\\text{(+)}}$ |`); } else if (row.changeType === 'removed') { - lines.push(`|
\`${escapeCell(row.name)}\` \`${escapeCell(row.chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | $\\color{green}{\\text{(-)}}$ |`); + lines.push(`|
\`${escapeCell(row.name)}\` \`${escapeCell(row.chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatBytesDiff(row.afterSize - row.beforeSize)} | $\\color{green}{\\text{(-)}}$ |`); } else { - lines.push(`|
\`${escapeCell(row.name)}\` \`${escapeCell(row.chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`); + lines.push(`|
\`${escapeCell(row.name)}\` \`${escapeCell(row.chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatBytesDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`); } } return lines.join('\n'); } -const beforeDir = process.argv[2]; -const afterDir = process.argv[3]; -const outFile = process.argv[4]; -const beforeSha = process.env.BASE_SHA; -const afterSha = process.env.HEAD_SHA; +function renderFrontendChunkReport(before, after) { + const commonChunkKeys = commonChunkKeys(before, after); + const allChunkKeys = [ + ...commonChunkKeys, + ...addedChunkKeys(before, after), + ...removedChunkKeys(before, after), + ]; + //const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after); + const allComparisonRows = getChunkComparisonRows(allChunkKeys, before, after); + const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged'); + const diffSummary = summarizeChunkChanges(changedRows); + const diffTotal = { + beforeSize: allComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0), + afterSize: allComparisonRows.reduce((sum, row) => sum + row.afterSize, 0), + }; + const diffRows = changedRows.sort(compareChunkComparisonRows).slice(0, 30); // TODO: 実際に30を超えて切り捨てられたrowがあった場合はその旨をmarkdown内に表示するようにする + + const startupKeys = new Set([ + ...before.startupKeys, + ...after.startupKeys, + ]); + const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after); + const startupRows = startupComparisonRows.sort(compareChunkComparisonRows); + const startupSummary = summarizeChunkChanges(startupComparisonRows); + const startupTotal = { + beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0), + afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0), + }; + + //const largeRows = comparisonRows + // .sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name)) + // .slice(0, 30); + + return [ + '
', + `${formatChunkChangeSummary('Diffs', diffSummary)}`, + '', + chunkMarkdownTable(diffRows, diffTotal), + '', + '
', + '', + '
', + `${formatChunkChangeSummary('Startup', startupSummary)}`, + '', + chunkMarkdownTable(startupRows, startupTotal), + '', + `_Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`, + '', + '
', + '', + //'
', + //`Largest`, + //'', + //markdownTable(largeRows), + //'', + //'
', + //'', + ].join('\n'); +} + +function renderFrontendBundleReport(before, after) { + const lines = [ + ...renderVisualizerSummaryTable(before, after), + '', + '
', + 'Top 10', + '', + ]; + + for (const row of after.hotModules.slice(0, 10)) { + lines.push(`- ${code(row.id)}: ${sharePercent(row.renderedLength, after.metrics.renderedLength)} (${formatBytes(row.renderedLength)})`); + } + + lines.push( + '', + '
', + ); + + lines.push( + '', + '
', + 'Hot Modules (Self Size)', + '', + '| Module | Bundles | Rendered | Share | Gzip | Brotli | Imports | Imported By |', + '|---|---:|---:|---:|---:|---:|---:|---:|', + ); + + for (const row of after.hotModules.slice(0, 15)) { + lines.push(`| ${tableCode(row.id)} | ${row.bundles} | ${formatBytes(row.renderedLength)} | ${sharePercent(row.renderedLength, after.metrics.renderedLength)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${row.importedCount} | ${row.importedByCount} |`); + } + + lines.push( + '', + '
', + ); + + return lines.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 commonChunkKeys = commonKeys(before, after); -const allChunkKeys = [ - ...commonChunkKeys, - ...addedKeys(before, after), - ...removedKeys(before, after), -]; -//const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after); -const allComparisonRows = getChunkComparisonRows(allChunkKeys, before, after); - -const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged'); -const diffSummary = summarizeChanges(changedRows); -const diffTotal = { - beforeSize: allComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0), - afterSize: allComparisonRows.reduce((sum, row) => sum + row.afterSize, 0), -}; -const diffRows = changedRows.sort(compareComparisonRows).slice(0, 30); // TODO: 実際に30を超えて切り捨てられたrowがあった場合はその旨をmarkdown内に表示するようにする - -const startupKeys = new Set([ - ...before.startupKeys, - ...after.startupKeys, -]); -const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after); -const startupRows = startupComparisonRows - .sort(compareComparisonRows); -const startupSummary = summarizeChanges(startupComparisonRows); -const startupTotal = { - beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0), - afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0), -}; - -//const largeRows = comparisonRows -// .sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name)) -// .slice(0, 30); +const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8')); +const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8')); const body = [ marker, + '', `## Frontend Chunk Report`, '', - '
', - `${formatChangeSummary('Diffs', diffSummary)}`, + renderFrontendChunkReport(before, after), '', - markdownTable(diffRows, diffTotal), + '## Frontend Bundle Report', '', - '
', - '', - '
', - `${formatChangeSummary('Startup', startupSummary)}`, - '', - markdownTable(startupRows, startupTotal), - '', - `_Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`, - '', - '
', - '', - //'
', - //`Largest`, - //'', - //markdownTable(largeRows), - //'', - //'
', - //'', + renderFrontendBundleReport(collectVisualizerReport(beforeStats), collectVisualizerReport(afterStats)), ].join('\n'); await fs.writeFile(outFile, body); diff --git a/.github/workflows/frontend-bundle-report-comment.yml b/.github/workflows/frontend-bundle-report-comment.yml index 32e12d8cfd..d1535c7710 100644 --- a/.github/workflows/frontend-bundle-report-comment.yml +++ b/.github/workflows/frontend-bundle-report-comment.yml @@ -26,7 +26,6 @@ on: - pnpm-workspace.yaml - .node-version - .github/scripts/frontend-js-size.mjs - - .github/scripts/frontend-bundle-visualizer-report.mjs - .github/workflows/frontend-bundle-report.yml - .github/workflows/frontend-bundle-report-comment.yml @@ -185,7 +184,6 @@ jobs: const reportMarkers = [jsSizeMarker, visualizerMarker]; const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-bundle-report'); const jsSizeReportPath = path.join(reportDir, 'frontend-js-size-report.md'); - const visualizerReportPath = path.join(reportDir, 'frontend-bundle-visualizer-report.md'); const prNumberPath = path.join(reportDir, 'pr-number.txt'); const headShaPath = path.join(reportDir, 'head-sha.txt'); const workflowRun = context.payload.workflow_run; @@ -197,10 +195,6 @@ jobs: core.setFailed('The frontend bundle report artifact does not contain frontend-js-size-report.md.'); return; } - if (!fs.existsSync(visualizerReportPath)) { - core.setFailed('The frontend bundle report artifact does not contain frontend-bundle-visualizer-report.md.'); - return; - } const artifactHeadSha = fs.existsSync(headShaPath) ? fs.readFileSync(headShaPath, 'utf8').trim() @@ -274,11 +268,7 @@ jobs: core.setFailed('The frontend JS size report is missing the expected marker.'); return; } - const visualizerReport = fs.readFileSync(visualizerReportPath, 'utf8').trim(); - let body = [ - jsSizeReport, - visualizerReport, - ].join('\n\n') + '\n'; + let body = `${jsSizeReport}\n`; const maxCommentLength = 65_000; if (body.length > maxCommentLength) { diff --git a/.github/workflows/frontend-bundle-report.yml b/.github/workflows/frontend-bundle-report.yml index 55d52b314c..e040bc390f 100644 --- a/.github/workflows/frontend-bundle-report.yml +++ b/.github/workflows/frontend-bundle-report.yml @@ -21,7 +21,6 @@ on: - pnpm-workspace.yaml - .node-version - .github/scripts/frontend-js-size.mjs - - .github/scripts/frontend-bundle-visualizer-report.mjs - .github/workflows/frontend-bundle-report.yml - .github/workflows/frontend-bundle-report-comment.yml @@ -134,8 +133,7 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} run: | REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report" - node after/.github/scripts/frontend-js-size.mjs before after "$REPORT_DIR/frontend-js-size-report.md" - node after/.github/scripts/frontend-bundle-visualizer-report.mjs "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-bundle-visualizer-report.md" + node after/.github/scripts/frontend-js-size.mjs before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-js-size-report.md" printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt" printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt" printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt" @@ -148,10 +146,7 @@ jobs: test -s "$REPORT_DIR/before-stats.json" test -s "$REPORT_DIR/after-stats.json" test -s "$REPORT_DIR/frontend-js-size-report.md" - test -s "$REPORT_DIR/frontend-bundle-visualizer-report.md" cat "$REPORT_DIR/frontend-js-size-report.md" >> "$GITHUB_STEP_SUMMARY" - printf '\n\n' >> "$GITHUB_STEP_SUMMARY" - cat "$REPORT_DIR/frontend-bundle-visualizer-report.md" >> "$GITHUB_STEP_SUMMARY" - name: Upload bundle report if: steps.check-base-visualizer.outputs.supported == 'true'