Skip to content

FIGMA 2024-04-27 Tier 3

Read original ↗

Figma — Speeding Up C++ Build Times

Summary

Figma's Core team retrospective on cutting C++ cold-build times ~50% after a year in which the codebase grew 10% but build times grew 50%. Hardware (M1 Maxs), Ccache, and remote caching were insufficient. The structural observation: C++ build times are roughly proportional to the number of bytes the compiler sees after pre-processing, and that number was growing disproportionately to source code — a signature of include bloat. Figma built two custom tools on top of libclang + its own transitive-byte accounting, plus a codebase-wide Fwd.h-per-directory pattern, and wired them into CI. 31% drop in compiled bytes and 25% drop in cold build time from an initial one-shot include cleanup of the largest files; full tooling now prevents 50-100 would-be slowdowns/day.

Key takeaways

  1. Post-pre-processing bytes is the right cost proxy for C++ build time, not LOC. In C++ compilation, #include directives transitively flatten all referenced header content into a single mega-file that the compiler then parses. If C includes B and B includes A, C's compile unit contains all of A's bytes. Figma's diagnostic signal was the ratio of bytes sent to compiler / lines of code added growing unboundedly. See concepts/c-plus-plus-compilation-model. (Source: article pre-processing explainer.)
  2. Removing unnecessary includes from the largest files alone delivered 31% compiled-byte reduction and 25% cold-build-time reduction. Proof that the bytes-proportional-to-build-time model was correct and that unnecessary includes were a dominant contributor. (Source: article "initial cleanup" results.)
  3. DIWYDU (Don't Include What You Don't Use) — a Figma tool, deliberately looser than Google's IWYU. IWYU demands the exact minimal include set (provably correct but hard to retrofit); DIWYDU demands only that every included header be directly used. Built on libclang's Python bindings; walks the AST of each source + header file, records which symbols come from which includes, flags includes whose symbols are never directly referenced. Runs on feature branches. Explicit scope: skips STL headers (private includes defeat both DIWYDU and IWYU) and sometimes can't resolve UNEXPOSED_EXPR AST nodes because libclang's Python bindings ride the C binding, not the full C++ Clang API — a known precision limitation. (Source: article DIWYDU section.)
  4. Include bloat has a second failure mode DIWYDU doesn't catch: a used header that pulls in a huge transitive tree. The include is genuinely used, so DIWYDU can't flag it; but adding it may send megabytes of extra bytes to the compiler. Fix = concepts/forward-declaration or splitting the header. Detection = measure the delta in transitive bytes. (Source: article motivation for includes.py.)
  5. includes.py — a pure-Python (no Clang) static byte counter over the include graph, fast enough (seconds) to run in CI on every PR. Crawls all first-party header/source files, treats standard-library includes as 0 bytes (safe for Figma: stdlib usage is gated to one wrapper directory), builds the include DAG, sums per-file + transitive bytes. The tool feeds CI policy: warn on PRs whose byte-delta per source file is significant, block merge until addressed. Canonical instance of a patterns/ci-regression-budget-gate — cost is measured, not assumed, and the gate stops regressions at authoring time. (Source: article includes.py section.)
  6. Fwd.h per directory — patterns/centralized-forward-declarations. When the fix to an includes.py regression is to forward-declare a symbol instead of including its header, the naive approach (copy the struct Foo; declaration into every file that needs it) hurts searchability and readability. Figma's directory-granular solution: one AnimalFwd.h per directory listing every forward declaration that other files in the directory need, included from every header in the directory. Critically: source files never include Fwd.h (forward declarations only help in headers where they prevent transitive includes). Result: individual engineers don't have to think about forward-declaration discipline — it's a directory-level default. (Source: article "Fwd.h" section + AnimalFwd.h code sample.)
  7. Bazel remote caching trimmed ~2+ min off local builds when the cache hits. Adopted with "make-sense" gating — only use remote cache for local builds when likely to hit. This is a classic concepts/content-addressed-caching add-on, complementary to but not a replacement for the bytes-reduction work. The order matters: reduce first, then cache. (Source: article "other improvements" section.)
  8. Reported end-to-end outcome. Build times cut ~50%. CI rejects / warns on 50-100 regressions per day that would otherwise have silently slowed builds. (Source: article conclusion.)

Numbers

  • Codebase growth vs build-time growth (2023): 10% code / 50% build time.
  • Initial one-shot cleanup (largest files, unnecessary includes): −31% compiled bytes, −25% cold build time.
  • End state after full tooling rollout: −50% build time.
  • CI gate throughput: 50-100 would-be regressions prevented / day.
  • Bazel remote cache: >2 min off local builds when cache hits.
  • includes.py runtime: "usually in just a couple of seconds" — fast enough to run in CI on every PR.

Architectural ideas

  • Measure the right thing: bytes, not LOC. The article is an object lesson in identifying the actual cost driver. LOC is cheap to measure and looks roughly correlated; post-pre-processing bytes is the cost. The bytes/LOC ratio growing unboundedly was the tell.
  • Two-tool split by detection class. DIWYDU catches the "included but not directly used" class (solvable by removing the include). includes.py catches the "used but enormous" class (solvable by forward-declaration or header splitting). Trying to make one tool do both would merge two different policies — DIWYDU would stay a static-analysis problem and includes.py would stay a budget-measurement problem. Clean separation.
  • Laxer-than-IWYU was a feature, not a shortcut. Figma called IWYU's precision-mode failure "challenging to apply retroactively to a substantial codebase" — the strictness forced engineers to debate every single #include line, blocking rollout. DIWYDU's "directly-used is sufficient" rule is migratable incrementally.
  • CI gate + measurement is the leverage point. DIWYDU + includes.py aren't invoked manually by engineers — they're in the CI signal loop. "50-100 regressions/day prevented" is only possible because the cost regression is surfaced before merge, not during a slow-build retrospective.
  • Fwd.h pushes policy to the directory, not the file. Individual authors neither read nor write forward declarations by hand. Centralization per directory is the discipline mechanism — the same shape as DIWYDU's "the tool enforces it, not the engineer."

Caveats / self-reported limits

  • DIWYDU doesn't analyze STL. Standard-library private includes defeat AST-based header-to-symbol mapping. IWYU has the same problem. For Figma, mitigated because stdlib usage is confined to one wrapper directory.
  • libclang Python bindings ride C bindings. UNEXPOSED_EXPR AST nodes lose type information; DIWYDU falls back to "less elegant solutions." Article flags migrating DIWYDU to C++ (full Clang C++ API) as a possible next step.
  • includes.py over-counts standard library as 0 bytes. Safe only because Figma wraps stdlib in one directory; would be unsound for projects that use STL directly everywhere. Known assumption.
  • Forward declaration has readability cost. Too many Fwd.h entries hurts codebase searchability (a symbol appears in declaration-only form in many places). The directory-local Fwd.h bounds this to one place per directory.

Why ingest (Tier-3 gate)

Figma is not in AGENTS.md's formal tier lists — treating as Tier-3-equivalent. The post passes the Tier-3 gate on two dimensions:

  • Distributed-systems / infrastructure-architecture substance: custom AST-based static-analysis tool (DIWYDU), custom CI-gated transitive-byte cost model (includes.py), codebase-wide convention (Fwd.h per directory). All three are portable design ideas, not feature announcements.
  • Production outcomes with numbers: 50% build-time cut, 31%/25% one-shot wins, 50-100 regressions/day caught. Real metrics.

Adjacent to the existing build-systems concepts (concepts/build-graph, concepts/content-addressed-caching, concepts/hermetic-build, concepts/remote-build-execution — all from the Canva CI ingest) but operates one layer below: not "how do we distribute the build and cache it" but "how do we shrink what the compiler has to do at all." These compose.

Cross-references

Raw

Last updated · 200 distilled / 1,178 read