Skip to content
Open
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
129 changes: 129 additions & 0 deletions .github/workflows/build-memory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Measures how much memory a full production build needs and compares it
# against the capacity of the Vercel build machine the docs are running on.
#
# Run this before a release. Adding a documentation version is what moves the
# number, so this answers "do we need a bigger build machine before we ship?"
# while there is still time to change it.
#
# Preview deployments only build English, so they never exercise the Japanese
# locale and cannot catch this. This workflow builds both.

name: 'Docs Build Memory Check'

on:
workflow_dispatch:
inputs:
machine:
description: 'Which Vercel build machine type is ionic-docs on? (Project Settings > Build and Deployment > Build Machine)'
required: true
type: choice
default: Standard
options:
- Standard
- Enhanced
- Turbo
- Elastic

permissions:
contents: read

jobs:
measure:
name: 📏 Measure Production Build Memory
# Must be Linux. `/usr/bin/time -v` is GNU specific, and Vercel builds on
# Linux, so the numbers are comparable.
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: ⚙️ Use Node.js 20
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think Vercel is building this on Node 24, not 20. Their docs say engines.node overrides whatever's picked in Project Settings, and their mapping table puts >=20.0.0 in the 24.x row, resolving to the latest 24.x. This repo's package.json declares exactly that.

If that's right, the measurement is on a different major than production, and GC behavior moves between majors enough to undercut the comparison. The quick fix is node-version: 24. A committed .nvmrc that both this workflow and Vercel read would be the durable one, since there's no .nvmrc or Volta pin right now.


- name: 🕸️ Install Dependencies
run: npm ci --legacy-peer-deps

- name: 🏗️ Build All Locales
env:
# `npm run build` resolves to build:${VERCEL_ENV:-preview}, and
# build:preview is English only. Without this the job would measure
# the half that already fits and would never catch the problem.
VERCEL_ENV: production
# scripts/release-notes.mjs exits non-zero in CI without a token.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# prebuild runs `crowdin upload` when this is non-empty, which pushes
# source strings to Crowdin. This job only measures memory and must
# never publish, so it is pinned empty rather than relying on the
# secret not being exposed to this workflow.
CROWDIN_PERSONAL_TOKEN: ''
run: /usr/bin/time -v -o build-memory.log npm run build
shell: bash

- name: 📊 Report Against Machine Capacity

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This step has no if:, so it inherits success() and gets skipped whenever the build fails, and build-memory.log isn't uploaded anywhere so it goes away with the runner.

That's worth keeping, because the log still has the number in it. GNU time writes its whole report before working out its own exit status, so an OOM-killed build leaves the peak RSS in the log with a Command terminated by signal 9 line in front of it, and exits 137. The awk match already tolerates that preamble. So if: always() plus an upload-artifact step would get you the figure in the case where it's least ambiguous.

env:
MACHINE: ${{ inputs.machine }}
run: |
set -euo pipefail

PEAK_KB=$(awk '/Maximum resident set size/ { print $NF }' build-memory.log)
if [ -z "$PEAK_KB" ]; then
echo "::error::Could not read peak memory from build-memory.log"
exit 1
fi
PEAK_GB=$(awk -v kb="$PEAK_KB" 'BEGIN { printf "%.2f", kb / 1048576 }')

# A build must stay well below a machine's advertised size, for two
# reasons. Vercel's own processes need part of that memory, so the
# nominal figure is not all available: the build that failed during
# the v9 release measured about 8.2 GB and was killed on a nominally
# 8 GB machine. And identical builds vary between runs, measured at
# 7.86 GB and 8.42 GB on the same input.
#
# This margin is a judgment call, not a number published by Vercel.
# It only becomes load bearing when the build comes within a couple
# of GB of a tier boundary.
THRESHOLD_PCT=75

# Smallest fixed tier this build fits on under the threshold. Falls
# back to Turbo, the largest, when nothing fits.
RECOMMENDED=$(awk -v peak="$PEAK_GB" -v t="$THRESHOLD_PCT" 'BEGIN {
split("Standard:8 Enhanced:16 Turbo:60", tiers, " ")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The PR description says there's no hardcoded capacity to keep up to date, but the dropdown only carries the tier name. The capacities are literals here and again in the case below, and the tier count is hardcoded a third time as the i <= 3 bound.

The values are all correct against Vercel's current specs, so nothing's wrong today. It's the next change I'd worry about: patching the case but not this string gives you a correct pass or fail with the wrong recommendation, and adding a fifth option to the dropdown would fall through to Turbo without anyone noticing. Could both halves read one shared tier table instead? Worth fixing the claim in the PR body too, since that's what someone reads first when they come to update it.

for (i = 1; i <= 3; i++) {
split(tiers[i], tier, ":")
if (peak / tier[2] * 100 <= t) { print tier[1]; exit }
}
print "Turbo"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: when nothing fits under the threshold this prints Turbo, which is also the largest tier, so on Turbo you'd get "Move to Turbo before releasing", and the Elastic warning would say the build fits on Turbo when it doesn't. It needs a peak over 45 GB to get here, so it's out of reach on a 16 GB runner today. A distinct "nothing fits" value would stop the message claiming a fit that isn't there, but up to you, this is cosmetic while the runner is the ceiling.

}')

# Elastic has no fixed capacity to check against. It sizes each build
# from recent successful builds, so when earlier builds were killed
# before recording what they needed it can assign a machine smaller
# than the build requires. Report the size, but make no pass or fail
# claim.
if [ "$MACHINE" = "Elastic" ]; then
echo "::warning title=Check the machine Elastic will assign::Elastic sizes each build from recent successful builds, so there is no fixed capacity to check against. Confirm what it will assign under Project Settings > Build and Deployment > Build Machine, where it names the machine your next deployment will use. Otherwise switch to ${RECOMMENDED}, which this ${PEAK_GB} GB build fits on."
exit 0
fi

case "$MACHINE" in
Standard) CAPACITY=8 ;;
Enhanced) CAPACITY=16 ;;
Turbo) CAPACITY=60 ;;
*)
echo "::error::Unknown machine type: $MACHINE"
exit 1
;;
esac

if awk -v cap="$CAPACITY" -v peak="$PEAK_GB" -v t="$THRESHOLD_PCT" 'BEGIN {
exit (peak / cap * 100 > t) ? 1 : 0
}'; then
echo "::notice title=Success! No action needed::Build peaks at ${PEAK_GB} GB, so it should have no issues on the ${MACHINE} machine."
exit 0
fi

echo "::error title=Move to ${RECOMMENDED} before releasing::Build peaks at ${PEAK_GB} GB, too close to the ${CAPACITY} GB ceiling of the ${MACHINE} machine. Vercel needs part of that memory for its own processes, so the build cannot use all of it."
exit 1
shell: bash