G01 — Chromium: Build, Debug, Trace, Test
Operational reference. Verified against upstream 2026-08-10 — re-verify anything here
older than ~6 months and update the Verification Log in PROGRESS.md.
Spec areas §3 (build), §20 (debugging), §21 (tracing), §22 (tests).
Rule for this file: it records procedures and the reasoning behind them, not a frozen command list. When a command here fails, the fix is to consult
docs/in your own checkout — which is the authoritative, versioned copy — and then update this file. In-tree docs beat any external source including this one.
1. Your specific machine
Apple M2 Pro · 12 cores · 32 GB RAM · ~146 GB free at start.
This is a comfortable build machine on CPU and RAM, and a tight one on disk. Budget:
| Item | Size | Where |
|---|---|---|
src + .git (full history) | ~80–100 GB | ~/chromium/src |
| git cache mirror | ~24 GB | ~/Library/Caches/depot_tools/git_cache ← measured; a surprise |
One out/ dir, component + symbol_level=0 | ~15–25 GB | ~/chromium/src/out/ |
One out/ dir with usable symbols | ~40–80 GB | ~/chromium/src/out/ |
The git-cache trap (measured on this machine, 2026-08-10)
fetch --git-cache is the fast-checkout path upstream recommends, and it works — but it
keeps a full bare mirror outside the checkout, in addition to the working checkout's
own .git. Peak storage is therefore roughly double what the checkout size suggests.
Measured after fetch completed here:
~/chromium/src 26 GB
~/Library/Caches/depot_tools/git_cache 24 GB ← the mirror
free space 146 GB → 90 GB
The mirror location is set by cache_dir in ~/chromium/.gclient — read that file rather
than guessing; it is not ~/.cache, and conflating the two will send you deleting the
wrong thing.
What to do about it:
- The mirror is a cache, not a dependency. Once
srcis synced it can be deleted; the cost is that a futurefetchof a second checkout loses its speedup, and the nextgclient syncre-fetches more over the network. With one checkout on a disk-constrained machine, deleting it is usually right. - If you keep it, budget for it from the start rather than discovering it at 90 % full mid-build.
- Check with
du -sh "$(grep cache_dir ~/chromium/.gclient | cut -d'"' -f2)".
Remaining budget here: 90 GB free, ~20 GB needed for a component build with modest symbols. Comfortable, but not comfortable enough for two symbol-rich output directories.
2. Build configuration
cd ~/chromium/src
gn gen out/Default
gn args out/Default # opens an editor
Recommended args.gn for this track
is_debug = false # release-mode codegen: much faster builds, usable perf
is_component_build = true # many small dylibs → fast incremental links. Essential.
symbol_level = 1 # function names + line numbers, no full type info
blink_symbol_level = 2 # full symbols for Blink only; cheap, since it's a subset
dcheck_always_on = true # ← the one most people omit, and the one you want most
# --- toolchain workaround for this machine; see §2.1. Remove once Xcode is 26+ ---
use_clang_modules = false
use_unified_system_module = false
Verified working on this checkout (gn gen → 32,319 targets, 2026-08-10).
Why each, because copying build flags without understanding them is how you end up unable to debug at the moment you need to:
is_component_build = trueis the single largest iteration-time win. A static build relinks a ~1 GB binary on every change; a component build relinks one small shared library. Non-negotiable for a learning checkout.symbol_level:0is fastest but stack traces become useless addresses.1gives you function names and line numbers — enough for almost all of this track.2gives full type info for variable inspection, and costs a lot of disk.blink_symbol_level = 2is the compromise: full fidelity exactly where you'll be setting breakpoints.dcheck_always_on = truekeepsDCHECKs in a release build. This is the highest-value flag for learning: DCHECKs are Blink's invariants written as executable assertions (bi-01 Technique 7), and having them fire when you break something turns a confusing misrender into a precise message naming the invariant you violated. Enable it and leave it on.is_debug = trueis deliberately not recommended as your default. It is far slower to build and to run, and its main benefit (assertions) you already have fromdcheck_always_on. Reach for it only when you need libc++ debug iterators or full unoptimised stepping.
2.0 WARNING - BLOCKER: trunk cannot compile on this machine (2026-08-10)
Status: checkout works, compilation does not. Read this before spending time on 2.1.
This machine: macOS 15.0 (Sequoia) - Xcode 16.2 - SDK 15.2
Trunk requires: macOS 26.2+ (Tahoe) - Xcode 26.5+ - SDK 26.5
git log dates the requirement exactly:
2026-05-13 mac: Switch to Xcode 26.5 17F42 (2026-05-11) and SDK 26.5 25F70
Trunk has required Xcode 26.5 for over a year. The failure is not configuration - it is a missing SDK symbol:
// base/process/launch_mac.cc
if (__builtin_available(macOS 26, *)) {
DPSXCHECK(posix_spawn_file_actions_addchdir(&file_actions_, path)); // SDK 26 only
} else {
DPSXCHECK(posix_spawn_file_actions_addchdir_np(&file_actions_, path)); // SDK 15 has this
}
__builtin_available is a runtime check, but the symbol must still exist at compile
time. SDK 15.2's spawn.h declares only the _np variant. This pattern recurs across the
tree, so it is not patchable in any sane way - it is a genuine toolchain-floor problem.
The gn workarounds in 2.1 get gn gen to succeed. They do not and cannot fix this.
What this does not block
The checkout is not wasted. Everything except compilation works right now:
git grep -nover 30M lines - faster than Code Search, and it sees generated inputsgit log -S'...'archaeology (section 25) - the most valuable local capability- reading
docs/,DEPS,OWNERS,.json5,.idl,.mojomin-tree - everything in bi-01, bi-02, bi-03, bi-04, bi-07 and Labs 01-02
Phases 0-2 require no build at all. A local build is first genuinely needed at Phase 3-4 (section 20, native debugging) and again at Phase 6 (sections 22/23 tests, 24 contribution). On the phase plan that is roughly two months of runway.
Your options, ranked
| Option | Cost | Consequence | |
|---|---|---|---|
| A | Upgrade macOS to 26.2+, then Xcode to 26.5+ | multi-hour OS upgrade + ~15 GB Xcode; reboot; some risk to existing toolchains (homebrew, anaconda, rust) | Recommended. Trunk builds; local source matches Code Search; contribution path stays open. |
| B | Check out a revision from before 2026-05-13 and gclient sync | another long sync; ~15 months of source drift | Builds today, but local source no longer matches Code Search or these modules - actively confusing while learning, and it forecloses section 24 contribution. |
| C | Defer the build; work from the checkout + stock Chrome | none | Zero risk, no loss before Phase 3. |
Recommendation: C now, A before Phase 3. There is no reason to take an OS upgrade during Phase 0, and no reason to arrive at Phase 3 without one. Option B is a genuine fallback only if you decide against upgrading at all - the staleness cost is real and it ends the contribution track, which is the stated target of section 24.
Do not delete out/Default or the checkout in the meantime; both are reusable the moment the
toolchain is current.
2.1 Case study: trunk required a toolchain this machine didn't have
This happened on the first gn gen here, and it is worth reading in full because the
diagnostic path is the transferable part — the specific flags will be obsolete within a
year.
Symptom. gn gen failed with three ERROR Input to targets not generated by a dependency errors naming files that do not exist:
//out/Default/sdk/xcode_links/MacOSX15.2.sdk/usr/include/DarwinFoundation1.modulemap
DarwinFoundation2.modulemap
DarwinFoundation3.modulemap
Diagnosis, step by step.
- The missing files are in the SDK, not in Chromium. So this is a toolchain-version
problem, not a checkout problem.
ls "$(xcrun --show-sdk-path)/usr/include" | grep modulemapshowedDarwinFoundation.modulemapbut no numbered variants — they exist only in newer SDKs. xcodebuild -version→ Xcode 16.2, SDK 15.2.grep mac_sdk_official_version build/config/mac/mac_sdk.gni→26.5. Chromium trunk had moved to the macOS 26 SDK.- Who wants those files?
grep -rn DarwinFoundation1 build/ buildtools/→buildtools/third_party/libc++/modules.gni, insideif (use_clang_modules). - Why is that path taken at all?
build/config/c++/modules.gni:use_autogenerated_modules = !(is_apple && use_system_xcode)— on macOS with a system Xcode this is false, which selects the manual modulemap path that hardcodes the numbered files. The autogenerated path assertsxcode_version_int >= 2600anyway, so both branches require Xcode 26. - Is there an escape?
grep -n "use_clang_modules =" build/config/c++/c++.gnishowed it inside adeclare_args()block → it is a settable gn arg, not a computed constant. That single observation is the whole fix.
Fix. use_clang_modules = false reduced the failure from four toolchain variants to
one remaining target (//build/modules:system_modulemap), which
build/modules/BUILD.gn gates on a second arg, use_unified_system_module. Setting both
to false generated cleanly.
Cost of the workaround. Clang header modules are a compile-time optimisation for libc++
headers. Disabling them means somewhat slower compiles and no -fmodules-strict-decluse
include hygiene checking. Nothing about Blink's behaviour changes, so it is a sound trade
for a learning checkout. Revisit it if you upgrade Xcode.
The transferable lessons, which matter more than the flags:
- A
gn generror naming a nonexistent SDK file is a toolchain-version problem. Checkmac_sdk_official_versionagainstxcrun --show-sdk-versionbefore anything else. - Chromium trunk tracks the newest toolchain aggressively. Being one Xcode major behind is enough to break the build. Expect this again.
- Before concluding "I must upgrade," check whether the offending behaviour sits behind a
declare_args()value.grep -n "<name> =" build/config/**/*.gniand look for the enclosingdeclare_args()block. A surprising amount of Chromium's build is switchable. - When it breaks again after a
gclient sync, re-run exactly this procedure. It will; a workaround pinned to a toolchain gap has a shelf life.
If you do upgrade Xcode later, delete both workaround lines and re-run gn gen — keeping
dead workarounds is how build configs become unexplainable.
Verified target names (2026-08-10)
//content/shell:content_shell
//:blink_tests
//third_party/blink/renderer/controller:blink_unittests
Do not trust this list — including here. Regenerate it with
gn ls out/Default | grep -E ':(content_shell|blink_tests)$'.
Inspect what you actually got:
gn args out/Default --list --short # every arg and its current value
gn args out/Default --list=symbol_level # docs for one arg, from the build files
Building
autoninja -C out/Default chrome # the full browser
autoninja -C out/Default content_shell # minimal embedder — prefer this
autoninja -C out/Default blink_tests # content_shell + web test infrastructure
autoninja selects the correct underlying executor (ninja or siso) and the right
parallelism. Use it rather than invoking ninja directly; the wrapper is where upstream
encodes build-system migrations.
Prefer
content_shelloverchromefor everything in this track. It is a minimal embedder of//content— no bookmarks, no sync, no extensions, no UI. It builds far faster, starts far faster, and it is what the web tests run against. If your question is about Blink,chromeis 90 % irrelevant code.
Navigating the build graph
The build system is searchable, and almost nobody learns this. These are the gn
equivalents of bi-01's Code Search techniques:
gn ls out/Default # every target
gn ls out/Default | grep -i blink # find the real target names — do this
# instead of trusting any doc's list
gn refs out/Default third_party/blink/renderer/core/html/parser/html_tree_builder.cc
# which targets contain this file?
gn desc out/Default //third_party/blink/renderer/core:core deps
gn path out/Default //chrome //third_party/blink/renderer/core:core
# why does A depend on B?
gn refs <file> answers "what do I have to rebuild to test this change," and gn path
answers "why is this even linked in," which is a genuine architecture question.
When the build breaks after a sync
In order, cheapest first:
gclient sync -D # sync deps, delete stale ones
gn clean out/Default # clear generated files, keep the args
rm -rf out/Default && gn gen out/Default # nuclear; costs a full rebuild
Most post-sync failures are stale generated files, so gn clean resolves them. Reach for
the third option rarely — on this machine it is an hour you did not need to spend.
3. Running
out/Default/Content\ Shell.app/Contents/MacOS/Content\ Shell https://example.com
out/Default/Content\ Shell.app/Contents/MacOS/Content\ Shell --run-web-tests <test path>
out/Default/Chromium.app/Contents/MacOS/Chromium --user-data-dir=/tmp/cr-profile
Always pass --user-data-dir to a scratch directory when running a local build, so you
never touch your real profile.
Flags that matter for this track:
| Flag | Use |
|---|---|
--user-data-dir=<path> | isolate profile — always |
--renderer-startup-dialog | pause each renderer at startup so you can attach |
--disable-hang-monitor | stop Chrome killing a renderer you're stopped in |
--enable-blink-features=Foo | turn on a runtime-enabled feature by name |
--disable-blink-features=Foo | ...and off, to A/B a behaviour |
--enable-logging=stderr --v=1 | see LOG()/VLOG() output |
--single-process | one process — convenient, frequently broken, never trust it for behaviour |
--no-sandbox | last resort for debugging; changes the security model, so never conclude anything about behaviour from a --no-sandbox run |
The --enable-blink-features / --disable-blink-features pair is the fastest way to
answer "is this behaviour behind a flag" — a question bi-01 Technique 4 says you should ask
early and often.
4. Debugging with lldb
Setup
Chromium ships lldb helpers in-tree. Wire them in once:
# see docs/lldbinit.md in your checkout for the current recommended contents
echo "command script import ~/chromium/src/tools/lldb/lldbinit.py" >> ~/.lldbinit
Without this, WTF::String, std::u16string and friends print as raw pointers and you
will waste time. Check docs/lldbinit.md in your own checkout for the current form — this
is exactly the kind of instruction that drifts.
Attaching to the right process
This is the part that trips everyone. Chromium is multiprocess; a breakpoint in Blink must be set in a renderer, not the browser process you launched.
# 1. Launch with renderers paused at startup
out/Default/Content\ Shell.app/Contents/MacOS/Content\ Shell \
--renderer-startup-dialog --disable-hang-monitor <url>
It prints something like:
Renderer (80156) paused waiting for debugger to attach. Send SIGUSR1 to unpause.
# 2. Attach, set breakpoints, then release it
lldb -p 80156
(lldb) breakpoint set --name blink::HTMLConstructionSite::FosterParent
(lldb) process handle SIGUSR1 -n true -p true -s false # don't stop on the unpause signal
(lldb) continue
# in another terminal:
kill -USR1 80156
Alternatively, find the renderer by inspecting Chrome's own task manager, or by
--wait-for-debugger style flags for other process types (--utility-startup-dialog,
and the equivalent GPU flag).
--disable-hang-monitor matters: without it, sitting at a breakpoint for 30 seconds gets
your renderer killed for being unresponsive, and you lose the state you were inspecting.
Useful lldb, for this track specifically
(lldb) breakpoint set -n blink::Document::UpdateStyleAndLayout
(lldb) breakpoint set -f html_tree_builder.cc -l 812
(lldb) breakpoint set -n blink::Element::SetAttribute -c 'name == "class"' # conditional
(lldb) breakpoint command add 1
> bt 12
> continue
> DONE
(lldb) thread backtrace all # every thread — reveals the thread boundaries directly
(lldb) frame variable
(lldb) expression -- node->DebugName()
thread backtrace all is underrated here: it is the fastest way to see the renderer's
thread structure — main, compositor, raster, IO — which is otherwise an abstract claim from
bi-02. Do it once early and read the thread names.
Getting a breakpoint to hit at all
The three reasons a Blink breakpoint doesn't hit, in order of frequency:
- You attached to the browser process, not a renderer.
- The function was inlined. Set the breakpoint on the caller, or build that file with less optimisation.
- The code path is behind a runtime-enabled feature that is off.
5. Tracing
Tracing is rung 3 of the bi-01 ladder — above search, below the debugger — and it is the right first tool when you have a behaviour and no hypothesis. It also works on stock Chrome with no build at all, which is why Phase 0 can start tracing before the build finishes.
Three levels, increasing power:
- DevTools → Performance. Curated view, JS-centric, good for main-thread work.
- Perfetto UI (
ui.perfetto.dev), orchrome://tracingin older builds. All categories, all processes, all threads. This is where you see the compositor thread, the GPU process, and cross-process flow arrows. - Command-line tracing — startup tracing flags for capturing things that happen before you can click record.
What to actually do with it in this track:
- Record with the
blink,cc,gpu,viz,toplevelanddevtools.timelinecategories and identify, by name, the events for: style recalculation, layout, pre-paint, paint, commit, activation, raster, and frame presentation. Those names are the vocabulary the rest of the browser modules use. - Follow a single frame across processes using flow arrows: renderer main → compositor → GPU → presented. This makes §15's "the app does not own the whole budget" concrete rather than rhetorical.
- Correlate a trace event name back to source: trace events are declared in the code with
TRACE_EVENTmacros, so the event name is a greppable string. This is the single best bridge between rung 3 and rung 1 — see something in a trace, grep its name, land in the implementation.
That last bullet is the technique that makes tracing a navigation tool rather than only a performance tool.
TRACE_EVENT0("blink", "...")names are searchable identifiers into the exact code that ran.
6. Tests
autoninja -C out/Default blink_tests
strip ./out/Default/Content\ Shell.app/Contents/MacOS/Content\ Shell # macOS: recommended upstream
third_party/blink/tools/run_web_tests.py -t Default
third_party/blink/tools/run_web_tests.py -t Default fast/forms
third_party/blink/tools/run_web_tests.py -t Default fast/fo\*
Direct, without the harness (you diff by hand):
out/Default/content_shell --run-web-tests fast/forms/001.html
Known failures are declared, not deleted:
third_party/blink/web_tests/TestExpectations
Read TestExpectations early — it is a map of Chromium's known interop gaps, and
therefore a map of tractable first contributions (§24 rung 3). An entry there is a
documented, accepted, currently-wrong behaviour with a bug attached.
C++ tests:
gn ls out/Default | grep unittests # find the current target names; don't guess
autoninja -C out/Default <target>
out/Default/<target> --gtest_filter='HTMLTreeBuilder*'
Remember the Blink/non-Blink naming split from bi-01 Technique 6: foo_test.cc in Blink,
foo_unittest.cc elsewhere.
7. Iteration-time discipline
Ranked by effect on this specific machine:
is_component_build = true— do not build without it.- Build the smallest target that answers your question:
content_shelloverchrome; a single unit-test target overblink_tests. - Use
gn refsto learn which targets a file belongs to, and build only those. symbol_level = 1+blink_symbol_level = 2rather thansymbol_level = 2everywhere.- ccache, if you frequently switch branches. Little benefit for linear work.
- Do not run
gclient syncmore often than you need. Every sync is a partial rebuild.
The general principle: a Chromium question answered by a 10-minute rebuild was usually answerable by a 30-second search or a 2-minute trace. The ladder in bi-01 exists to protect build time, which is your scarcest resource.
8. Verification checklist
Update PROGRESS.md §8 whenever you confirm or refute one of these.
-
args.gnvalues still valid (gn args out/Default --list) -
blink_tests,content_shelltarget names still current (gn ls) -
run_web_tests.pypath still current -
TestExpectationspath still current -
docs/lldbinit.mdcontents still match what you put in~/.lldbinit -
--renderer-startup-dialogstill the documented attach mechanism -
Perfetto vs
chrome://tracing— which does upstream currently document?