Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/actions/next-stats-action/src/add-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably test this by adding something like this to the binary cdylib crate:

#[used]
#[unsafe(no_mangle)]
pub static  LARGE_ARRAY: [u8; 10_000_000] = [1; 10_000_000];

    // elsewhere...
    if std::hint::black_box(&LARGE_ARRAY).len() == 0 {
        eprintln!("{LARGE_ARRAY:?}");
    }

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 ''

Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand Down
28 changes: 28 additions & 0 deletions .github/actions/next-stats-action/src/run/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be more valuable if we either break out per-platform or choose one/two to focus on (maybe x86_64 Linux + aarch64 darwin?). The total size is only somewhat meaningful.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only Linux, since we only build linux

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be good to measure the size when it is stripped and when it is gzipped as well

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

Expand Down Expand Up @@ -57,6 +83,7 @@ async function runConfigs(
let curStats = {
General: {
nodeModulesSize: null,
swcBinarySize: null,
},
}

Expand All @@ -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
Expand Down
Loading