Skip to content

Commit e7e97b0

Browse files
committed
perf: reduce vp binary size
1 parent cfa4061 commit e7e97b0

7 files changed

Lines changed: 327 additions & 226 deletions

File tree

.github/actions/compute-native-cache-input-hash/action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ runs:
4242
'packages/cli/build.ts',
4343
'packages/cli/tsdown.config.ts',
4444
'.github/actions/build-upstream/action.yml',
45+
'.github/actions/clone/action.yml',
4546
'.github/actions/build-windows-cli/action.yml',
4647
'.github/actions/compute-native-cache-input-hash/action.yml',
48+
'.github/actions/download-rolldown-binaries/action.yml',
4749
'.github/actions/setup-xwin/action.yml'
4850
)
4951
}}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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

Comments
 (0)