豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit bb2a445

Browse files
sokraclaude
andauthored
CI: Track SWC native binary size in PR stats comment (#92938)
### What? Adds a new "Native Binary" section to the `Stats from current PR` comment posted by the `generate-pull-request-stats` GitHub Action. The section reports the size of the SWC native binary (`packages/next-swc/native/*.node`) built for the PR and the change relative to the latest value recorded on `canary`. Example row: > | Metric | Canary | PR | Change | Trend | > |:-------|-------:|---:|-------:|:-----:| > | SWC Binary Size | 113 MB | 121 MB | 🔴 +7.34 MB (+6%) | ▁▃▅▇█ | ### Why? The `.node` binary is a sizable, user-facing artifact (it ships with `next`). Today there's no visibility on its size in PRs, so regressions from Rust/Cargo changes are easy to miss. Surfacing the size (and a diff vs. canary) in the existing stats comment gives immediate feedback on any change that affects the binary. ### How? - **Measurement (`.github/actions/next-stats-action/src/run/index.js`):** After the action copies the pre-built native binary into the working dirs, sum the size of every `*.node` file under the action's `native/` directory and store it as `General.swcBinarySize` on both the canary and PR stat sets. - **Canary baseline via KV history (`.github/actions/next-stats-action/src/add-comment.js`):** The workflow downloads a single pre-built binary and copies it into both the canary and PR checkouts, so an in-run diff would always be zero. Instead, for PR runs we override `mainRepoStats.General.swcBinarySize` with the most recent `swcBinarySize` from the Vercel KV history (the same store already used for other canary metrics). Canary runs keep the measured value, and the existing persistence path (`saveToHistory` / `General`) writes it back so future PRs see an updated baseline. If no history exists yet, the Canary column renders `N/A` and no change is shown. - **Rendering:** New `generateNativeBinarySection` emits a `<details>`-wrapped section styled like Bundle Sizes / All Metrics, rendered right after the Bundle Sizes block. It reuses `prettify`, `formatChange`, and `generateTrendBar` so formatting, thresholds, and sparklines are consistent with the rest of the comment. A tight threshold entry (`swcBinarySize: { absoluteMin: 10 KB, percentMin: 0.5%, percentOnly: 0.05% }`) is added so meaningful movement is flagged while tiny determinism noise is ignored. - **Top-line summary:** Because `swcBinarySize` lives on `General`, the existing `generateChangeSummary` automatically promotes significant changes into the headline regression/improvement table at the top of the comment, using the label `SWC Binary Size`. - **Sharded runs:** The workflow runs two sharded jobs (Webpack and Turbopack). The binary is bundler-independent, so both shards report the same value; `aggregate-results.js` merges `General` via `Object.assign`, which is correct here (last-writer-wins on an identical value). No workflow changes are required — the existing `pull_request_stats.yml` already downloads the `next-swc-binary` artifact and copies it into the action's `native/` directory. ### Verification Smoke-tested `add-comment.js` locally with `LOCAL_STATS=1` in two scenarios: - No KV history configured → Canary column renders `N/A`, PR value renders, Change is `-`, section still shown. - Mocked KV history containing prior `swcBinarySize` values → full diff rendered in both the Native Binary section and the top-level significance summary, with a sparkline of the last 5 historical entries. Closes NEXT- <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Tobias Koppers <sokra@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 87e7b08 commit bb2a445

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

.github/actions/next-stats-action/src/add-comment.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const METRIC_LABELS = {
5555
buildDurationCachedTurbo: 'Turbo Build Time (cached)',
5656
// General metrics
5757
nodeModulesSize: 'node_modules Size',
58+
swcBinarySize: 'SWC Binary Size',
5859
}
5960

6061
// Group configuration for organizing the comment
@@ -209,6 +210,10 @@ const METRIC_THRESHOLDS = {
209210
// <10KB AND <1%, OR <0.01%
210211
nodeModulesSize: { absoluteMin: 10240, percentMin: 1, percentOnly: 0.01 },
211212

213+
// SWC native binary (~tens of MB): deterministic, but smaller baseline
214+
// <10KB AND <0.5%, OR <0.05%
215+
swcBinarySize: { absoluteMin: 10240, percentMin: 0.5, percentOnly: 0.05 },
216+
212217
// Bundle sizes (KB-MB): deterministic
213218
// <2KB AND <1%, OR <0.1%
214219
bytes: { absoluteMin: 2048, percentMin: 1, percentOnly: 0.1 },
@@ -973,6 +978,57 @@ function generateDiffsSection(result) {
973978
return content
974979
}
975980

981+
// Find the most recent value for a metric in the KV history.
982+
function getLatestHistoricalValue(history, metricKey) {
983+
if (!history?.entries?.length) return undefined
984+
for (let i = history.entries.length - 1; i >= 0; i--) {
985+
const val = history.entries[i].metrics?.[metricKey]
986+
if (typeof val === 'number') return val
987+
}
988+
return undefined
989+
}
990+
991+
// Generate the dedicated Native Binary section shown after Bundle Sizes.
992+
function generateNativeBinarySection(mainStats, diffStats, history) {
993+
const mainGeneral = mainStats?.General || {}
994+
const diffGeneral = diffStats?.General || {}
995+
996+
const mainVal = mainGeneral.swcBinarySize
997+
const diffVal = diffGeneral.swcBinarySize
998+
999+
// Nothing to show if we don't have any measurement
1000+
if (typeof mainVal !== 'number' && typeof diffVal !== 'number') return ''
1001+
1002+
const mainStr = prettify(mainVal, 'bytes')
1003+
const diffStr = prettify(diffVal, 'bytes')
1004+
const change = formatChange(mainVal, diffVal, 'bytes', 'swcBinarySize')
1005+
const histValues = getHistoricalValues(history, 'swcBinarySize')
1006+
const sparkline = generateTrendBar(histValues)
1007+
1008+
const hasTrend = Boolean(sparkline)
1009+
const header = hasTrend
1010+
? `| Metric | Canary | PR | Change | Trend |
1011+
|:-------|-------:|---:|-------:|:-----:|`
1012+
: `| Metric | Canary | PR | Change |
1013+
|:-------|-------:|---:|-------:|`
1014+
1015+
const row = hasTrend
1016+
? `| SWC Binary Size | ${mainStr} | ${diffStr} | ${change.text} | ${sparkline} |`
1017+
: `| SWC Binary Size | ${mainStr} | ${diffStr} | ${change.text} |`
1018+
1019+
return `<details>
1020+
<summary><strong>🦀 Native Binary</strong></summary>
1021+
1022+
Size of the native SWC binary (\`packages/next-swc/native/*.node\`). The Canary column is the most recent value recorded on the canary branch.
1023+
1024+
${header}
1025+
${row}
1026+
1027+
</details>
1028+
1029+
`
1030+
}
1031+
9761032
function generatePrTarballSection(actionInfo) {
9771033
if (actionInfo.isRelease || !actionInfo.githubHeadSha) return ''
9781034

@@ -1016,6 +1072,24 @@ module.exports = async function addComment(
10161072
const result = results[i]
10171073
const isLastResult = i === results.length - 1
10181074

1075+
// The native SWC binary is shared between the canary and PR checkouts in
1076+
// a single run (the workflow downloads it once and copies it into both),
1077+
// so the in-run "canary" value is identical to the PR value. Override the
1078+
// canary baseline with the last recorded value from KV history so the
1079+
// diff is meaningful. Canary runs skip this and keep the measured value.
1080+
if (!actionInfo.isRelease && result.mainRepoStats?.General) {
1081+
const historicalSwcSize = getLatestHistoricalValue(
1082+
history,
1083+
'swcBinarySize'
1084+
)
1085+
if (typeof historicalSwcSize === 'number') {
1086+
result.mainRepoStats.General.swcBinarySize = historicalSwcSize
1087+
} else {
1088+
// No history yet — hide the canary value so the table renders N/A
1089+
delete result.mainRepoStats.General.swcBinarySize
1090+
}
1091+
}
1092+
10191093
// Add summary showing only significant changes (not collapsed)
10201094
if (i === 0) {
10211095
comment += generateChangeSummary(
@@ -1041,6 +1115,13 @@ module.exports = async function addComment(
10411115
comment += `<details>\n<summary><strong>📦 Bundle Sizes</strong></summary>\n\n${bundleSection}</details>\n\n`
10421116
}
10431117

1118+
// Add native binary size section (not collapsed, small)
1119+
comment += generateNativeBinarySection(
1120+
result.mainRepoStats,
1121+
result.diffRepoStats,
1122+
history
1123+
)
1124+
10441125
// Add diffs (already collapsed)
10451126
comment += generateDiffsSection(result)
10461127

.github/actions/next-stats-action/src/run/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ const collectDiffs = require('./collect-diffs')
99
const { statsAppDir, diffRepoDir } = require('../constants')
1010
const { calcStats } = require('../util/stats')
1111

12+
// Location of the native binary that the workflow copies into the action dir.
13+
// From src/run/index.js → .github/actions/next-stats-action/native
14+
const nativeBinaryDir = path.join(__dirname, '../../native')
15+
16+
// Sum the size of all *.node files in the action's native/ directory.
17+
async function getSwcBinarySize() {
18+
try {
19+
const entries = await fs.readdir(nativeBinaryDir)
20+
let total = 0
21+
let found = 0
22+
for (const entry of entries) {
23+
if (!entry.endsWith('.node')) continue
24+
const stat = await fs.stat(path.join(nativeBinaryDir, entry))
25+
if (stat.isFile()) {
26+
total += stat.size
27+
found++
28+
}
29+
}
30+
if (found === 0) return null
31+
return total
32+
} catch (err) {
33+
logger(`Unable to measure SWC binary size: ${err.message}`)
34+
return null
35+
}
36+
}
37+
1238
// Number of iterations for build benchmarks to get stable median
1339
const BUILD_BENCHMARK_ITERATIONS = 5
1440

@@ -57,6 +83,7 @@ async function runConfigs(
5783
let curStats = {
5884
General: {
5985
nodeModulesSize: null,
86+
swcBinarySize: null,
6087
},
6188
}
6289

@@ -83,6 +110,7 @@ async function runConfigs(
83110
curStats.General.nodeModulesSize = await getDirSize(
84111
path.join(statsAppDir, 'node_modules')
85112
)
113+
curStats.General.swcBinarySize = await getSwcBinarySize()
86114
}
87115

88116
// Run builds for selected bundler(s) and collect stats separately

0 commit comments

Comments
 (0)