Skip to content
shippy

Tune a project

The cache can't help the first build after a real edit: every crate the edit reaches has to compile. What helps there is settings: a faster linker, less debug info, a parallel front end. shippy-bench tune measures them on real edits in your project and writes the winner as a shippy.toml for you to apply, so no project file changes.

What it found on tempo

Rebuild after an edit, geometric mean of three edits:

cargoshippy
Stable: wild linker, no debug info
10.05s
6.77s1.5× faster
Nightly: + -Zthreads=8
10.05s
5.4s1.9× faster
Nightly + Cranelift for workspace crates
10.05s
5.33s1.9× faster

The linker alone is worth 1.4x, the stable set 1.5x, nightly 1.9x.

shippy.toml

shippy.toml at the workspace root holds settings shippy adds to every cargo command. This is tempo's, from bench/examples/tempo.shippy.toml:

[cargo]
toolchain = "nightly"
config = [
    'target.x86_64-unknown-linux-gnu.linker="clang"',
    'target.x86_64-unknown-linux-gnu.rustflags=["-Clink-arg=--ld-path=wild", "-Zthreads=8"]',
    'profile.dev.debug=false',
]
 
[cache]
build_scripts = true   # rocksdb, jemalloc and the other C builds are restored too
fixed_paths = true     # every checkout shares, wherever it lives (needs bubblewrap)
 
[env]
SOURCE_DATE_EPOCH = "1794500000"

Each config entry becomes a --config argument to cargo. It needs wild and clang on PATH. For stable Rust, drop toolchain and -Zthreads=8.

Find your own settings

Build the benchmark

cargo build --release -p shippy-bench

Run it on one edit

From your checkout, name the file an edit goes to and the cargo command you run after it:

shippy-bench tune --file crates/foo/src/lib.rs --prune -- build -p foo --bin foo

It times plain cargo, then every setting stable Rust has (linker, debug info, split debug info, codegen units), and prints the winner as a shippy.toml. That is the whole of it for most projects. The next two steps are for several edits, nightly settings or a baseline of your own.

Describe a few edits

A bench.toml names your checkout and the edits to time. Each scenario edits one file and always puts it back, even on Ctrl-C.

repo = "/path/to/checkout"
runs = 5
 
[[scenario]]
name = "edit-leaf"
cargo = ["build", "-p", "foo", "--bin", "foo"]
file = "crates/foo/src/lib.rs"
edit = "append_fn"   # adds a function, so dependents rebuild
 
[[scenario]]
name = "edit-body"
cargo = ["build", "-p", "foo", "--bin", "foo"]
file = "crates/bar/src/lib.rs"
edit = "replace"
find = "let limit = 10;"
replace = "let limit = 10 + ({n} - {n});"   # {n} is new every time
 
[[variant]]
name = "cargo"   # the baseline

Run the tuner

shippy-bench tune --config bench.toml --knobs bench/knobs.default.toml --prune

Without --knobs it tries what stable Rust has; knobs.default.toml adds the nightly settings. It starts from plain cargo, then tries each knob (linker, debug info, Cranelift, parallel front end, codegen units...) and keeps a value when it lowers the score by more than --min-gain (3% by default). --prune deletes the target directory of every candidate that lost.

Apply the winner

The tuner prints the winning settings and writes them to shippy.toml in its output directory. Copy that file to the workspace root, or add its [cargo] table to the shippy.toml there. shippy never applies them by itself: they change what is built (no debug info means no source lines in a debugger), and the first build with them compiles everything once.

Loading diagram...

Other settings that pay

  • Keep the store and target directories in memory, or on a filesystem with reflinks. On ext4 a restored library is a link to the store's file, and a crate's metadata, executables and build scripts' output directories are copied out of the store: about half of what a build of tempo restores.
  • Set SOURCE_DATE_EPOCH if build scripts stamp the build time. On tempo it is the difference between 38 s and 6 s for every new checkout.
  • Leave linked outputs cached (the default) so undoing an edit restores the executable too.

Go deeper