mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 17:55:06 +02:00
wip
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
const inputFile = process.argv[2];
|
||||
const outputFile = process.argv[3];
|
||||
const [beforeFile, afterFile, outputFile] = process.argv.slice(2);
|
||||
|
||||
if (inputFile == null || outputFile == null) {
|
||||
console.error('Usage: node .github/scripts/frontend-bundle-visualizer-report.mjs <stats.json> <report.md>');
|
||||
if (beforeFile == null || afterFile == null || outputFile == null) {
|
||||
console.error('Usage: node .github/scripts/frontend-bundle-visualizer-report.mjs <before-stats.json> <after-stats.json> <report.md>');
|
||||
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';
|
||||
@@ -25,6 +25,10 @@ function formatBytes(value) {
|
||||
return `${byteFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
return numberFormatter.format(value);
|
||||
}
|
||||
|
||||
function formatPercent(value) {
|
||||
return `${value.toFixed(1)}%`;
|
||||
}
|
||||
@@ -34,6 +38,38 @@ function sharePercent(value, total) {
|
||||
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', ' ');
|
||||
}
|
||||
@@ -52,96 +88,149 @@ function tableCode(value) {
|
||||
return tableCell(code(value));
|
||||
}
|
||||
|
||||
const data = JSON.parse(await readFile(inputFile, 'utf8'));
|
||||
const nodeParts = data.nodeParts ?? {};
|
||||
const nodeMetas = Object.values(data.nodeMetas ?? {});
|
||||
const moduleRows = [];
|
||||
const bundleMap = new Map();
|
||||
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,
|
||||
parts: [],
|
||||
};
|
||||
|
||||
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;
|
||||
row.parts.push({ bundleId, ...part });
|
||||
|
||||
const bundle = bundleMap.get(bundleId) ?? {
|
||||
id: bundleId,
|
||||
modules: 0,
|
||||
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,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) {
|
||||
const part = nodeParts[partUid];
|
||||
if (part == null) continue;
|
||||
|
||||
const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength);
|
||||
const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength);
|
||||
row.bundles += 1;
|
||||
row.renderedLength += part.renderedLength;
|
||||
row.gzipLength += part.gzipLength;
|
||||
row.brotliLength += part.brotliLength;
|
||||
|
||||
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 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,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
const entries = nodeMetas.filter((meta) => meta.isEntry).length;
|
||||
const externals = nodeMetas.filter((meta) => meta.isExternal).length;
|
||||
function renderSummaryTable(before, after) {
|
||||
const metrics = [
|
||||
{ key: 'bundles', label: 'Bundles' },
|
||||
{ key: 'modules', label: 'Modules' },
|
||||
{ key: 'entries', label: 'Entries' },
|
||||
{ key: 'externals', label: 'Externals' },
|
||||
{ key: 'staticImports', label: 'Static Imports' },
|
||||
{ key: 'dynamicImports', label: 'Dynamic Imports' },
|
||||
];
|
||||
|
||||
return [
|
||||
`| | ${metrics.map((metric) => metric.label).join(' | ')} |`,
|
||||
`|---|${metrics.map(() => '---:').join('|')}|`,
|
||||
`| Before | ${metrics.map((metric) => formatNumber(before.summary[metric.key])).join(' | ')} |`,
|
||||
`| After | ${metrics.map((metric) => formatNumber(after.summary[metric.key])).join(' | ')} |`,
|
||||
`| Diff | ${metrics.map((metric) => formatDiff(before.summary[metric.key], after.summary[metric.key], formatNumber)).join(' | ')} |`,
|
||||
`| Diff (%) | ${metrics.map((metric) => formatDiffPercent(before.summary[metric.key], after.summary[metric.key])).join(' | ')} |`,
|
||||
];
|
||||
}
|
||||
|
||||
function renderMetricTable(before, after) {
|
||||
const metrics = [
|
||||
{ key: 'renderedLength', label: 'Rendered size' },
|
||||
];
|
||||
if (before.options.gzip || after.options.gzip) {
|
||||
metrics.push({ key: 'gzipLength', label: 'Gzip size' });
|
||||
}
|
||||
if (before.options.brotli || after.options.brotli) {
|
||||
metrics.push({ key: 'brotliLength', label: 'Brotli size' });
|
||||
}
|
||||
|
||||
const lines = [
|
||||
'| Metric | Before | After | Diff | Diff (%) |',
|
||||
'|---|---:|---:|---:|---:|',
|
||||
];
|
||||
for (const metric of metrics) {
|
||||
const beforeValue = before.metrics[metric.key];
|
||||
const afterValue = after.metrics[metric.key];
|
||||
lines.push(`| ${metric.label} | ${formatBytes(beforeValue)} | ${formatBytes(afterValue)} | ${formatDiff(beforeValue, afterValue, formatBytes)} | ${formatDiffPercent(beforeValue, afterValue)} |`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
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 = [
|
||||
'# Bundle Report',
|
||||
'',
|
||||
'| Bundles | Modules | Entries | Externals | Static Imports | Dynamic Imports |',
|
||||
'|---:|---:|---:|---:|---:|---:|',
|
||||
`| ${bundleRows.length} | ${moduleRows.length} | ${entries} | ${externals} | ${staticImports} | ${dynamicImports} |`,
|
||||
...renderSummaryTable(before, after),
|
||||
'',
|
||||
...renderMetricTable(before, after),
|
||||
'',
|
||||
'## Top 10',
|
||||
'',
|
||||
'| Metric | Total |',
|
||||
'|---|---:|',
|
||||
`| Rendered size | ${formatBytes(totalRendered)} |`,
|
||||
];
|
||||
|
||||
if (data.options?.gzip) {
|
||||
lines.push(`| Gzip size | ${formatBytes(totalGzip)} |`);
|
||||
}
|
||||
if (data.options?.brotli) {
|
||||
lines.push(`| Brotli size | ${formatBytes(totalBrotli)} |`);
|
||||
}
|
||||
|
||||
lines.push('', '## Top 10', '');
|
||||
for (const row of hotModules.slice(0, 10)) {
|
||||
lines.push(`- ${code(row.id)}: ${sharePercent(row.renderedLength, totalRendered)} (${formatBytes(row.renderedLength)})`);
|
||||
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(
|
||||
@@ -152,8 +241,8 @@ lines.push(
|
||||
'|---|---:|---:|---:|---:|---:|---:|---:|',
|
||||
);
|
||||
|
||||
for (const row of hotModules.slice(0, 15)) {
|
||||
lines.push(`| ${tableCode(row.id)} | ${row.bundles} | ${formatBytes(row.renderedLength)} | ${sharePercent(row.renderedLength, totalRendered)} | ${formatBytes(row.gzipLength)} | ${formatBytes(row.brotliLength)} | ${row.importedCount} | ${row.importedByCount} |`);
|
||||
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} |`);
|
||||
}
|
||||
|
||||
await writeFile(outputFile, `${lines.join('\n')}\n`);
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Generate visualizer report
|
||||
run: node .github/scripts/frontend-bundle-visualizer-report.mjs "$RUNNER_TEMP/frontend-bundle-visualizer/stats.json" "$RUNNER_TEMP/frontend-bundle-visualizer/report.md"
|
||||
run: node .github/scripts/frontend-bundle-visualizer-report.mjs "$RUNNER_TEMP/frontend-bundle-visualizer/before-stats.json" "$RUNNER_TEMP/frontend-bundle-visualizer/after-stats.json" "$RUNNER_TEMP/frontend-bundle-visualizer/report.md"
|
||||
|
||||
- name: Comment on pull request
|
||||
uses: actions/github-script@v9
|
||||
@@ -52,6 +52,7 @@ jobs:
|
||||
const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-bundle-visualizer');
|
||||
const reportPath = path.join(reportDir, 'report.md');
|
||||
const prNumberPath = path.join(reportDir, 'pr-number.txt');
|
||||
const baseShaPath = path.join(reportDir, 'base-sha.txt');
|
||||
const headShaPath = path.join(reportDir, 'head-sha.txt');
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const headSha = workflowRun.head_sha;
|
||||
@@ -104,11 +105,16 @@ jobs:
|
||||
}
|
||||
|
||||
const report = fs.readFileSync(reportPath, 'utf8').trim();
|
||||
const baseSha = fs.existsSync(baseShaPath)
|
||||
? fs.readFileSync(baseShaPath, 'utf8').trim()
|
||||
: null;
|
||||
const shortSha = headSha.slice(0, 7);
|
||||
const shortBaseSha = baseSha != null ? baseSha.slice(0, 7) : null;
|
||||
let body = [
|
||||
marker,
|
||||
'## Frontend bundle visualizer',
|
||||
'',
|
||||
...(shortBaseSha != null ? [`Base: \`${shortBaseSha}\``] : []),
|
||||
`Head: \`${shortSha}\``,
|
||||
'',
|
||||
report,
|
||||
|
||||
58
.github/workflows/frontend-bundle-visualizer.yml
vendored
58
.github/workflows/frontend-bundle-visualizer.yml
vendored
@@ -37,50 +37,94 @@ jobs:
|
||||
name: Build frontend bundle visualizer
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: before
|
||||
submodules: true
|
||||
|
||||
- name: Checkout pull request
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: after
|
||||
submodules: true
|
||||
|
||||
- name: Backport visualizer tooling to base if needed
|
||||
shell: bash
|
||||
run: |
|
||||
if ! grep -q 'FRONTEND_BUNDLE_VISUALIZER' before/packages/frontend/vite.config.ts; then
|
||||
cp after/packages/frontend/package.json before/packages/frontend/package.json
|
||||
cp after/packages/frontend/vite.config.ts before/packages/frontend/vite.config.ts
|
||||
cp after/pnpm-lock.yaml before/pnpm-lock.yaml
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
with:
|
||||
package_json_file: after/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
node-version-file: after/.node-version
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
before/pnpm-lock.yaml
|
||||
after/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install dependencies for base
|
||||
working-directory: before
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies
|
||||
- name: Build frontend dependencies for base
|
||||
working-directory: before
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Prepare visualizer output
|
||||
run: mkdir -p "$RUNNER_TEMP/frontend-bundle-visualizer"
|
||||
|
||||
- name: Build frontend visualizer
|
||||
- name: Build frontend visualizer for base
|
||||
working-directory: before
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
FRONTEND_BUNDLE_VISUALIZER_TEMPLATE: raw-data
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-visualizer/stats.json
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-visualizer/before-stats.json
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Install dependencies for pull request
|
||||
working-directory: after
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for pull request
|
||||
working-directory: after
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Build frontend visualizer for pull request
|
||||
working-directory: after
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
FRONTEND_BUNDLE_VISUALIZER_TEMPLATE: raw-data
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-visualizer/after-stats.json
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Generate visualizer report
|
||||
run: node .github/scripts/frontend-bundle-visualizer-report.mjs "$RUNNER_TEMP/frontend-bundle-visualizer/stats.json" "$RUNNER_TEMP/frontend-bundle-visualizer/report.md"
|
||||
run: node after/.github/scripts/frontend-bundle-visualizer-report.mjs "$RUNNER_TEMP/frontend-bundle-visualizer/before-stats.json" "$RUNNER_TEMP/frontend-bundle-visualizer/after-stats.json" "$RUNNER_TEMP/frontend-bundle-visualizer/report.md"
|
||||
|
||||
- name: Check visualizer report
|
||||
run: |
|
||||
test -s "$RUNNER_TEMP/frontend-bundle-visualizer/stats.json"
|
||||
test -s "$RUNNER_TEMP/frontend-bundle-visualizer/before-stats.json"
|
||||
test -s "$RUNNER_TEMP/frontend-bundle-visualizer/after-stats.json"
|
||||
test -s "$RUNNER_TEMP/frontend-bundle-visualizer/report.md"
|
||||
cat "$RUNNER_TEMP/frontend-bundle-visualizer/report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Write report metadata
|
||||
run: |
|
||||
printf '%s\n' "${{ github.event.pull_request.number }}" > "$RUNNER_TEMP/frontend-bundle-visualizer/pr-number.txt"
|
||||
printf '%s\n' "${{ github.event.pull_request.base.sha }}" > "$RUNNER_TEMP/frontend-bundle-visualizer/base-sha.txt"
|
||||
printf '%s\n' "${{ github.event.pull_request.head.sha }}" > "$RUNNER_TEMP/frontend-bundle-visualizer/head-sha.txt"
|
||||
printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$RUNNER_TEMP/frontend-bundle-visualizer/pr-url.txt"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user