Skip to content

Repository files navigation

buildline

CI crates.io docs.rs license

One timeline for your whole build, no matter how many build systems it's made of.

Your CI build takes 22 minutes. cargo build --timings says cargo is fine. Ninja's log says linking is fine. Every tool profiles its own silo, and yet the build is slow. Where did the time actually go?

buildline merges the profiling output your build tools already produce into a single, unified timeline: cargo, ninja, and more, side by side on one time axis, opened in Perfetto. It's not another profiler. It's the layer that makes the profilers you already have talk to each other. Think OpenTelemetry, but for builds.

It sees the time no profiler sees

Because buildline wraps each build step and stamps the real wall-clock, it captures the dead time between and around your tools: toolchain downloads, dependency resolution, environment setup, the minutes that never show up in any single tool's trace because they happen outside it. That's usually where the surprise is.

The screenshot below is a real run: a tiny ninja build finishes in under a second, then a real cargo build starts about 2.7 seconds later. Neither tool's own profiler records that gap. Ninja doesn't know cargo exists, and cargo's timer starts from zero when it launches. On the merged timeline it's just... visibly empty space, exactly where the surprise usually is in a real CI log.

buildline timeline in Perfetto, showing a real ninja build followed by a real cargo build with the gap between them visible

Installation

With Rust installed:

cargo install buildline

Without Rust: pre-built binaries for Linux, macOS (Intel and Apple Silicon) and Windows are attached to each release.

Try it with no build on hand

buildline demo
# open demo.trace.json in https://ui.perfetto.dev

Writes an illustrative trace (synthetic data, clearly labelled as such) so you can see the merge mechanism, two tracks, the wall-clock gap between them, before pointing buildline at a real build.

Usage

BUILDLINE_SESSION=./build.trace buildline -- ninja
BUILDLINE_SESSION=./build.trace buildline -- cargo build
# open build.trace in https://ui.perfetto.dev, flamegraph, zoom, drill-down included

Each invocation wraps one real tool invocation. Run it once per tool, from wherever you'd normally run that tool. No orchestrator wrapping, no process interception: buildline runs the command transparently (inherited stdout/stderr, exact exit code passed through), then reads that tool's own profiling artifact, timestamps it onto the shared session clock, and appends it to the trace file. There's no finalize step: build.trace is a valid, openable trace after every single invocation.

Supported tools today: ninja (.ninja_log, written automatically by every ninja build), cargo (--timings, injected automatically if you don't already pass it, parsed from the report's embedded data, since stable cargo has no machine-readable --timings=json output), and webpack (--profile --json=<path>, both injected automatically since webpack's --json prints to stdout by default and nothing lands on disk otherwise).

A note on that cargo parsing, honestly: it reads UNIT_DATA, a JavaScript array embedded in cargo's --timings HTML report. That's cargo's own dashboard data, not a documented or versioned format, verified empirically against cargo 1.96.1, not against any spec, because there isn't one. It could change in a future cargo release without notice. If the cargo golden test starts failing after a rustup update, that's the likely cause. Please open an issue with your cargo --version and, ideally, the new HTML report.

A note on webpack, also honestly: unlike cargo or ninja, webpack's --profile output is duration-only, there's no per-module start offset anywhere in the stats JSON. The adapter stacks modules sequentially by webpack's own module-graph visit order as an approximation, which means the merged timeline will show webpack's work as more serial than it actually was (webpack builds modules in parallel). Flagged rather than guessed at silently; see the doc comment in src/adapters/webpack.rs for the detail.

What it is not

  • Not a profiler that competes with cargo --timings or ninja's log. It consumes them.
  • Not a visualizer. It emits standard Chrome Trace Event Format, Perfetto does the UI.
  • Not for distributed, multi-machine builds yet. Single machine first, because you can't align clocks you don't control.

How it works

Every adapter is a pure function: native tool output in, a list of normalized Spans out, relative to that tool's own start, no wall-clock involved. The wrapper is a thin layer on top: it stamps the real wall-clock instant each tool launched, and a Session (persisted alongside the trace file) offsets each batch of spans onto one shared axis.

pub struct Span {
    pub name: String,       // "serde v1.0", "obj/parser.o": no tool prefix, track already carries it
    pub category: Category, // Compile | Link | Configure | Resolve | Download | Test | Other(String)
    pub status: Status,     // Success | Failed | Skipped | Incomplete
    pub track: String,      // "cargo", "ninja": groups rows in Perfetto
    pub lane: u32,          // sub-row for parallel work within a track
    pub start_us: i64,
    pub dur_us: i64,
    pub args: BTreeMap<String, String>,
}

Category is a closed vocabulary, not a free-form string, on purpose: a golden test only diffs an adapter against itself, so nothing would stop one adapter emitting "compile" and another "Compile", each passing its own test while the "unified" timeline is silently incoherent. The enum is the contract that makes it actually one timeline. Other(String) is the escape hatch for genuinely tool-specific states, e.g. cargo's run-custom-build (running an already-compiled build script) isn't a compile step, so it stays Other rather than being folded into Compile.

See CATEGORIES.md for what each term means, tool-agnostic: the bar a new adapter's steps are held to.

Contributing

Support for a new build system is one file plus one fixture pair: a real trace from that tool, and the exact normalized output it should produce. CI diffs the two. No human judgment call required to review it.

src/adapters/<tool>.rs
tests/fixtures/<tool>/<name>.<native-format>
tests/fixtures/<tool>/<name>.expected.json
tests/golden_<tool>.rs

Bring the build system you use that this doesn't support yet. Your real build trace is coverage nobody else can produce.

Status

Early. Ninja, cargo, and webpack adapters are golden-tested; the serializer that turns spans into Chrome Trace events is golden-tested too. Single-machine only. The cargo adapter reads an undocumented internal format (see above); the webpack adapter approximates timing order rather than measuring it (also see above). Both work today, neither is guaranteed to keep working exactly as is.

License

Licensed under either of

at your option.

About

One timeline for your whole build, no matter how many build systems it's made of.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages