How Ninja Achieves Fast Incremental Builds for Chromium
This article explores the architectural choices that enable the Ninja build system to execute rapid incremental compilations for the massive Chromium web browser project. By offloading complex build configuration to meta-build systems, utilizing simple manifest files, optimizing C++ header dependency tracking into a single binary log, and minimizing file system queries, Ninja eliminates the pre-compilation overhead that slows down traditional build tools.
Offloading Logic to Meta-Build Systems
Traditional build systems like Make must evaluate variables, conditionals, and string manipulations every time a developer initiates a build. In a project the size of Chromium, which contains tens of thousands of source files, evaluating this logic takes a significant amount of time before a single file is even compiled.
Ninja solves this by acting like an assembler rather than a
high-level programming language. It strips away all conditional logic,
loops, and platform checks. Instead, Chromium uses a meta-build system
called GN (Generate Ninja) to handle the complex configuration logic
once. GN generates flat, static .ninja manifest files.
Because these files require zero evaluation, Ninja can lex and parse
them into memory in milliseconds.
Centralizing Header Dependencies
In C++ projects, modifying a single header file requires recompiling
every source file that includes it. Compilers track this by outputting
.d (dependency) files during compilation, which list all
the headers used by a specific source file.
On an incremental build, a standard build system must open and read
tens of thousands of individual .d text files scattered
across the hard drive to build its dependency graph. This causes a
massive disk I/O bottleneck.
Ninja avoids this bottleneck by processing a .d file
immediately after a file is compiled and storing that information in a
single, centralized binary file named .ninja_deps. The
original .d file is then ignored. When a Chromium developer
runs an incremental build, Ninja reads this single, compact binary file
sequentially into memory. Reading one large binary file is orders of
magnitude faster than opening and parsing thousands of small text
files.
Minimizing File System Calls
To determine which files have changed since the last build, a build
system must query the operating system for file modification timestamps
using the stat() system call. When building Chromium,
checking the timestamps of over 100,000 input, header, and output files
is one of the most time-consuming operations, especially on operating
systems with slower file systems like Windows.
Ninja structures its internal dependency graph so that it never calls
stat() on the same file more than once per build. It
resolves paths to canonical strings in memory and maps them directly to
graph nodes, avoiding expensive operating system calls to resolve
relative file paths.
The Build Log
Alongside the dependency database, Ninja maintains a file called
.ninja_log. This file records the exact command line used
to generate every output file, along with the timestamps of the last
build.
During an incremental build, Ninja compares the current build
manifest against the .ninja_log. This allows Ninja to
instantly detect not only if a source file has changed, but also if a
compiler flag or build rule was modified, without needing to invoke
external shell scripts or helper processes to verify the build
state.
Low-Overhead Execution
Once Ninja parses its manifests, loads the .ninja_deps
and .ninja_log files, and checks the file timestamps, it
immediately begins executing the necessary compilation commands. Unlike
older build systems that require manual flags to enable multithreading,
Ninja automatically detects the number of available CPU cores on the
host machine and runs commands in parallel by default.
By reducing the decision-making phase of the build to less than a second, Ninja ensures that when a Chromium developer changes a single C++ file, the compiler starts working almost instantaneously.