buildline 0.1.0

One timeline for your whole build, no matter how many build systems it's made of.
Documentation
  • Coverage
  • 59.26%
    32 out of 54 items documented0 out of 22 items with examples
  • Size
  • Source code size: 473.28 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 853.92 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 9s Average build duration of successful builds.
  • all releases: 9s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • nabsei/buildline
    3 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • nabsei

buildline

CI

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

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) and 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).

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.

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.

Contributing

Support for a new build system is one file + 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 and cargo 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) — it works today, it isn't guaranteed to keep working.