Skip to content
Open
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
34 changes: 28 additions & 6 deletions .github/workflows/heph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ jobs:
PLUGIN_GO_NAME="heph-go-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext"
PLUGIN_GHA_NAME="heph-gha-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext"
PLUGIN_OCI_NAME="heph-oci-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext"
PLUGIN_JS_NAME="heph-js-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext"
# heph-bench: the perf-regression harness (`perfbench` job). Publishing
# it as a release asset alongside `heph` itself means that job never
# builds anything to compare a PR against its baseline — both the
Expand All @@ -277,6 +278,7 @@ jobs:
echo "plugin_go_name=$PLUGIN_GO_NAME" >> $GITHUB_OUTPUT
echo "plugin_gha_name=$PLUGIN_GHA_NAME" >> $GITHUB_OUTPUT
echo "plugin_oci_name=$PLUGIN_OCI_NAME" >> $GITHUB_OUTPUT
echo "plugin_js_name=$PLUGIN_JS_NAME" >> $GITHUB_OUTPUT
echo "bench_bin_name=$BENCH_BIN_NAME" >> $GITHUB_OUTPUT
echo "debug_bin_name=$DEBUG_BIN_NAME" >> $GITHUB_OUTPUT
OUT="$CARGO_TARGET_DIR/${{ matrix.target }}/release"
Expand All @@ -299,20 +301,21 @@ jobs:
# share nearly the whole workspace dep graph, and under the release
# profile (thin-LTO + opt-level=3) each artifact's final codegen/LTO pass
# is heavy — splitting into separate `cargo build` calls serializes those
# three passes and pays cargo startup + freshness re-resolution each time.
# passes and pays cargo startup + freshness re-resolution each time.
# A single invocation lets cargo's jobserver overlap them (one artifact's
# link tail filling cores while the next codegens). `--bin heph` selects
# the binary; `--lib` adds every selected package's lib target — i.e. both
# cdylibs (heph's own lib is already built as the bin's dependency, so it
# the binary; `--lib` adds every selected package's lib target — i.e. every
# cdylib (heph's own lib is already built as the bin's dependency, so it
# costs nothing extra). `heph-bench` links the same `heph` lib crate, so
# adding it here is marginal — the expensive part (engine + deps) is
# already being compiled for `heph` itself.
TARGETS="--bin heph --bin heph-bench --lib -p heph -p plugin-go-cdylib -p plugin-gha-cdylib -p plugin-oci-cdylib -p bench"
TARGETS="--bin heph --bin heph-bench --lib -p heph -p plugin-go-cdylib -p plugin-gha-cdylib -p plugin-oci-cdylib -p plugin-js-cdylib -p bench"
if [ "${{ matrix.os }}" = "darwin" ]; then
cargo build --release --locked --target ${{ matrix.target }} $TARGETS
lib="$OUT/libplugin_go_cdylib.dylib"
gha_lib="$OUT/libplugin_gha_cdylib.dylib"
oci_lib="$OUT/libplugin_oci_cdylib.dylib"
js_lib="$OUT/libplugin_js_cdylib.dylib"
# The nix toolchain hard-links libiconv against its /nix/store path,
# which is absent on user machines (dyld aborts at launch). Rewrite
# those load commands to the OS /usr/lib copies — for every artifact.
Expand All @@ -321,16 +324,19 @@ jobs:
bash scripts/macos-portable.sh "$lib"
bash scripts/macos-portable.sh "$gha_lib"
bash scripts/macos-portable.sh "$oci_lib"
bash scripts/macos-portable.sh "$js_lib"
else
cargo zigbuild --release --locked --target ${{ matrix.target }} $TARGETS
lib="$OUT/libplugin_go_cdylib.so"
gha_lib="$OUT/libplugin_gha_cdylib.so"
oci_lib="$OUT/libplugin_oci_cdylib.so"
js_lib="$OUT/libplugin_js_cdylib.so"
fi
cp "$OUT/heph-bench" $BENCH_BIN_NAME
cp "$lib" $PLUGIN_GO_NAME
cp "$gha_lib" $PLUGIN_GHA_NAME
cp "$oci_lib" $PLUGIN_OCI_NAME
cp "$js_lib" $PLUGIN_JS_NAME

# `heph` diverges into its two flavours from here — each stamped by
# `patch-flavour.sh` with which one it is (`heph version` / self-upgrade
Expand Down Expand Up @@ -425,6 +431,12 @@ jobs:
name: ${{steps.build.outputs.plugin_oci_name}}
path: ${{steps.build.outputs.plugin_oci_name}}

- name: Upload js plugin artifact
uses: actions/upload-artifact@v6
with:
name: ${{steps.build.outputs.plugin_js_name}}
path: ${{steps.build.outputs.plugin_js_name}}

upload_artifacts:
name: Pre-release
needs: [gen, govet, build]
Expand All @@ -449,8 +461,9 @@ jobs:
uses: actions/download-artifact@v7
with:
# Matches the CLI, both release flavours (`heph_<os>_<arch>` std,
# `heph_debug_<os>_<arch>` debug) and the go plugin cdylib
# (`heph-go-plugin_<os>_<arch>.{so,dylib}`); excludes the `repo` source artifact.
# `heph_debug_<os>_<arch>` debug) and every plugin cdylib
# (`heph-{go,gha,oci,js}-plugin_<os>_<arch>.{so,dylib}`); excludes
# the `repo` source artifact.
pattern: "heph*"
path: dist
merge-multiple: true
Expand Down Expand Up @@ -488,6 +501,15 @@ jobs:
-out "$GITHUB_WORKSPACE/dist/heph-oci-plugin.json" )
cat dist/heph-oci-plugin.json
echo "manifest checksum: $(cat dist/heph-oci-plugin.json.sha256)"
# The js plugin ships the same way: one manifest entry per
# per-os/arch cdylib in dist/, published as a release asset. A real
# workspace opts in the same way as go/gha:
# `plugins: - { identifier: { url: .../heph-js-plugin.json } }`.
( cd tools/pluginmanifest && go run . -name js -version "$VERSION" \
-prefix heph-js-plugin -from-dir "$GITHUB_WORKSPACE/dist" -url-base "$BASE" \
-out "$GITHUB_WORKSPACE/dist/heph-js-plugin.json" )
cat dist/heph-js-plugin.json
echo "manifest checksum: $(cat dist/heph-js-plugin.json.sha256)"

- name: Create Release in artifacts repo
id: create_release
Expand Down
75 changes: 72 additions & 3 deletions .github/workflows/perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ jobs:
PERFBENCH_LAYERS: "10"
PERFBENCH_FAN_OUT: "4"
PERFBENCH_GO_PACKAGES: "60"
# Same corpus size as go's — no measured reason yet to size the two
# differently, and keeping them equal makes go-vs-js deltas easier to
# read at a glance.
PERFBENCH_JS_PACKAGES: "60"
# 3 was too few to tell signal from CI-runner noise (the first live
# runs showed exactly that). Interleaving (see the run steps below)
# fixes systematic drift between candidate/baseline; more reps is what
Expand Down Expand Up @@ -240,6 +244,21 @@ jobs:
echo "no heph-bench asset in ${{ steps.baseline_release.outputs.version }} — predates publishing it, Tier A skipped"
echo "bench_ok=false" >> "$GITHUB_OUTPUT"
fi
# js plugin cdylib, fetched separately and best-effort for the same
# bootstrap reason as heph-bench above: baseline releases published
# before the bench js/go/both selector landed have no
# heph-js-plugin_<os>_<arch> asset at all, and that must not fail
# this whole step (go's Tier B comparison must still run).
if gh release download "${{ steps.baseline_release.outputs.version }}" \
--repo hephbuild/heph-artifacts-v1 \
--dir baseline-dist-raw \
--pattern "heph-js-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext"; then
cp "baseline-dist-raw/heph-js-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext" "baseline-dist/heph-js-plugin.$ext"
echo "js_ok=true" >> "$GITHUB_OUTPUT"
else
echo "no heph-js-plugin asset in ${{ steps.baseline_release.outputs.version }} — predates publishing it, JS Tier B skipped"
echo "js_ok=false" >> "$GITHUB_OUTPUT"
fi

- name: Download candidate (N) artifacts
uses: actions/download-artifact@v7
Expand All @@ -255,6 +274,11 @@ jobs:
cp "candidate-dist-raw/heph_${{ matrix.os }}_${{ matrix.arch }}" candidate-dist/heph
cp "candidate-dist-raw/heph-bench_${{ matrix.os }}_${{ matrix.arch }}" candidate-dist/heph-bench
cp "candidate-dist-raw/heph-go-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext" "candidate-dist/heph-go-plugin.$ext"
# Candidate is always this same run's `build` job — no bootstrap gap
# like baseline's (see "Fetch baseline" above), so this is
# unconditional: the js plugin cdylib is always present here once
# the `build` job publishes it.
cp "candidate-dist-raw/heph-js-plugin_${{ matrix.os }}_${{ matrix.arch }}.$ext" "candidate-dist/heph-js-plugin.$ext"
chmod +x candidate-dist/heph candidate-dist/heph-bench

- name: Generate corpus
Expand All @@ -264,6 +288,7 @@ jobs:
--targets "$PERFBENCH_TARGETS" --packages "$PERFBENCH_PACKAGES" \
--layers "$PERFBENCH_LAYERS" --fan-out "$PERFBENCH_FAN_OUT" \
--go-packages "$PERFBENCH_GO_PACKAGES" \
--js-packages "$PERFBENCH_JS_PACKAGES" \
--out bench-corpus

# `heph-bench run inprocess`/`run dist` are themselves the orchestrator
Expand Down Expand Up @@ -301,6 +326,41 @@ jobs:
--out-candidate "candidate_dist_${scenario}.json" --out-baseline "baseline_dist_${scenario}.json"
done

# Separate `--lang js` invocation (rather than folding into the go step
# above via `--lang both`) so `distjs` is its own tier below, gated on
# its own `js_ok` (baseline releases predating the js plugin cdylib
# must not sink the go comparison). The "Compare and report" jq merge
# below flattens `.scenarios[]` (not a hardcoded `[0]`) specifically so
# a future `--lang both` invocation — two `ScenarioResult`s per file —
# merges both instead of silently dropping the second.
#
# KNOWN GAP, disclosed rather than silently left: every generated js
# package always lists a `js_lint` target (`oxlint` by default) and,
# once it has a usable entry point, a `js_bundle` target (`esbuild`) —
# see `crates/plugin-js/src/pluginjs/provider.rs`'s `list()`. Neither
# tool is installed on these runners (no `setup-node`/`oxlint`/`esbuild`
# anywhere in this repo's workflows or `devenv.nix` as of this change),
# and this plugin has "no hermetic toolchain yet" for either (its own
# error message says so) — so this step will fail with "no oxlint/
# esbuild binary found" every run until a js toolchain is provisioned
# for CI. `continue-on-error: true` + "Compare and report"'s missing-
# file handling absorb that the same way a missing baseline artifact
# is absorbed elsewhere in this job: the `distjs` tier reports "no
# results", nothing else in this job is affected. Provisioning that
# toolchain is a separate, larger decision (which linter/bundler
# version, hermetic vs host) left for a follow-up.
- name: Time Tier B (dist) JS scenarios
if: steps.baseline_fetch.outputs.ok == 'true' && steps.baseline_fetch.outputs.js_ok == 'true'
continue-on-error: true
run: |
for scenario in cold full-hit incremental; do
./candidate-dist/heph-bench run dist \
--candidate-dist candidate-dist --baseline-dist baseline-dist \
--corpus bench-corpus --scenario "$scenario" --warmup 1 --reps "$PERFBENCH_REPS" \
--lang js \
--out-candidate "candidate_distjs_${scenario}.json" --out-baseline "baseline_distjs_${scenario}.json"
done

# Every scenario/tier combination is always attempted and reported —
# `--allow-regression` keeps an individual `compare` call from ever
# exiting non-zero, so one regression doesn't cut the table short. The
Expand Down Expand Up @@ -330,13 +390,17 @@ jobs:
echo ""
echo "_FAILED: no published release found for baseline \`${{ steps.base.outputs.sha }}\`_"
else
for tier in inproc dist; do
for tier in inproc dist distjs; do
echo ""
echo "## Tier: $tier"
if [ "$tier" = "inproc" ] && [ "${{ steps.baseline_fetch.outputs.bench_ok }}" != "true" ]; then
echo "_skipped: baseline release predates publishing \`heph-bench\`_"
continue
fi
if [ "$tier" = "distjs" ] && [ "${{ steps.baseline_fetch.outputs.js_ok }}" != "true" ]; then
echo "_skipped: baseline release predates publishing the js plugin cdylib_"
continue
fi
base_files=()
cand_files=()
for scenario in cold full-hit incremental; do
Expand All @@ -351,8 +415,13 @@ jobs:
echo "_no results (run step failed — see logs)_"
continue
fi
jq -s '{tier: .[0].tier, scenarios: [.[].scenarios[0]]}' "${base_files[@]}" > "merged_baseline_${tier}.json"
jq -s '{tier: .[0].tier, scenarios: [.[].scenarios[0]]}' "${cand_files[@]}" > "merged_candidate_${tier}.json"
# `.scenarios[]`, not a hardcoded `.scenarios[0]`: a
# single-scenario result file (every tier today) still
# yields exactly one element, but a future `--lang both`
# result file (two `ScenarioResult`s per file) merges both
# instead of silently keeping only the first.
jq -s '{tier: .[0].tier, scenarios: [.[].scenarios[]]}' "${base_files[@]}" > "merged_baseline_${tier}.json"
jq -s '{tier: .[0].tier, scenarios: [.[].scenarios[]]}' "${cand_files[@]}" > "merged_candidate_${tier}.json"
"$BENCH" compare --baseline "merged_baseline_${tier}.json" --candidate "merged_candidate_${tier}.json" \
--thresholds "$GITHUB_WORKSPACE/.github/perfbench-thresholds.json" \
--json "verdicts/${tier}.json" \
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions crates/bench-corpus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ serde_json = "1.0"

[dev-dependencies]
tempfile = "3"
# Test-only: parse the generated `pnpm-workspace.yaml` / match its globs
# against generated package dirs using the exact crate+version
# `crates/plugin-js/src/pluginjs/workspace.rs` uses, so the discoverability
# assertion in `js_only_corpus_matches_manifest_and_workspace_shape` proves
# something about the real provider's matching semantics, not a hand-rolled
# approximation of them.
serde_yaml = "0.9"
wax = "0.7"
Loading
Loading