|
| 1 | +name: Native Binary Size |
| 2 | + |
| 3 | +permissions: {} |
| 4 | + |
| 5 | +on: |
| 6 | + pull_request: |
| 7 | + types: [opened, synchronize, reopened] |
| 8 | + |
| 9 | +concurrency: |
| 10 | + group: native-binary-size-${{ github.event.pull_request.number }} |
| 11 | + cancel-in-progress: true |
| 12 | + |
| 13 | +defaults: |
| 14 | + run: |
| 15 | + shell: bash |
| 16 | + |
| 17 | +jobs: |
| 18 | + inputs: |
| 19 | + name: Detect native input changes |
| 20 | + if: github.event.pull_request.head.repo.full_name == github.repository |
| 21 | + runs-on: ubuntu-latest |
| 22 | + permissions: |
| 23 | + contents: read |
| 24 | + outputs: |
| 25 | + changed: ${{ steps.compare.outputs.changed }} |
| 26 | + steps: |
| 27 | + - name: Check out base |
| 28 | + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 29 | + with: |
| 30 | + ref: ${{ github.event.pull_request.base.sha }} |
| 31 | + fetch-depth: 0 |
| 32 | + persist-credentials: false |
| 33 | + |
| 34 | + - name: Compute base native input hash |
| 35 | + id: base |
| 36 | + uses: ./.github/actions/compute-native-cache-input-hash |
| 37 | + |
| 38 | + - name: Check out PR |
| 39 | + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 40 | + with: |
| 41 | + ref: ${{ github.event.pull_request.head.sha }} |
| 42 | + fetch-depth: 0 |
| 43 | + persist-credentials: false |
| 44 | + |
| 45 | + - name: Compute PR native input hash |
| 46 | + id: head |
| 47 | + uses: ./.github/actions/compute-native-cache-input-hash |
| 48 | + |
| 49 | + - name: Compare native inputs |
| 50 | + id: compare |
| 51 | + env: |
| 52 | + BASE_HASH: ${{ steps.base.outputs.hash }} |
| 53 | + BASE_SHA: ${{ github.event.pull_request.base.sha }} |
| 54 | + HEAD_HASH: ${{ steps.head.outputs.hash }} |
| 55 | + HEAD_SHA: ${{ github.event.pull_request.head.sha }} |
| 56 | + run: | |
| 57 | + if [[ "$BASE_HASH" != "$HEAD_HASH" ]] || |
| 58 | + ! git diff --quiet "$BASE_SHA" "$HEAD_SHA" -- .github/workflows/vp-binary-size.yml; then |
| 59 | + echo "changed=true" >> "$GITHUB_OUTPUT" |
| 60 | + else |
| 61 | + echo "changed=false" >> "$GITHUB_OUTPUT" |
| 62 | + fi |
| 63 | +
|
| 64 | + build: |
| 65 | + name: Build ${{ matrix.source }} ${{ matrix.settings.label }} |
| 66 | + needs: inputs |
| 67 | + if: needs.inputs.outputs.changed == 'true' |
| 68 | + strategy: |
| 69 | + fail-fast: false |
| 70 | + matrix: |
| 71 | + source: [base, head] |
| 72 | + settings: |
| 73 | + - label: Linux x64 |
| 74 | + builder: upstream |
| 75 | + os: ubuntu-latest |
| 76 | + platform: linux |
| 77 | + target: x86_64-unknown-linux-gnu |
| 78 | + - label: macOS ARM64 |
| 79 | + builder: upstream |
| 80 | + os: macos-latest |
| 81 | + platform: macos |
| 82 | + target: aarch64-apple-darwin |
| 83 | + - label: Windows x64 |
| 84 | + builder: windows-cross |
| 85 | + os: namespace-profile-linux-x64-default |
| 86 | + platform: windows |
| 87 | + target: x86_64-pc-windows-msvc |
| 88 | + runs-on: ${{ matrix.settings.os }} |
| 89 | + permissions: |
| 90 | + contents: read |
| 91 | + env: |
| 92 | + DEBUG: 'napi:*' |
| 93 | + RELEASE_BUILD: 'true' |
| 94 | + VERSION: '0.0.0-native-size' |
| 95 | + steps: |
| 96 | + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 |
| 97 | + with: |
| 98 | + ref: ${{ matrix.source == 'base' && github.event.pull_request.base.sha || github.event.pull_request.head.sha }} |
| 99 | + persist-credentials: false |
| 100 | + |
| 101 | + - uses: ./.github/actions/clone |
| 102 | + |
| 103 | + - uses: oxc-project/setup-rust@68c3199c5339f965e6e163924c3c450773eba42b # main (pending v1.0.17 - Swatinem/rust-cache v2.9.1 for node24) |
| 104 | + if: matrix.settings.builder == 'upstream' |
| 105 | + with: |
| 106 | + save-cache: false |
| 107 | + cache-key: native-binary-size-${{ matrix.source }}-${{ matrix.settings.target }} |
| 108 | + |
| 109 | + - name: Add Rust target |
| 110 | + if: matrix.settings.builder == 'upstream' |
| 111 | + run: rustup target add ${{ matrix.settings.target }} |
| 112 | + |
| 113 | + - uses: oxc-project/setup-node@4c588e9266bd930b6ddc34307df0659ed511d187 # v1.3.1 |
| 114 | + if: matrix.settings.builder == 'upstream' |
| 115 | + |
| 116 | + - name: Build final artifacts |
| 117 | + if: matrix.settings.builder == 'upstream' |
| 118 | + uses: ./.github/actions/build-upstream |
| 119 | + with: |
| 120 | + target: ${{ matrix.settings.target }} |
| 121 | + |
| 122 | + - name: Cross-compile final Windows artifacts |
| 123 | + if: matrix.settings.builder == 'windows-cross' |
| 124 | + uses: ./.github/actions/build-windows-cli |
| 125 | + with: |
| 126 | + artifact-name: windows-cli-binaries-size-${{ matrix.source }} |
| 127 | + save-cache: false |
| 128 | + |
| 129 | + - name: Measure final artifacts |
| 130 | + env: |
| 131 | + PLATFORM: ${{ matrix.settings.platform }} |
| 132 | + SIZE_FILE: ${{ runner.temp }}/native-size-${{ matrix.source }}-${{ matrix.settings.platform }}.json |
| 133 | + SOURCE: ${{ matrix.source }} |
| 134 | + TARGET: ${{ matrix.settings.target }} |
| 135 | + run: | |
| 136 | + node <<'NODE' |
| 137 | + const { existsSync, readFileSync, writeFileSync } = require('node:fs'); |
| 138 | + const { gzipSync } = require('node:zlib'); |
| 139 | +
|
| 140 | + const target = process.env.TARGET; |
| 141 | + const paths = |
| 142 | + process.env.PLATFORM === 'linux' |
| 143 | + ? { |
| 144 | + vp: `target/${target}/release/vp`, |
| 145 | + napi: 'packages/cli/binding/vite-plus.linux-x64-gnu.node', |
| 146 | + } |
| 147 | + : process.env.PLATFORM === 'macos' |
| 148 | + ? { |
| 149 | + vp: `target/${target}/release/vp`, |
| 150 | + napi: 'packages/cli/binding/vite-plus.darwin-arm64.node', |
| 151 | + } |
| 152 | + : { |
| 153 | + trampoline: `target/${target}/release/vp-shim.exe`, |
| 154 | + }; |
| 155 | +
|
| 156 | + const artifacts = {}; |
| 157 | + for (const [name, file] of Object.entries(paths)) { |
| 158 | + if (!existsSync(file)) { |
| 159 | + throw new Error(`Expected final ${name} artifact at ${file}`); |
| 160 | + } |
| 161 | + const contents = readFileSync(file); |
| 162 | + artifacts[name] = { |
| 163 | + path: file, |
| 164 | + raw: contents.length, |
| 165 | + gzip: gzipSync(contents, { level: 9 }).length, |
| 166 | + }; |
| 167 | + } |
| 168 | +
|
| 169 | + const result = { |
| 170 | + source: process.env.SOURCE, |
| 171 | + target, |
| 172 | + artifacts, |
| 173 | + }; |
| 174 | + writeFileSync(process.env.SIZE_FILE, `${JSON.stringify(result, null, 2)}\n`); |
| 175 | + console.log(JSON.stringify(result, null, 2)); |
| 176 | + NODE |
| 177 | +
|
| 178 | + - name: Upload size measurements |
| 179 | + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 |
| 180 | + with: |
| 181 | + name: native-size-${{ matrix.source }}-${{ matrix.settings.platform }} |
| 182 | + path: ${{ runner.temp }}/native-size-${{ matrix.source }}-${{ matrix.settings.platform }}.json |
| 183 | + if-no-files-found: error |
| 184 | + retention-days: 1 |
| 185 | + |
| 186 | + comment: |
| 187 | + name: Report binary size |
| 188 | + needs: build |
| 189 | + if: github.event.pull_request.head.repo.full_name == github.repository |
| 190 | + runs-on: ubuntu-latest |
| 191 | + permissions: |
| 192 | + contents: read |
| 193 | + issues: write |
| 194 | + pull-requests: write |
| 195 | + steps: |
| 196 | + - name: Download size measurements |
| 197 | + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 |
| 198 | + with: |
| 199 | + pattern: native-size-* |
| 200 | + path: ${{ runner.temp }}/native-size |
| 201 | + merge-multiple: true |
| 202 | + |
| 203 | + - name: Comment size comparison |
| 204 | + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 |
| 205 | + env: |
| 206 | + HEAD_SHA: ${{ github.event.pull_request.head.sha }} |
| 207 | + SIZE_DIR: ${{ runner.temp }}/native-size |
| 208 | + with: |
| 209 | + script: | |
| 210 | + const fs = require('node:fs'); |
| 211 | + const path = require('node:path'); |
| 212 | +
|
| 213 | + const marker = '<!-- vp-binary-size -->'; |
| 214 | + const load = (source, platform) => |
| 215 | + JSON.parse( |
| 216 | + fs.readFileSync(path.join(process.env.SIZE_DIR, `native-size-${source}-${platform}.json`)), |
| 217 | + ); |
| 218 | + const baseLinux = load('base', 'linux'); |
| 219 | + const headLinux = load('head', 'linux'); |
| 220 | + const baseMacos = load('base', 'macos'); |
| 221 | + const headMacos = load('head', 'macos'); |
| 222 | + const baseWindows = load('base', 'windows'); |
| 223 | + const headWindows = load('head', 'windows'); |
| 224 | + const artifacts = [ |
| 225 | + { |
| 226 | + name: '`vp` (Linux x64)', |
| 227 | + baseRaw: baseLinux.artifacts.vp.raw, |
| 228 | + baseGzip: baseLinux.artifacts.vp.gzip, |
| 229 | + headRaw: headLinux.artifacts.vp.raw, |
| 230 | + headGzip: headLinux.artifacts.vp.gzip, |
| 231 | + }, |
| 232 | + { |
| 233 | + name: 'NAPI (Linux x64)', |
| 234 | + baseRaw: baseLinux.artifacts.napi.raw, |
| 235 | + baseGzip: baseLinux.artifacts.napi.gzip, |
| 236 | + headRaw: headLinux.artifacts.napi.raw, |
| 237 | + headGzip: headLinux.artifacts.napi.gzip, |
| 238 | + }, |
| 239 | + { |
| 240 | + name: '`vp` (macOS ARM64)', |
| 241 | + baseRaw: baseMacos.artifacts.vp.raw, |
| 242 | + baseGzip: baseMacos.artifacts.vp.gzip, |
| 243 | + headRaw: headMacos.artifacts.vp.raw, |
| 244 | + headGzip: headMacos.artifacts.vp.gzip, |
| 245 | + }, |
| 246 | + { |
| 247 | + name: 'NAPI (macOS ARM64)', |
| 248 | + baseRaw: baseMacos.artifacts.napi.raw, |
| 249 | + baseGzip: baseMacos.artifacts.napi.gzip, |
| 250 | + headRaw: headMacos.artifacts.napi.raw, |
| 251 | + headGzip: headMacos.artifacts.napi.gzip, |
| 252 | + }, |
| 253 | + { |
| 254 | + name: 'Trampoline (Windows x64)', |
| 255 | + baseRaw: baseWindows.artifacts.trampoline.raw, |
| 256 | + baseGzip: baseWindows.artifacts.trampoline.gzip, |
| 257 | + headRaw: headWindows.artifacts.trampoline.raw, |
| 258 | + headGzip: headWindows.artifacts.trampoline.gzip, |
| 259 | + }, |
| 260 | + ]; |
| 261 | +
|
| 262 | + const formatSize = (bytes) => |
| 263 | + `${bytes.toLocaleString('en-US')} B (${(bytes / 1024 / 1024).toFixed(2)} MiB)`; |
| 264 | + const formatDelta = (base, head) => { |
| 265 | + const delta = head - base; |
| 266 | + const percent = base === 0 ? 0 : (delta / base) * 100; |
| 267 | + const sign = delta > 0 ? '+' : ''; |
| 268 | + return `${sign}${delta.toLocaleString('en-US')} B (${sign}${percent.toFixed(2)}%)`; |
| 269 | + }; |
| 270 | +
|
| 271 | + const shortSha = process.env.HEAD_SHA.slice(0, 7); |
| 272 | + const rows = artifacts.flatMap((artifact) => [ |
| 273 | + `| ${artifact.name} | Binary | ${formatSize(artifact.baseRaw)} | ${formatSize(artifact.headRaw)} | ${formatDelta(artifact.baseRaw, artifact.headRaw)} |`, |
| 274 | + `| ${artifact.name} | gzip -9 | ${formatSize(artifact.baseGzip)} | ${formatSize(artifact.headGzip)} | ${formatDelta(artifact.baseGzip, artifact.headGzip)} |`, |
| 275 | + ]); |
| 276 | + const body = [ |
| 277 | + marker, |
| 278 | + '', |
| 279 | + `### Native binary sizes (\`${shortSha}\`)`, |
| 280 | + '', |
| 281 | + 'Final release artifacts built by the canonical `build-upstream` and `build-windows-cli` actions.', |
| 282 | + '', |
| 283 | + '| Artifact | Format | Base | PR | Change |', |
| 284 | + '| --- | --- | ---: | ---: | ---: |', |
| 285 | + ...rows, |
| 286 | + ].join('\n'); |
| 287 | +
|
| 288 | + await core.summary.addRaw(body.replace(marker, '')).write(); |
| 289 | +
|
| 290 | + const comments = await github.paginate(github.rest.issues.listComments, { |
| 291 | + owner: context.repo.owner, |
| 292 | + repo: context.repo.repo, |
| 293 | + issue_number: context.issue.number, |
| 294 | + }); |
| 295 | + const existing = comments.find((comment) => comment.body?.includes(marker)); |
| 296 | + if (existing) { |
| 297 | + await github.rest.issues.updateComment({ |
| 298 | + owner: context.repo.owner, |
| 299 | + repo: context.repo.repo, |
| 300 | + comment_id: existing.id, |
| 301 | + body, |
| 302 | + }); |
| 303 | + } else { |
| 304 | + await github.rest.issues.createComment({ |
| 305 | + owner: context.repo.owner, |
| 306 | + repo: context.repo.repo, |
| 307 | + issue_number: context.issue.number, |
| 308 | + body, |
| 309 | + }); |
| 310 | + } |
0 commit comments