The Build Timeline Gap Nobody Talks About (Until It Costs You Hours)
Admin User
Author
Last month, I was debugging why our CI pipeline was taking 22 minutes for a Rust service that should compile in under five. I ran cargo build --timings, saw that compilation was fine. I checked the Ninja logs, saw linking was fine. I stared at the CI logs for an hour like an idiot, watching the wall-clock time tick away while every tool insisted it wasn't the problem. Then I realized: nobody was measuring the space between the tools.
That gap—where toolchain downloads happen, where dependencies resolve, where environments initialize—doesn't belong to any single tool, so no single tool profiles it. It's the invisible tax of modern builds, and I've burned enough Friday afternoons chasing ghosts in these gaps that I immediately understood why someone built buildline.
The Problem Is Simpler Than the Solution
Here's what happens in a real build: Cargo starts, but before it even touches compilation, it's waiting for Rust toolchain setup. Then Ninja starts. Then linking waits for something else entirely. Each tool has perfect visibility into its own work and zero visibility into everything else. You end up with a Frankenstein build profile where nothing adds up.
The original article's author nails this—the actual bottleneck in multi-tool builds isn't usually inside one tool, it's the join between them. And nobody sees the joins because the tools weren't designed knowing each other existed.
How buildline Solves It Without Becoming a Fragile Mess
I respect the architecture decision here. The obvious approach would be to wrap the entire build process with some orchestrator that walks the process tree and intercepts everything. That's a nightmare—platform-specific, breaks with containers, turns your profiling tool into a ptrace project that's fragile on arrival.
Instead, buildline wraps each tool individually. You run:
BUILDLINE_SESSION=./build.trace buildline -- ninja
BUILDLINE_SESSION=./build.trace buildline -- cargo build
Each wrapper stamps the wall-clock time right before exec'ing the real command, then reads the tool's own profiling output after it finishes. The wrapper is the only thing that knows about absolute time. Genius move—it keeps complexity out of adapters and makes them deterministic and testable.
The implementation detail I appreciate most: each adapter outputs spans relative to that tool's own start time. This means golden-file testing actually works. A .ninja_log fixture always produces identical JSON output. No timing-dependent flakiness, no "it passes locally but fails in CI" nonsense.
Where the Design Gets Opinionated (And Right)
The span schema is thoughtfully constrained. Category is a closed enum, not a free-form string. This matters because if one adapter emits "compile" and another emits "Compile", your unified timeline silently becomes incoherent. The enum enforces a contract across adapters.
I also like that Status includes Incomplete—for builds that hung or got killed. That's not a duration problem, that's a "the last thing that happened before everything went sideways" problem. Most profiling tools miss this entirely.
The escape hatch Other(String) for tool-specific states shows the author understands that you can't force everything into a taxonomy. Cargo's run-custom-build isn't a compile step, so it stays Other rather than getting folded into something it isn't.
What I'd Want to Know
The cargo adapter reads undocumented HTML-embedded JSON from --timings reports. That's acknowledged as fragile—a future cargo release could break it silently. This is the kind of technical debt that feels acceptable for an early project, but it's worth knowing you're betting on cargo's internal format stability.
Also, this is single-machine only for now. Distributed builds and remote execution are explicitly out of scope until there's a sane way to sync clocks. I respect calling that boundary upfront instead of having it surprise users later.
For teams with complex build systems—Bazel, Gradle, MSBuild—you'd need to contribute adapters. The contribution model is clean (fixture pair, not subjective code review), but it depends on community contributions from people running systems the author doesn't have access to.
The Real Value
What I'm taking from this: the next time I'm chasing build time, I'm looking for the gaps between tools, not the time within them. And if buildline catches on, I won't need to guess anymore—I'll see it.
The other lesson is architectural—keeping concerns separated (adapters produce relative time, the wrapper handles absolute time) makes testing and maintenance tractable. That's a pattern I'm thinking about for other profiling work.
Have you hit this problem in your own builds? Do you know where your CI time actually goes, or are you guessing like I was?
Source: This post was inspired by "buildline: merging cargo and ninja's build profiling into one timeline" by Dev.to. Read the original article