Skip to content

Commit 6eb3ef7

Browse files
authored
perf: cache package managers and use native toolchain setup (#209)
Fresh runners still run a shell installer after a Vite+/Node.js cache hit and download the project package manager. `cache-toolchain: true` now caches the selected managed package manager and activates validated toolchains through the cached CLI. A temporary copy of the validated CLI performs Vite+'s native setup. It creates the activation links, configuration, and shims. `VP_SELF_SETUP_NO_MODIFY_PATH=1` skips persistent shell-profile and user `PATH` changes. The action exports `PATH` for itself and subsequent steps through `GITHUB_PATH`. Native activation requires an exact validated cache hit and no active installation. Failed activation falls back to the normal installer. The selected managed `pnpm`, `npm`, `yarn`, or `bun` installation uses a separate archive. Its key includes the toolchain identity, exact manager version, and a hash of npm configuration. Configuration files and credentials stay outside the archive. Existing installations remain intact; invalid archives fall back to normal installation. Independent directory and manager queries run together. All cache uploads run in parallel and follow `cache-save`. `package-manager-cache-hit` reports the manager archive. `toolchain-cache-hit` retains its Vite+/Node.js meaning. ### Hosted comparison The [GitHub Actions run](https://github.com/voidzero-dev/setup-vp/actions/runs/39234874369) passed all 57 jobs: three warm-ups and six samples per case on each platform. The PR base is [`c025a93`](c025a93). Manager caching with shell setup uses [`f28bb92`](f28bb92). The optimized implementation is [`06fe7ec`](06fe7ec). All cases use fresh runners, the same project checkout, and warm dependency and Vite+/Node.js archives. Both PR implementations also restore the same manager archive. Versions are fixed at `Vite+ 1.3.0`, Node.js `24.22.0`, and `pnpm@12.7.0`. The local `vite-plus` package and `vite` alias also use `1.3.0`. Median setup plus post-action time includes `vp install`. All cases use `cache-save: true` and `sfw: false`. | Runner | PR base | Before optimization | Optimized | Reduction vs before | Reduction vs base | | --- | ---: | ---: | ---: | ---: | ---: | | Linux x64 | 7 s | 9.5 s | 5.5 s | 42.1% | 21.4% | | macOS ARM64 | 15.5 s | 18.5 s | 13.5 s | 27.0% | 12.9% | | Windows x64 | 18 s | 16.5 s | 12 s | 27.3% | 33.3% | ```mermaid xychart-beta title "PR 209: median setup + post time" x-axis ["Linux before", "Linux after", "macOS before", "macOS after", "Windows before", "Windows after"] y-axis "Seconds — lower is better" 0 --> 30 bar [9.5, 5.5, 18.5, 13.5, 16.5, 12] ``` The chart compares the two PR implementations with manager caching enabled. All 18 optimized samples used native activation without installer fallback. All 36 PR samples reported `package-manager-cache-hit: true`. ### Why cache hits could take more time A manager archive adds a lookup, transfer, extraction, and executable check. In the [earlier run](https://github.com/voidzero-dev/setup-vp/actions/runs/39227680298), Windows manager restoration took a median `1.18 s` after the cache match. Dependency-cache discovery saved about `0.91 s`, so archive costs could exceed that saving. The earlier Windows totals also included bootstrap variation: `4.57 s` at the PR base versus `6.19 s` with manager caching. The [shell installer](https://github.com/voidzero-dev/vite-plus/blob/v1.3.0/packages/cli/install.ps1) downloads a standalone CLI before native setup, even with a restored payload. Native setup also [starts PowerShell to update user PATH](https://github.com/voidzero-dev/vite-plus/blob/v1.3.0/crates/vp_global_cli/src/self_setup/shell.rs). The optimized cache path removes these operations. The new run measures this activation phase from toolchain validation to Node.js configuration: | Runner | Before optimization | Optimized | | --- | ---: | ---: | | Linux x64 | 0.605 s | 0.033 s | | macOS ARM64 | 0.739 s | 0.056 s | | Windows x64 | 4.632 s | 0.119 s | The Windows activation saving is about `4.51 s`. Cache transfer and runner variation also affect total time. The full Linux and macOS reductions must not be attributed solely to native activation. <details> <summary>Samples, cache storage, and reproduction</summary> All six samples are in seconds, in sample order: | Runner | PR base | Before optimization | Optimized | | --- | --- | --- | --- | | Linux x64 | 5, 5, 7, 7, 9, 9 | 12, 8, 6, 6, 12, 11 | 7, 5, 6, 6, 4, 5 | | macOS ARM64 | 18, 21, 13, 16, 10, 15 | 9, 10, 18, 23, 19, 22 | 16, 13, 12, 21, 10, 14 | | Windows x64 | 23, 21, 17, 16, 16, 19 | 27, 17, 14, 15, 20, 16 | 11, 12, 12, 12, 11, 18 | GitHub reports step durations in whole seconds; a six-sample median can include half a second. Phase timings use log timestamps. Measurements exclude runner startup, checkout, metadata checks, the project build, and artifact uploads. The [benchmark workflow](https://github.com/voidzero-dev/setup-vp/blob/b0b235ae74110b9aa3637fb2762bf71658986cc9/.github/workflows/package-manager-benchmark.yml) pins all three action commits and the project checkout. It uses isolated cache paths and records each installation. The analysis checks versions, binary hashes, lockfile hashes, runner images, and cache outputs. The benchmark harness stays outside this PR's source diff. The manager archives use `20.6 MiB` on Linux, `17.8 MiB` on macOS, and `20.1 MiB` on Windows. One entry per platform totals about `59 MiB`. </details>
1 parent c025a93 commit 6eb3ef7

17 files changed

Lines changed: 935 additions & 131 deletions

‎.github/workflows/test.yml‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ jobs:
3030
fail-fast: false
3131
matrix:
3232
os: [ubuntu-latest, macos-latest, windows-latest]
33+
manager: ["pnpm@12.14.0", "yarn@1.22.22", "npm@11.6.2", "bun@1.4.2"]
3334
runs-on: ${{ matrix.os }}
3435
timeout-minutes: 10
3536
env:
3637
VP_PR_VERSION: ""
38+
TEST_PACKAGE_MANAGER: ${{ matrix.manager }}
3739
steps:
3840
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
3941
- name: Select a fresh installation directory
@@ -43,12 +45,12 @@ jobs:
4345
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
4446
import { join } from "node:path";
4547
// Both jobs use the same absolute paths, but separate fresh runners.
46-
const home = join(process.env.RUNNER_TEMP, `setup-vp-toolchain-${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`);
48+
const home = join(process.env.RUNNER_TEMP, `setup-vp-toolchain-${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}-${process.env.TEST_PACKAGE_MANAGER}`);
4749
if (existsSync(home)) throw new Error("Expected a fresh installation");
4850
appendFileSync(process.env.GITHUB_ENV, `VP_HOME=${home}\n`);
4951
const project = join(process.env.RUNNER_TEMP, "setup-vp-toolchain-project");
5052
mkdirSync(project, { recursive: true });
51-
writeFileSync(join(project, "package.json"), JSON.stringify({ private: true }));
53+
writeFileSync(join(project, "package.json"), JSON.stringify({ private: true, packageManager: process.env.TEST_PACKAGE_MANAGER }));
5254
NODE
5355
- name: Setup Vite+ with a cache miss
5456
id: setup
@@ -65,25 +67,30 @@ jobs:
6567
working-directory: ${{ runner.temp }}/setup-vp-toolchain-project
6668
env:
6769
CACHE_HIT: ${{ steps.setup.outputs.toolchain-cache-hit }}
70+
PM_CACHE_HIT: ${{ steps.setup.outputs.package-manager-cache-hit }}
6871
VP_VERSION: ${{ steps.setup.outputs.version }}
6972
run: |
7073
test "$CACHE_HIT" = "false"
74+
test "$PM_CACHE_HIT" = "false"
7175
test "$VP_VERSION" = "1.3.0"
7276
test "$(node --version)" = "v22.23.1"
7377
vp_binary=vp
7478
if [ "$RUNNER_OS" = "Windows" ]; then vp_binary=vp.exe; fi
7579
"$vp_binary" fmt --version
80+
"$vp_binary" install --ignore-scripts
7681
7782
test-toolchain-cache-hit:
7883
needs: test-toolchain-cache-miss
7984
strategy:
8085
fail-fast: false
8186
matrix:
8287
os: [ubuntu-latest, macos-latest, windows-latest]
88+
manager: ["pnpm@12.14.0", "yarn@1.22.22", "npm@11.6.2", "bun@1.4.2"]
8389
runs-on: ${{ matrix.os }}
8490
timeout-minutes: 10
8591
env:
8692
VP_PR_VERSION: ""
93+
TEST_PACKAGE_MANAGER: ${{ matrix.manager }}
8794
steps:
8895
- uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2
8996
- name: Select a fresh installation directory
@@ -93,12 +100,12 @@ jobs:
93100
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
94101
import { join } from "node:path";
95102
// Both jobs use the same absolute paths, but separate fresh runners.
96-
const home = join(process.env.RUNNER_TEMP, `setup-vp-toolchain-${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}`);
103+
const home = join(process.env.RUNNER_TEMP, `setup-vp-toolchain-${process.env.GITHUB_RUN_ID}-${process.env.GITHUB_RUN_ATTEMPT}-${process.env.TEST_PACKAGE_MANAGER}`);
97104
if (existsSync(home)) throw new Error("Expected a fresh installation");
98105
appendFileSync(process.env.GITHUB_ENV, `VP_HOME=${home}\n`);
99106
const project = join(process.env.RUNNER_TEMP, "setup-vp-toolchain-project");
100107
mkdirSync(project, { recursive: true });
101-
writeFileSync(join(project, "package.json"), JSON.stringify({ private: true }));
108+
writeFileSync(join(project, "package.json"), JSON.stringify({ private: true, packageManager: process.env.TEST_PACKAGE_MANAGER }));
102109
NODE
103110
- name: Setup Vite+ with a cache hit
104111
id: setup
@@ -116,14 +123,17 @@ jobs:
116123
working-directory: ${{ runner.temp }}/setup-vp-toolchain-project
117124
env:
118125
CACHE_HIT: ${{ steps.setup.outputs.toolchain-cache-hit }}
126+
PM_CACHE_HIT: ${{ steps.setup.outputs.package-manager-cache-hit }}
119127
VP_VERSION: ${{ steps.setup.outputs.version }}
120128
run: |
121129
test "$CACHE_HIT" = "true"
130+
test "$PM_CACHE_HIT" = "true"
122131
test "$VP_VERSION" = "1.3.0"
123132
test "$(node --version)" = "v22.23.1"
124133
vp_binary=vp
125134
if [ "$RUNNER_OS" = "Windows" ]; then vp_binary=vp.exe; fi
126135
"$vp_binary" fmt --version
136+
"$vp_binary" install --ignore-scripts
127137
128138
zizmor:
129139
name: Audit GitHub Actions

‎README.md‎

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ jobs:
344344
| `run-install` | Run `vp install` after setup. Accepts boolean or YAML object with `cwd`/`args` | No | `true` |
345345
| `sfw` | Wrap `vp install` with [Socket Firewall Free](https://docs.socket.dev/docs/socket-firewall-free) (`sfw`) | No | `false` |
346346
| `cache` | Enable caching of project dependencies | No | `false` |
347-
| `cache-toolchain` | Cache the Vite+ payload and managed Node.js runtime in GitHub Actions. Requires exact versions | No | `false` |
347+
| `cache-toolchain` | Cache Vite+, managed Node.js, and the selected package manager in GitHub Actions. Requires exact versions | No | `false` |
348348
| `cache-save` | Save enabled dependency and toolchain caches in the post action | No | `true` |
349349
| `cache-dependency-path` | Path to lock file for cache key generation | No | Auto-detected |
350350
| `registry-url` | Optional registry to set up for auth. Sets the registry in `.npmrc` and reads auth from `NODE_AUTH_TOKEN` | No | |
@@ -377,14 +377,15 @@ Separate package-manager modes require Vite+ 0.3.1+. Any explicit configuration,
377377

378378
## Outputs
379379

380-
| Output | Description |
381-
| ------------------------- | ------------------------------------------------------------------------------------ |
382-
| `version` | The installed version of Vite+ |
383-
| `cache-hit` | Boolean indicating if cache was restored |
384-
| `toolchain-cache-hit` | Whether an exact toolchain cache passed validation (`true` or `false`, when enabled) |
385-
| `node-version` | The Node.js version Vite+ resolved, such as `24.20.0` |
386-
| `package-manager` | The package manager Vite+ resolved, such as `pnpm` or `bun` |
387-
| `package-manager-version` | The version of that package manager, such as `10.33.0` |
380+
| Output | Description |
381+
| --------------------------- | ------------------------------------------------------------------------------------ |
382+
| `version` | The installed version of Vite+ |
383+
| `cache-hit` | Boolean indicating if cache was restored |
384+
| `toolchain-cache-hit` | Whether an exact toolchain cache passed validation (`true` or `false`, when enabled) |
385+
| `package-manager-cache-hit` | Whether the selected package manager passed cache validation (when enabled) |
386+
| `node-version` | The Node.js version Vite+ resolved, such as `24.20.0` |
387+
| `package-manager` | The package manager Vite+ resolved, such as `pnpm` or `bun` |
388+
| `package-manager-version` | The version of that package manager, such as `10.33.0` |
388389

389390
The last three come from `vp env current --json` in `working-directory` at the end of setup. They require Vite+ 0.3.1+ and are `unknown` on older versions or when Vite+ reports no value. With `node-manager: false`, `node-version` is the version of the Node.js found on `PATH`.
390391

@@ -406,7 +407,8 @@ does not restore an installation onto a fresh runner.
406407
### Toolchain cache (GitHub Actions)
407408

408409
Set `cache-toolchain: true` to cache the versioned Vite+ payload and the selected
409-
Node.js runtime. This cache is independent of the project dependency cache. It
410+
Node.js runtime. It also caches the package manager selected for `working-directory`.
411+
This cache is independent of the project dependency cache. It
410412
can restore these files onto a fresh runner before the installer runs.
411413

412414
Use an exact Vite+ version with native self-setup support, or a preview version
@@ -418,10 +420,12 @@ declarations described above. Moving versions such as `latest`, `lts`, and `22`,
418420
disabled Node.js management, and local installer overrides skip this cache.
419421

420422
The cache key includes the OS, architecture, Linux libc, both versions, installation
421-
path, npm registry, and Node.js mirror. There are no fallback keys. The installer
422-
still runs after restoration to create the active links and apply the current
423-
configuration. Configuration, credentials, project dependencies, package stores,
424-
and other Node.js versions are not in this cache.
423+
path, npm registry, and Node.js mirror. There are no fallback keys. On a validated
424+
cache hit with no active installation, the cached CLI runs its native setup to
425+
create links and apply the current configuration. This avoids downloading and
426+
running a shell installer. Other cases use the normal installer, including a
427+
failed native setup. Configuration, credentials, project dependencies, package
428+
stores, and other Node.js versions are not in this cache.
425429

426430
The action checks restored versions, installation paths, dependencies, and the
427431
native binding. An invalid or unavailable cache falls back to installation. If
@@ -432,6 +436,24 @@ time for short jobs; measure both setup and the post action for your workload.
432436
This input is available only in the GitHub Action. GitLab and Azure retain their
433437
existing dependency cache behavior.
434438

439+
The selected managed `pnpm`, `yarn`, `npm`, or `bun` installation uses a separate
440+
archive under the same `cache-toolchain` input. The action asks
441+
`vp env current --json` for the exact version and installation path after setup,
442+
then restores the archive before dependency cache discovery or `vp install`.
443+
A package-manager version change can reuse the Vite+/Node.js archive.
444+
445+
The package-manager key includes the toolchain identity, manager name, resolved
446+
version, and a hash of npm configuration. Configuration files and credentials
447+
are not in the archive. System managers, Node.js bundled npm, unresolved versions,
448+
and unsupported layouts skip this archive. Bundled npm is already in the Node.js
449+
runtime archive. An existing installation is preserved. Invalid archives fall
450+
back to normal installation. The post action saves only complete installations;
451+
`run-install: false` does not install a manager just to fill its cache.
452+
453+
`toolchain-cache-hit` continues to report the Vite+/Node.js archive.
454+
`package-manager-cache-hit` reports the separate manager archive. Both archives
455+
follow `cache-save`, and uploads run in parallel with dependency cache uploads.
456+
435457
#### Toolchain cache storage
436458

437459
For Vite+ `1.3.0` and Node.js `22.23.1`, the
@@ -447,10 +469,26 @@ show these compressed archive sizes per cache entry:
447469
One entry for each platform totals about **244 MiB (0.24 GiB)**. These figures
448470
describe storage in GitHub's cache service. Extracted files require more disk
449471
space on the runner. Sizes vary with the toolchain versions and platform.
472+
The selected package manager adds one archive per key; its size depends on the
473+
manager, version, and platform. The figures above exclude that archive.
474+
475+
For `pnpm@12.7.0`, the
476+
[hosted measurements](https://github.com/voidzero-dev/setup-vp/actions/runs/39227680298)
477+
show these additional compressed archive sizes:
478+
479+
| Runner | Package manager archive |
480+
| ----------------- | ----------------------: |
481+
| Linux x64 (glibc) | 20.6 MiB |
482+
| macOS ARM64 | 17.8 MiB |
483+
| Windows x64 | 20.1 MiB |
484+
485+
One entry per platform adds about **59 MiB**. Other managers and versions can
486+
have different sizes. A package-manager key change creates a new manager
487+
archive and can reuse the existing Vite+/Node.js archive.
450488

451489
A cache hit reuses an existing entry without uploading another copy. Each new
452-
cache key stores a complete archive of both Vite+ and Node.js, even when only one
453-
version changes. Different keys and GitHub branch or pull request scopes can
490+
Vite+/Node.js cache key stores a complete archive of both tools, even when only
491+
one version changes. Different keys and GitHub branch or pull request scopes can
454492
retain separate entries. See [GitHub's cache scope rules](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache).
455493

456494
The project dependency cache (`cache: true`) uses additional storage that depends

‎action.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ inputs:
4646
required: false
4747
default: "true"
4848
cache-toolchain:
49-
description: "Cache the Vite+ installation and managed Node.js runtime before setup. Requires exact Vite+ and Node.js versions. Independent of the project dependency cache."
49+
description: "Cache Vite+, the managed Node.js runtime, and the selected managed package manager. Requires exact Vite+ and Node.js versions. Independent of the project dependency cache."
5050
required: false
5151
default: "false"
5252
cache-dependency-path:
@@ -66,6 +66,8 @@ outputs:
6666
description: "Boolean indicating if cache was restored"
6767
toolchain-cache-hit:
6868
description: "Whether an exact toolchain cache passed validation before installation"
69+
package-manager-cache-hit:
70+
description: "Whether the selected package manager passed cache validation (when cache-toolchain is enabled)"
6971
node-version:
7072
description: "The Node.js version Vite+ resolved for working-directory, from `vp env current` (0.3.1+). `unknown` on older versions or when Vite+ reports none."
7173
package-manager:

0 commit comments

Comments
 (0)