<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: nabsei</title>
    <description>The latest articles on DEV Community by nabsei (@nabsei).</description>
    <link>https://dev.to/nabsei</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4040922%2F57bdc805-6706-4a3f-9ec6-b771a3b3ea40.png</url>
      <title>DEV Community: nabsei</title>
      <link>https://dev.to/nabsei</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nabsei"/>
    <language>en</language>
    <item>
      <title>buildline: merging cargo and ninja's build profiling into one timeline</title>
      <dc:creator>nabsei</dc:creator>
      <pubDate>Wed, 22 Jul 2026 01:43:04 +0000</pubDate>
      <link>https://dev.to/nabsei/buildline-merging-cargo-and-ninjas-build-profiling-into-one-timeline-2373</link>
      <guid>https://dev.to/nabsei/buildline-merging-cargo-and-ninjas-build-profiling-into-one-timeline-2373</guid>
      <description>&lt;p&gt;Every build tool profiles its own silo. &lt;code&gt;cargo build --timings&lt;/code&gt; will tell you cargo is fine. Ninja's build log will tell you linking is fine. And yet a CI build takes 22 minutes and nobody can point at where the time actually went, because none of these tools know the others exist.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;buildline&lt;/strong&gt; to fix that: it merges the profiling output your build tools already produce into one wall-clock timeline, emitted as standard &lt;strong&gt;Chrome Trace Event Format&lt;/strong&gt; so it opens directly in Perfetto - no UI to build or maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap nobody profiles
&lt;/h2&gt;

&lt;p&gt;Here's the part I didn't expect going in. A tiny &lt;code&gt;ninja&lt;/code&gt; build finishes in under a second. A real &lt;code&gt;cargo build&lt;/code&gt; starts about 2.7 seconds later. Neither tool's own profiler records that gap — ninja doesn't know cargo exists, and cargo's own timer starts from zero the moment it launches. On a merged timeline, it's just visibly empty space, exactly where the surprise usually is in a real CI log: toolchain downloads, dependency resolution, environment setup, all invisible to every tool's own instrumentation because it happens around them, not inside them.&lt;/p&gt;

&lt;p&gt;That's the actual thesis of the project: the interesting bottleneck in a multi-tool build is often the join between tools, not inside any one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it avoids becoming a fragile process-tree hack
&lt;/h2&gt;

&lt;p&gt;The obvious design for "profile a build made of several tools" is an orchestrator that wraps the whole build and somehow intercepts every child process it spawns. I didn't want that — process interception is&lt;br&gt;
platform-specific, fragile against anything that isn't a direct child&lt;br&gt;
(containers, remote shells, build systems that re-exec themselves), and it turns a normalization tool into a ptrace project.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildline&lt;/code&gt; wraps each tool invocation instead, correlated through a shared session file:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BUILDLINE_SESSION=./build.trace buildline -- ninja&lt;br&gt;
BUILDLINE_SESSION=./build.trace buildline -- cargo build&lt;/code&gt;&lt;br&gt;
&lt;code&gt;# open build.trace in &amp;lt;https://ui.perfetto.dev&amp;gt;&lt;/code&gt;&lt;br&gt;
​&lt;br&gt;
Each invocation is transparent — inherited stdout/stderr, the tool's real exit code passed through unchanged. &lt;code&gt;buildline&lt;/code&gt; stamps the wall-clock instant right before it execs the wrapped command, then reads that tool's own profiling artifact after it exits (&lt;code&gt;.ninja_log&lt;/code&gt;, cargo's &lt;code&gt;--timings&lt;/code&gt; report), and appends the result to the shared trace file. There's no orchestration, no process-tree walking, and no finalize step — &lt;code&gt;build.trace&lt;/code&gt; is a valid, openable Chrome Trace file after every single invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The span schema, and the one design decision that matters most
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pub struct Span {&lt;br&gt;
    pub name: String,       // "serde v1.0", "obj/parser.o"&lt;br&gt;
    pub category: Category, // Compile | Link | Configure | Resolve | Download | Test | Other(String)&lt;br&gt;
    pub status: Status,     // Success | Failed | Skipped | Incomplete&lt;br&gt;
    pub track: String,      // "cargo", "ninja" — groups rows in Perfetto&lt;br&gt;
    pub lane: u32,          // sub-row for parallel work within a track&lt;br&gt;
    pub start_us: i64,      // relative to that tool's own start — no wall-clock here&lt;br&gt;
    pub dur_us: i64,&lt;br&gt;
    pub args: BTreeMap&amp;lt;String, String&amp;gt;,&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
​&lt;br&gt;
Every adapter is a pure function: native tool output in, a &lt;code&gt;Vec&amp;lt;Span&amp;gt;&lt;/code&gt; out, timestamps relative to that tool's own start. The wrapper is the only thing that knows about wall-clock time — it's a thin layer that offsets each adapter's relative spans onto the shared axis. Keeping wall-clock entirely out of the adapters is what makes golden-file testing possible at all: an adapter's output is 100% deterministic, so a fixture (a real &lt;code&gt;.ninja_log&lt;/code&gt; or &lt;code&gt;--timings&lt;/code&gt;report) always produces the exact same normalized JSON, with nothing timing-dependent to make the test flaky.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Category&lt;/code&gt; is a closed enum, not a free-form string, on purpose. &lt;br&gt;
A golden test only diffs an adapter against itself — nothing stops one adapter emitting &lt;code&gt;"compile"&lt;/code&gt; and another emitting &lt;code&gt;"Compile"&lt;/code&gt;, each passing its own test while the "unified" timeline is silently incoherent underneath. The enum is the actual contract that makes it one timeline instead of N timelines that happen to render next to each other. &lt;code&gt;Other(String)&lt;/code&gt; is the escape hatch for genuinely tool-specific states — cargo's &lt;code&gt;run-custom-build&lt;/code&gt; (running an already-compiled build script, as opposed to compiling one) isn't a compile step, so it stays &lt;code&gt;Other&lt;/code&gt; rather than being folded into &lt;code&gt;Compile&lt;/code&gt; and losing that distinction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Status&lt;/code&gt; includes &lt;code&gt;Incomplete&lt;/code&gt; for a reason I only appreciated after hitting it myself elsewhere: a span that started and never finished — the build that hung or got killed. That's not a "duration" problem, it's a "the last thing that happened before everything went sideways" problem, and no other Status variant models it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contributing a new adapter is a fixture pair, not a code review
&lt;/h2&gt;

&lt;p&gt;Supporting a new build system (Bazel, Gradle, MSBuild, whatever isn't&lt;br&gt;
covered) is one adapter file plus a fixture pair: a real trace from that tool, and the exact normalized JSON it should produce. CI diffs the two — no subjective judgment call needed to review it. The best fixtures come from people running build systems I don't have access to myself (a real Bazel profile from a remote-execution setup, a real MSBuild trace on Windows) — that coverage isn't something I can produce alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's at
&lt;/h2&gt;

&lt;p&gt;Early: &lt;strong&gt;ninja&lt;/strong&gt; and &lt;strong&gt;cargo&lt;/strong&gt; adapters today, both covered by golden-file tests.&lt;br&gt;
Single-machine only for now — distributed/remote-execution builds are out of scope until there's a sane way to align clocks I don't control, and I'd rather say that upfront than have it be a surprise.&lt;br&gt;
The cargo adapter has one real fragility worth naming: stable cargo has no &lt;code&gt;--timings=json output&lt;/code&gt;, so it reads the &lt;code&gt;UNIT_DATA&lt;/code&gt; array embedded in the &lt;code&gt;--timings&lt;/code&gt; HTML report — cargo's own dashboard data, not a documented or versioned format. It could change without notice in a future cargo release. If that golden test ever starts failing after a &lt;code&gt;rustup update&lt;/code&gt;, that's why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cargo install buildline&lt;/code&gt;​&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/nabsei/buildline" rel="noopener noreferrer"&gt;https://github.com/nabsei/buildline&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Crate: &lt;a href="https://crates.io/crates/buildline" rel="noopener noreferrer"&gt;https://crates.io/crates/buildline&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; built with the assistance of Claude Code. I made the design decisions described above, reviewed the implementation, and tested the result myself.&lt;/p&gt;

&lt;p&gt;Feedback very welcome — especially "this already exists, here's why it's&lt;br&gt;
better," since I'd rather know that now.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>buildtools</category>
      <category>cli</category>
      <category>cargo</category>
    </item>
  </channel>
</rss>
