Skip to content

perf(uv): rebuild fbuild ~14x faster on no-source-change reinstall - #743

Merged
zackees merged 1 commit into
mainfrom
perf/uv-rebuild-speedup
Jun 21, 2026
Merged

perf(uv): rebuild fbuild ~14x faster on no-source-change reinstall#743
zackees merged 1 commit into
mainfrom
perf/uv-rebuild-speedup

Conversation

@zackees

@zackeeszackees commented Jun 21, 2026

Copy link
Copy Markdown
Member

Summary

  • uv sync --reinstall-package fbuild (and version bumps / lockfile churn that trigger the same path): 14.9s → 1.1s (13.6×). PEP 517 build isolation used to copy the source tree to a temp dir and discard cargo's incremental cache; now CARGO_TARGET_DIR is pinned + a mtime-skip short-circuits cargo entirely when the staged binary is already current.
  • Editable installs of fbuild no longer silently use a stale _native.pyd after .rs edits: cache-keys watches crates/**/*.rs, Cargo.{toml,lock}, pyproject.toml, so uv run triggers re-sync on Rust changes. The real-edit rebuild is at the cargo + linker floor (~14s on Windows); pushing further needs a linker swap, which is a separate change.
  • No regression on the warm uv run path (~100ms either way).

Numbers

ScenarioBeforeAfter
Forced reinstall, no source change14.9s1.1s
Real .rs edit + uv run python --version15.7s14.3s
Warm uv run python --version110ms100ms

Full methodology + raw ci/bench_uv_run.py JSON in ci/bench-results/REPORT.md.

Files

  • setup.pyCARGO_TARGET_DIR pin (separate from <repo>/target/ so dev-CLI soldr cargo runs don't churn the same cache); _staged_binary_is_up_to_date mtime check in BuildWithCargo.run.
  • pyproject.toml[tool.uv] no-build-isolation-package = ["fbuild"], default-groups = ["dev"], setuptools added to dev group (no chicken-and-egg with no-build-isolation), cache-keys so editable installs catch Rust source changes.
  • ci/bench_uv_run.py + ci/bench-results/ — benchmark script + raw timings + writeup.

Test plan

  • uv sync --reinstall-package fbuild with no source change → measured 1.1s
  • Append \n to crates/fbuild-core/src/lib.rs then uv run python --version → measured 14.3s, fresh binary
  • Warm uv run python --version → ~100ms
  • from fbuild.api import SerialMonitor succeeds against the rebuilt wheel
  • Linux/macOS — not exercised locally; CI matrix will catch regressions

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Added benchmarking infrastructure and performance reports to measure and track development rebuild efficiency across various workflows.
    • Optimized the build system with persistent caching and incremental build detection to reduce development rebuild times.

The slow path was `uv sync --reinstall-package fbuild` (also fires on
version bumps / lockfile churn): 14.9s of cargo work even when no Rust
source had changed, because PEP 517 build isolation copies the source
tree to a temp dir each time and cargo's incremental cache lives at
`<cwd>/target/` — discarded after the build.
Three coordinated changes:
- `setup.py`: pin `CARGO_TARGET_DIR` to `~/.fbuild/cargo-target/wheel-build`
so the incremental cache survives. Add an mtime-skip in
`BuildWithCargo.run` that short-circuits cargo entirely when the
staged `ci/bin/fbuild[.exe]` is newer than every `crates/**/*.rs`,
`Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`.
- `pyproject.toml`: `[tool.uv] no-build-isolation-package = ["fbuild"]`
so the build runs in the real repo (not a temp copy). `default-groups
= ["dev"]` + setuptools in the `dev` group so setuptools is in the
venv when uv tries to rebuild the wheel (no chicken-and-egg).
- `pyproject.toml`: `cache-keys = [..., "crates/**/*.rs", ...]` so uv's
editable install actually re-syncs on Rust edits — without this the
compiled `_native.pyd` silently stayed stale until manual reinstall.
Measurements (on Windows, soldr 0.7.55, uv 0.8.11):
| Scenario | Before | After |
|-------------------------------------------|--------:|--------:|
| forced reinstall, no source change | 14.9s | 1.1s |
| real `.rs` edit + `uv run python --ver` | 15.7s | 14.3s |
| warm `uv run python --version` | ~110ms | ~100ms |
The 13.6x win is on the no-source-change path (mtime-skip never
invokes cargo). The real-edit path is at cargo's incremental + linker
floor on this workspace; pushing further would need a faster linker
(rust-lld/mold) — separate change.
See `ci/bench-results/REPORT.md` for full methodology and raw timings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zackees
zackees merged commit 325d8c3 into mainJun 21, 2026
84 of 91 checks passed
@coderabbitai

coderabbitaiBot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c5386ed-3358-48e7-99d4-9158db031834

📥 Commits

Reviewing files that changed from the base of the PR and between a5baf02 and f8b7160.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • ci/bench-results/README.md
  • ci/bench-results/REPORT.md
  • ci/bench-results/after_fixes.json
  • ci/bench-results/baseline.json
  • ci/bench_uv_run.py
  • pyproject.toml
  • setup.py

📝 Walkthrough

Walkthrough

Adds a persistent CARGO_TARGET_DIR and staged-binary freshness check to setup.py so BuildWithCargo.run() skips Cargo when the binary is already up-to-date. Updates pyproject.toml to add fbuild/fbuild.api package config, uv cache-keys tied to Rust sources, and a dev dependency group. Introduces ci/bench_uv_run.py to measure uv sync/uv run timing across several dev-loop scenarios, and commits baseline and after-fix JSON results plus a full optimization report.

Changes

uv run Rebuild Speedup

Layer / File(s)Summary
Cargo fast-path and packaging config
setup.py, pyproject.toml
setup.py sets a stable per-user CARGO_TARGET_DIR, adds helpers to enumerate Cargo inputs and check staged-binary mtime, and short-circuits BuildWithCargo.run() when the binary is fresh. pyproject.toml adds a dev dependency group with setuptools>=64, disables build isolation for fbuild, extends uv cache-keys to Rust sources and Cargo manifests, maps fbuild/fbuild.api packages to python/, and ships _native.pyd/_native.so as package data.
Benchmark script
ci/bench_uv_run.py
Defines time_command, touch, _bench_env, warm_state, force_rebuild, and measure_scenario helpers. main() runs all scenario groups (warm, Rust/Python/both touched, forced reinstall clean/dirty), writes structured JSON to ci/bench-results/<label>.json, and returns process status.
Benchmark result artifacts and documentation
ci/bench-results/README.md, ci/bench-results/REPORT.md, ci/bench-results/baseline.json, ci/bench-results/after_fixes.json
Commits measured timing data for all scenarios before and after the build fixes, a README describing the directory layout and reproduction command, and a REPORT documenting root causes, applied changes, measured results, and remaining optimization items.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • FastLED/fbuild#458: Also modifies setup.py's Rust-build and binary-staging flow, making it directly related to this PR's new _staged_binary_is_up_to_date short-circuit and CARGO_TARGET_DIR pinning.

Poem

🐇 A bunny once waited for Cargo to spin,
Now the binary's cached — no rebuild again!
We touched a .rs, peeked at the mtime,
"Already fresh!" said the script, working fine.
JSON results tell the story complete:
Warm runs go fast, and stale builds retreat. 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/uv-rebuild-speedup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zackees
zackees deleted the perf/uv-rebuild-speedup branch June 21, 2026 23:49
zackees added a commit that referenced this pull request Jun 22, 2026
- Stop tracking ad-hoc `port_scan_*.txt` captures. PR #741 added the
`fbuild port scan` command; users sometimes redirect its output to a
local file for diagnostic comparison. Those captures aren't useful
to anyone else and never belonged in the working tree.
- Refresh `ci/bench-results/REPORT.md` to cover the full cumulative
story after PR #743 and PR #744:
- No-source-change reinstall: 14.9s -> 1.1s (13.6x, PR #743).
- Real `.rs` edit + uv run: 100.1s -> 18.9s (5.3x, PR #744).
- The remaining ~14s touch-only floor is soldr/zccache wrapper
overhead; profiled it down to the second and filed upstream as
zackees/soldr#883.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackees added a commit to zackees/template-python-rust-cmd that referenced this pull request Jun 22, 2026
…tems 4+5) (#5)
Two complementary changes that together let cargo's incremental
fingerprint cache survive across `pip install .` / `uv build`
invocations. Ported from FastLED/fbuild#743.
1. `os.environ.setdefault("CARGO_TARGET_DIR", ~/.template-python-rust-cmd/cargo-target/wheel-build)`
at the top of `ci/build_wheel.py`. PEP 517 isolated builds copy the
source tree to a temp dir and throw `<temp>/target/` away after
each install — every install runs cargo cold (25-30s wall-clock
without this). Pinning to a stable home-dir path preserves the
fingerprint across invocations. Deliberately separate from
`<repo>/target/` so iteration with bare `cargo check` doesn't churn
the wheel-build cache and vice versa.
2. `[tool.uv] no-build-isolation-package = ["template-python-rust-cmd"]`
in `pyproject.toml`. Without this, `uv build` (and `uv pip install
.`) copy the source tree to a temp dir, and (a) cargo's incremental
cache lands in that temp dir then gets discarded, (b) the future
mtime-skip fast path can't see the staged binary because it's
outside the temp tree. `maturin` already ships in the dev
dependency group, so the build env has what it needs without
isolation.
3. Refactor: pull `_cargo_target_root()` out so it respects
`CARGO_TARGET_DIR`. Previously hardcoded `ROOT / "target" /
"release"` — would have masked the pinned path.
Refs #2 (items 4 + 5).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
zackees added a commit to zackees/template-python-rust-cmd that referenced this pull request Jun 22, 2026
… items 6+8) (#6)
Two upgrades to `ci/build_wheel.py` ported from FastLED/fbuild#743.
1. mtime-skip fast path. `_staged_binary_is_up_to_date()` walks the
workspace's cargo inputs (Cargo.toml, Cargo.lock, rust-toolchain.toml,
crates/**/Cargo.toml, crates/**/*.rs) and compares each st_mtime
against the staged `src/template_python_rust_cmd/_bin/template-cli`.
If staged is newer, skip cargo entirely — `_ensure_staged_cli_binary()`
returns the already-staged file. Triggered on no-op reinstalls
(version bumps, lockfile churn, --reinstall-package). Even cargo's
"Fresh" pass walks the workspace and burns wall-clock seconds; an
mtime check is milliseconds. fbuild measured 14.9s → 1.1s on its
forced reinstall path.
2. Structured artifact discovery. `build_cli_binary()` now invokes
cargo with `--message-format=json-render-diagnostics` and walks the
JSON artifact stream for `reason == "compiler-artifact"` +
`target.name == "template-cli"` + non-null `executable`. Fallback:
`_find_cli_executable_by_search()` probes the target root (respecting
CARGO_TARGET_DIR) and every per-host-triple subdir for cases where
cargo emits no compiler-artifact line (fully cached `Fresh` runs) or
where the build is configured with a host triple (CARGO_BUILD_TARGET).
Replaces the previous hardcoded `target/release/template-cli` path,
which would have masked the pinned CARGO_TARGET_DIR landed in the
previous PR.
Refs #2 (items 6 + 8).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
zackees added a commit that referenced this pull request Jun 22, 2026
….pth (not a PEP 660 finder) (#748)
The per-package `package-dir = {fbuild = "python/fbuild",
"fbuild.api" = "python/fbuild/api"}` map worked for the wheel build but
made setuptools' editable install emit a PEP 660 meta-path finder
(`__editable___fbuild_*_finder.py`) whose `MAPPING` only registered
`fbuild`. `fbuild.api` resolved at runtime via
`importlib.machinery.PathFinder` — fine for `import fbuild.api`, but
invisible to static analyzers (ty, pyright, mypy) that walk
`top_level.txt` and `.pth` files. Downstream consumers (FastLED hit this
on `from fbuild.api import SerialMonitor`) saw
`Cannot resolve imported module 'fbuild.api'` even though runtime imports
worked.
Replace the per-package map with the canonical src-layout pattern
`package-dir = {"" = "python"}`. Setuptools now emits a plain
`.pth` pointing at `python/`, which every static analyzer handles, and
the wheel-build path is unaffected.
Verified:
- Wheel (`python -m build`): byte-for-byte identical to baseline
(10,454,675 B). Same contents: `fbuild/__init__.py`,
`fbuild/_native.pyd`, `fbuild/api/__init__.py`,
`fbuild-2.2.31.data/scripts/fbuild.exe`, dist-info.
- Sdist: +2,811 B / +0.12%, entirely from `fbuild.egg-info/` relocating
to `python/fbuild.egg-info/`. Cosmetic.
- `ci/publish.py::build_wheel` is independent of setuptools — it
hand-rolls wheels from the hard-coded `PYTHON_SHIMS_DIR = ROOT /
"python"`. Smoke-tested: still finds the same 2 shims
(`fbuild/__init__.py`, `fbuild/api/__init__.py`). Release path
unaffected.
- Editable reinstall path: ~8s (FastLED → fbuild via uv sources),
within noise of baseline. No regression to the perf work in #743 /
#744.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

1 participant

@zackees