trusty-mpm 1.7.7

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
Documentation
---
name: rust-build-performance
description: "Practical Rust build-performance discipline for the inner dev loop: cargo check first, measure with --timings before tuning, trim the dependency/feature graph, preserve incremental compilation, and use sccache across worktrees. Use when a Rust build feels slow or before reaching for compiler-flag tricks."
user-invocable: false
version: "1.1.0"
category: agent-reference
effort: low
---
# Rust Build Performance

The big wins live in the **development loop** and the **dependency graph**,
not in obscure compiler flags. Optimize those two first; treat flags and LTO
tuning as a last resort for measured hotspots, not a starting point.

## 1. Core Principle

Practical inner loop, in order:

1. `cargo check` (or `cargo check -p <crate>`) while writing code — fast,
   catches type/borrow errors, never blocks on codegen.
2. Targeted tests for the crate you're touching — `cargo test -p <crate>`.
3. A full `cargo build` (or `cargo build --workspace`) only occasionally —
   before a PR, before a release, when you need a runnable binary.

Reach for `cargo check` by default; reach for a full build only when you
actually need generated code (running the binary, running tests that need it,
or the project-required shipping gate).

## 2. `cargo check` While Coding

```bash
cargo check                 # whole workspace, no codegen
cargo check -p trusty-search # narrow to the crate you're actually editing
```

`cargo check` runs the same type-checking and borrow-checking as `cargo
build` but skips code generation, so it's dramatically faster for the
edit-check-edit cycle. In a large workspace, always
narrow with `-p <crate>` unless you specifically need cross-crate diagnostics
— checking the whole workspace on every keystroke-adjacent save wastes the
exact time `cargo check` exists to save.

**The project's risk/stage test ladder defines the shipping gate.** Run its
required crate, consumer and release checks; prose-only changes need its doc
gates. Do not add a workspace build merely because work is ready to land.

**Before a concurrent build**, name the actual assigned `CARGO_TARGET_DIR`,
job limit, profile/features and lock-wait bound in the brief. Use the existing
allocator if available; otherwise coordinate ownership with the PM. Do not
invent a slot command or assume a fixed slot count. On a lock wait, identify
the holder and report the bound; never kill another session's build or start
a duplicate. Preserve the assigned cache across compatible runs. See #8021.

## 3. Measure Before Tuning

Never guess at what's slow. Profile first:

```bash
cargo build --timings
# → target/cargo-timings/cargo-timing.html
```

Open the HTML report. It shows, per crate: wall-clock compile time, how much
of that is the crate's own codegen vs. waiting on dependencies, build-script
(`build.rs`) time, and where parallelism is blocked (a long serial chain in
the dependency graph caps how much your core count actually helps). Use this
to find the actual bottleneck — a single slow proc-macro crate or a build
script re-running unnecessarily — before touching anything else. Tuning
without a `--timings` report first is optimizing blind.

Reference: <https://doc.rust-lang.org/cargo/reference/timings.html>

## 4. Reduce Dependency / Feature Load

Dependencies and proc-macros dominate clean-build time far more than your own
code usually does. Audit the graph:

```bash
cargo tree --duplicates       # multiple versions of the same crate = wasted compiles
cargo tree --edges features   # which features are pulled in, and by what
```

- Disable default features you don't need
  (`serde = { version = "1", default-features = false, features = ["derive"] }`).
- Remove heavyweight dependencies that provide marginal value — every crate in
  the graph is compile time, not just runtime weight.
- Consolidate duplicate versions of the same crate (`cargo tree --duplicates`)
  — cargo compiles each distinct version separately.
- Don't reflexively enable every feature/workspace member during ordinary
  dev; use `-p <crate>` and feature flags to compile only what you're
  actually exercising.
- Cargo's workspace feature resolver (resolver = "2", already the default for
  this workspace) unifies features per-target rather than globally — verify
  your `Cargo.toml` isn't accidentally forcing broader unification.

**Workspace-specific note:** `[workspace.dependencies]` sharing is already
this repo's convention (see project `CLAUDE.md`) — never pin a dependency
locally if it's already in the workspace table; a locally-pinned duplicate
defeats both dependency-graph hygiene and cargo's version unification.

**Dev-dependency edges are compile-graph edges, not favors.** The entire
point of separate crates is a more efficient compilation process; a
test-only edge that welds two crates' compile graphs together defeats it.

- Never add a workspace crate as a `[dev-dependencies]` or
  `[build-dependencies]` entry when it is absent from the consumer's normal
  dependency tree.
- A test that needs two crates lives in its own `publish = false` test crate
  that depends on both, or in the crate that already depends on the other.
- Re-declaring a normal dependency under `[dev-dependencies]` with extra
  features compiles that crate a second time, under a different feature set.
  Do this only when the feature must never leak into production, and say why
  in a manifest comment.
- Check it without a build: `cargo tree -p <crate> -e dev --prefix none` must
  add no workspace crate that `-e normal` lacks.

See #8341.

Reference: <https://doc.rust-lang.org/cargo/reference/features.html>,
<https://doc.rust-lang.org/cargo/commands/cargo-tree.html>

## 5. Preserve Incremental Compilation

The `dev` profile is incremental by default — cargo caches per-crate
compilation artifacts in `target/` and reuses them across builds. Protect
that cache:

- Don't delete `target/` mid-task. A deleted `target/` forces a full cold
  rebuild of the entire dependency graph.
- Don't churn `RUSTFLAGS` between builds — a changed flag set invalidates the
  incremental cache for everything built under it.
- Don't switch feature sets back and forth on the same crate in the same
  session — each switch is effectively a different build configuration.
- Don't switch toolchains mid-task (e.g. stable ↔ nightly, or MSRV bump)
  without expecting a full rebuild.

**Optional faster local dev profile** — for a snappier inner loop, some
engineers add a lighter debug profile locally:

```toml
[profile.dev]
opt-level = 0
debug = "line-tables-only"
incremental = true
```

Treat this as a **local, uncommitted** override
(`~/.cargo/config.toml` or an untracked `Cargo.toml` edit) — do **not** commit
profile changes to the workspace root `Cargo.toml` without explicit team
approval; it affects every contributor's build and debugging experience.

**Worktree-lifecycle note:** this project's parallel-worktree discipline
means every fresh `git worktree add` is a cold build — there is no shared
`target/` to inherit. Don't delete a *live* worktree's `target/` mid-task
expecting a quick rebuild. Reclaiming a merged worktree (and the `target/`
inside it) is the PM's to run, never an agent's (#5791): `tm session
prune-worktrees --merged-prs --force` sweeps the trees whose PR has landed
(#2919), sparing any that still holds unsaved work or a live owner. Nothing
runs it automatically, so a machine that has not had it run still carries every
merged worktree's `target/`.

Reference: <https://doc.rust-lang.org/cargo/reference/profiles.html#incremental>

## 6. sccache

[`sccache`](https://github.com/mozilla/sccache) is a shared compilation
cache — it caches compiler *outputs* keyed by input hash, so identical
compilation units are never recompiled even across different `target/`
directories.

```bash
cargo install sccache
```

Wire it in via environment variable or `.cargo/config.toml`:

```bash
export RUSTC_WRAPPER=sccache
```

```toml
# .cargo/config.toml
[build]
rustc-wrapper = "sccache"
```

**Incremental-compilation caveat — read before expecting a workspace-crate
win:** sccache caches whole-unit compiler *outputs*; it cannot cache
*incremental* compilation artifacts. This project's dev profile is
incremental by default, and every workspace-member/path crate (e.g.
`trusty-common`, `trusty-search`) builds incrementally under it — so by
default sccache gets **no hit** across worktrees for those crates, because
each worktree's incremental build produces incremental (not cacheable)
output. To get cross-worktree hits on path crates, the sccache-wrapped build
must run with `CARGO_INCREMENTAL=0` (trading away the incremental win to gain
the cross-worktree cache win instead — a real tradeoff, not a free lunch).

**Where sccache earns its keep unconditionally:** external, non-path
dependencies — `tokio`, the `aws-smithy-*` family, and the rest of the
`[workspace.dependencies]` graph. These build non-incrementally by default
and are byte-identical across every worktree building the same
`Cargo.lock`-pinned version, so two worktrees compiling the same dependency
graph hit the sccache cache hard on that (large) portion of the build without
any `CARGO_INCREMENTAL` tradeoff.

**Where it helps least:** inside a single tree doing normal incremental
edit-check-edit cycles on a path crate — cargo's own incremental cache
already covers that case, so sccache adds overhead (hashing, cache lookup)
without a corresponding win.

Adopting `sccache` machine-wide (e.g. in shell rc files, CI images) is an
ops decision with tradeoffs (disk usage, cache invalidation, shared-cache
correctness) — recommend it to the team/operator, don't silently enable it
as a side effect of an unrelated change.

## 7. Restructure Hotspots

When `--timings` (§3) points at a genuine structural bottleneck rather than
something fixable by §4/§5/§6:

- **Split large crates** that create a serial bottleneck in the dependency
  graph — a huge crate blocks everything downstream of it until it finishes;
  this project's own 500-SLOC file cap (see `CLAUDE.md`) is a forcing
  function toward exactly this kind of split.
- **Separate hot-edit code from stable, expensive-to-compile code.** Code you
  edit constantly should live in a small crate that recompiles fast; code
  that rarely changes but is expensive to compile belongs in its own crate so
  editing the hot path doesn't force recompiling the stable part.
- **Limit proc-macros and generated code.** Proc-macro crates are
  disproportionately expensive to compile and often can't be parallelized
  with the code that uses them. Prefer hand-written code or a
  build-script-generated file over a proc-macro when compile time matters
  more than call-site ergonomics.
- **Watch generic instantiation.** Heavily generic code gets monomorphized
  per concrete type at every use site — excessive generics can multiply
  codegen work. Prefer trait objects or fewer type parameters when the
  compile-time cost outweighs the runtime win.
- **Keep build scripts deterministic and cheap.** A `build.rs` that
  re-runs unnecessarily (missing `cargo:rerun-if-changed` directives, or
  doing expensive work every invocation) defeats incremental compilation for
  the whole crate it's attached to.
- **No LTO during iteration.** Link-time optimization is a `--release`-only,
  ship-time concern — never enable it for the dev/inner-loop profile. Reserve
  full optimization and LTO for production release builds, measured
  separately from the dev-loop concerns above.

Reference: <https://doc.rust-lang.org/cargo/reference/profiles.html>,
<https://doc.rust-lang.org/cargo/reference/build-scripts.html>