mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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 <[email protected]> Co-authored-by: Claude <[email protected]>
This commit is contained in:
co-authored by
Tobias Koppers
Claude
parent
87e7b083b8
commit
bb2a4457b6
@@ -55,6 +55,7 @@ const METRIC_LABELS = {
|
||||
buildDurationCachedTurbo: 'Turbo Build Time (cached)',
|
||||
// General metrics
|
||||
nodeModulesSize: 'node_modules Size',
|
||||
swcBinarySize: 'SWC Binary Size',
|
||||
}
|
||||
|
||||
// Group configuration for organizing the comment
|
||||
@@ -209,6 +210,10 @@ const METRIC_THRESHOLDS = {
|
||||
// <10KB AND <1%, OR <0.01%
|
||||
nodeModulesSize: { absoluteMin: 10240, percentMin: 1, percentOnly: 0.01 },
|
||||
|
||||
// SWC native binary (~tens of MB): deterministic, but smaller baseline
|
||||
// <10KB AND <0.5%, OR <0.05%
|
||||
swcBinarySize: { absoluteMin: 10240, percentMin: 0.5, percentOnly: 0.05 },
|
||||
|
||||
// Bundle sizes (KB-MB): deterministic
|
||||
// <2KB AND <1%, OR <0.1%
|
||||
bytes: { absoluteMin: 2048, percentMin: 1, percentOnly: 0.1 },
|
||||
@@ -973,6 +978,57 @@ function generateDiffsSection(result) {
|
||||
return content
|
||||
}
|
||||
|
||||
// Find the most recent value for a metric in the KV history.
|
||||
function getLatestHistoricalValue(history, metricKey) {
|
||||
if (!history?.entries?.length) return undefined
|
||||
for (let i = history.entries.length - 1; i >= 0; i--) {
|
||||
const val = history.entries[i].metrics?.[metricKey]
|
||||
if (typeof val === 'number') return val
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Generate the dedicated Native Binary section shown after Bundle Sizes.
|
||||
function generateNativeBinarySection(mainStats, diffStats, history) {
|
||||
const mainGeneral = mainStats?.General || {}
|
||||
const diffGeneral = diffStats?.General || {}
|
||||
|
||||
const mainVal = mainGeneral.swcBinarySize
|
||||
const diffVal = diffGeneral.swcBinarySize
|
||||
|
||||
// Nothing to show if we don't have any measurement
|
||||
if (typeof mainVal !== 'number' && typeof diffVal !== 'number') return ''
|
||||
|
||||
const mainStr = prettify(mainVal, 'bytes')
|
||||
const diffStr = prettify(diffVal, 'bytes')
|
||||
const change = formatChange(mainVal, diffVal, 'bytes', 'swcBinarySize')
|
||||
const histValues = getHistoricalValues(history, 'swcBinarySize')
|
||||
const sparkline = generateTrendBar(histValues)
|
||||
|
||||
const hasTrend = Boolean(sparkline)
|
||||
const header = hasTrend
|
||||
? `| Metric | Canary | PR | Change | Trend |
|
||||
|:-------|-------:|---:|-------:|:-----:|`
|
||||
: `| Metric | Canary | PR | Change |
|
||||
|:-------|-------:|---:|-------:|`
|
||||
|
||||
const row = hasTrend
|
||||
? `| SWC Binary Size | ${mainStr} | ${diffStr} | ${change.text} | ${sparkline} |`
|
||||
: `| SWC Binary Size | ${mainStr} | ${diffStr} | ${change.text} |`
|
||||
|
||||
return `<details>
|
||||
<summary><strong>🦀 Native Binary</strong></summary>
|
||||
|
||||
Size of the native SWC binary (\`packages/next-swc/native/*.node\`). The Canary column is the most recent value recorded on the canary branch.
|
||||
|
||||
${header}
|
||||
${row}
|
||||
|
||||
</details>
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
function generatePrTarballSection(actionInfo) {
|
||||
if (actionInfo.isRelease || !actionInfo.githubHeadSha) return ''
|
||||
|
||||
@@ -1016,6 +1072,24 @@ module.exports = async function addComment(
|
||||
const result = results[i]
|
||||
const isLastResult = i === results.length - 1
|
||||
|
||||
// The native SWC binary is shared between the canary and PR checkouts in
|
||||
// a single run (the workflow downloads it once and copies it into both),
|
||||
// so the in-run "canary" value is identical to the PR value. Override the
|
||||
// canary baseline with the last recorded value from KV history so the
|
||||
// diff is meaningful. Canary runs skip this and keep the measured value.
|
||||
if (!actionInfo.isRelease && result.mainRepoStats?.General) {
|
||||
const historicalSwcSize = getLatestHistoricalValue(
|
||||
history,
|
||||
'swcBinarySize'
|
||||
)
|
||||
if (typeof historicalSwcSize === 'number') {
|
||||
result.mainRepoStats.General.swcBinarySize = historicalSwcSize
|
||||
} else {
|
||||
// No history yet — hide the canary value so the table renders N/A
|
||||
delete result.mainRepoStats.General.swcBinarySize
|
||||
}
|
||||
}
|
||||
|
||||
// Add summary showing only significant changes (not collapsed)
|
||||
if (i === 0) {
|
||||
comment += generateChangeSummary(
|
||||
@@ -1041,6 +1115,13 @@ module.exports = async function addComment(
|
||||
comment += `<details>\n<summary><strong>📦 Bundle Sizes</strong></summary>\n\n${bundleSection}</details>\n\n`
|
||||
}
|
||||
|
||||
// Add native binary size section (not collapsed, small)
|
||||
comment += generateNativeBinarySection(
|
||||
result.mainRepoStats,
|
||||
result.diffRepoStats,
|
||||
history
|
||||
)
|
||||
|
||||
// Add diffs (already collapsed)
|
||||
comment += generateDiffsSection(result)
|
||||
|
||||
|
||||
@@ -9,6 +9,32 @@ const collectDiffs = require('./collect-diffs')
|
||||
const { statsAppDir, diffRepoDir } = require('../constants')
|
||||
const { calcStats } = require('../util/stats')
|
||||
|
||||
// Location of the native binary that the workflow copies into the action dir.
|
||||
// From src/run/index.js → .github/actions/next-stats-action/native
|
||||
const nativeBinaryDir = path.join(__dirname, '../../native')
|
||||
|
||||
// Sum the size of all *.node files in the action's native/ directory.
|
||||
async function getSwcBinarySize() {
|
||||
try {
|
||||
const entries = await fs.readdir(nativeBinaryDir)
|
||||
let total = 0
|
||||
let found = 0
|
||||
for (const entry of entries) {
|
||||
if (!entry.endsWith('.node')) continue
|
||||
const stat = await fs.stat(path.join(nativeBinaryDir, entry))
|
||||
if (stat.isFile()) {
|
||||
total += stat.size
|
||||
found++
|
||||
}
|
||||
}
|
||||
if (found === 0) return null
|
||||
return total
|
||||
} catch (err) {
|
||||
logger(`Unable to measure SWC binary size: ${err.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Number of iterations for build benchmarks to get stable median
|
||||
const BUILD_BENCHMARK_ITERATIONS = 5
|
||||
|
||||
@@ -57,6 +83,7 @@ async function runConfigs(
|
||||
let curStats = {
|
||||
General: {
|
||||
nodeModulesSize: null,
|
||||
swcBinarySize: null,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -83,6 +110,7 @@ async function runConfigs(
|
||||
curStats.General.nodeModulesSize = await getDirSize(
|
||||
path.join(statsAppDir, 'node_modules')
|
||||
)
|
||||
curStats.General.swcBinarySize = await getSwcBinarySize()
|
||||
}
|
||||
|
||||
// Run builds for selected bundler(s) and collect stats separately
|
||||
|
||||
Reference in New Issue
Block a user