bi-01 — Navigating Chromium Independently

Phase 0 · Instrumentation · Spec areas §2 (architecture map), §25 (source archaeology), §44 (reading ladder). Prerequisite for every other B-module.

Cross-track hook: none. This module is deliberately self-contained and requires no local build.


Why a Principal Engineer needs this

Chromium is roughly 30 million lines across ~500,000 files. Nobody has read it. The engineers who are effective in it are not the ones who have memorised more of it — they are the ones whose search converges. That is a learnable, mechanical skill, and it is the difference between "I could probably find that in an afternoon" and "I don't know where to start."

Three reasons this is the first module rather than a footnote:

1. Memorised paths rot; navigation does not. In the last few years Blink renamed every ng_* layout file, moved the issue tracker, and rewrote the parser's fast path. Anyone whose knowledge was a list of paths is now wrong. Anyone whose knowledge was "spec term → grep → xref" is unaffected. The tracker's Verification Log exists to keep score on exactly this.

2. The expensive failure mode is searching in the wrong subsystem, confidently. Most wasted Chromium time is not slow reading. It is two hours in //content for something that lives in //third_party/blink/renderer/core, because the engineer never asked which process the behaviour belongs to. §3 below is the antidote.

3. Your value is arbitration, not recall. "Is this a Blink bug, a compositor bug, or our bug?" is the question you will actually be asked. Answering it requires locating evidence quickly in a codebase you do not own.

The rule for this entire track: you are never given a path. You are given a technique, and you record the query that worked. The queries transfer. The paths don't.


Mental Model

The tool ladder

Five tools, in strictly increasing cost. Never reach for rung N+1 before rung N is exhausted — the most common time sink in Chromium work is debugging something that a 30-second search would have answered.

RungToolCostAnswers
1Code Search (source.chromium.org)secondsWhere is it? Who calls it? When did it change?
2Local grep on a checkoutsecondsSame, plus generated files, plus git log -S
3Tracing (DevTools → Perfetto)minutesDoes this code even run? On which thread? How often?
4Local build + logging~minutes/iterationWhat are the actual values?
5Debugger (lldb)slow, high valueExact call stack, exact state, exact ordering

Note that tracing is rung 3, above search but below the debugger. This is deliberate and it is the ordering most people get wrong. Tracing answers whether and where far faster than a breakpoint does, and it works on a stock Chrome with no build at all. When you have a behaviour and no hypothesis, trace first — a breakpoint requires you to already know where to put it.

The four questions that start every navigation

Before searching, answer these. Guessing them costs you minutes; skipping them costs you hours.

  1. Which process? Browser, renderer, GPU, network service, or utility.
  2. Which thread? Main, compositor, raster, IO, worker.
  3. Is it web-exposed? If yes, there is an .idl file and a spec, and both are entry points.
  4. Is it spec-defined? If yes, spec-phrase search will land you within one or two hits. If no, you need the naming grammar instead (§ Technique 3).

Question 1 is answerable from the directory path alone, which is the single highest-value fact in this module.

The path is the architecture

Chromium's directory layout is not organisational convenience — it is enforced. DEPS files declare which directories may include which, and presubmit rejects violations. So the path tells you the layer, and the layer tells you the process.

PathProcessNotes
//content/browser/Browserprivileged; trusts nothing from renderers
//content/renderer/Rendererthe content-layer side of the renderer
//content/common/bothIPC/shared definitions only
//content/public/boththe embedder-facing API surface
//third_party/blink/renderer/Rendererthe engine implementation
//third_party/blink/public/boundarywhat //content is allowed to see of Blink
//cc/Renderer (+ viz)compositor; runs on main and impl threads
//gpu/, //components/viz/GPUcommand buffer, display compositor
//services/network/Network serviceits own process
//net/network servicethe stack itself
//v8/rendererseparate project, separate repo, separate bug tracker
//base/allthreading, callbacks, containers
//mojo/allthe IPC system itself

The public/ convention is the one to internalise. //content/public and //third_party/blink/public exist so that layers below cannot reach into layers above. When you find yourself wondering "how does the browser process tell Blink to do X," the answer is nearly always: it doesn't directly — it goes through a Mojo interface declared in a .mojom file, and the Blink side lives behind public/. Searching //content for Blink internals is the single most common wasted hour.

Blink's own split, from its README.md:

  • platform/ — no DOM. Graphics primitives, fonts, geometry, WTF containers, threading. Depends on nothing above it.
  • core/ — the Web Platform features that everything else needs: DOM, HTML, CSS, style, layout, paint, events, editing.
  • modules/ — self-contained web-exposed features that core does not need in order to render a page: WebAudio, IndexedDB, WebRTC, Bluetooth, and so on.
  • bindings/ — the V8 boundary. Mostly generated.
  • controller/ — the code that drives the whole thing.

The dependency direction is platform → core → modules, one way, enforced. So:

If a feature can be removed without breaking basic page rendering, look in modules/ first. If removing it would break rendering a plain HTML page, it is in core/.

This one heuristic resolves maybe a third of "which directory" questions immediately. Blink also ships SpecMapping.md at renderer/ root, which maps specifications to directories — read it once; it is the officially maintained version of the guess you would otherwise be making.


Under the Hood: eight techniques

Technique 1 — Spec-phrase search (highest yield)

Blink's spec-implementing code quotes the specification in comments, often step by step. This makes the spec a search index over the implementation.

Take the most distinctive multi-word term of art from the algorithm — not a common word — and search it as a quoted phrase.

"appropriate place for inserting a node"
"reconstruct the active formatting elements"
"in-flow"    ← too common, will not converge

Convergence check: if the top hits are in the subsystem you predicted, your architectural model is correct. If they are somewhere else entirely, stop and fix the model before reading any code — you have just been handed a free correction.

Works extremely well for: HTML parsing, DOM, events, CSS cascade, fetch, URL. Works poorly for: compositor, GPU, scheduling — these are implementation inventions with no spec text (see Technique 3).

Technique 2 — Symbol, then cross-reference

symbol:ShouldFosterParent          declaration, not the 200 mentions
class:HTMLConstructionSite
function:FindFosterSite

Then use the cross-references panel. It gives you callers and callees mechanically.

Gate questions 3 ("who calls it") and 4 ("what does it call") are never to be answered by guessing or by reading nearby code. They are xref lookups. If you find yourself inferring callers from context, you are doing archaeology on a tool that would have told you the answer.

The xref panel also reveals call-site count, which is diagnostic on its own. A function with one caller is an extracted helper — read it with its caller. A function with 200 callers is an invariant, and changing it is a cross-component change (§24 rung 5).

Technique 3 — The naming grammar

For non-spec code, names are the index. Chromium's conventions are consistent enough to search by shape:

Suffix / prefixMeans
FooImplthe concrete implementation of interface Foo, often a Mojo interface
FooClientcallback interface into the layer above (inverted dependency)
FooObservermulti-subscriber notification
FooDelegatesingle-subscriber policy hook, usually embedder-provided
FooBaseshared base for several implementations
FooTraitscompile-time policy/customisation
FooBuilderstaged construction of an immutable object
ScopedFooRAII — does something in the ctor, undoes it in the dtor
FooHandle, FooTokenopaque identity, safe to pass across processes
blink::Foo vs FooBlink type vs content/browser type — often both exist

That last row matters more than it looks. There are frequently two types with the same name on either side of the Blink/content boundary (e.g. a WebFoo in blink/public and a Foo in blink/renderer). Landing on the wrong one and concluding the code "does nothing" is a classic beginner hour.

Also search the inverted direction: Client and Delegate suffixes tell you where a dependency was deliberately inverted to keep the layering legal. Those are the seams of the architecture.

Technique 4 — Generated code: the #1 reason grep fails

If you grep for something that obviously must exist and find nothing, it is generated.

Chromium generates enormous amounts of C++ at build time from declarative inputs:

InputGenerates
*.idl (Web IDL)V8 bindings — the JS-visible surface of every Web API
*.mojomIPC interfaces, both ends, for every language
css_properties.json5property IDs, parsing, computed-style storage, longhand expansion
css_value_keywords.json5every CSS keyword identifier
html_tag_names.json5, html_attribute_names.json5tag/attribute atoms
runtime_enabled_features.json5RuntimeEnabledFeatures::FooEnabled() for every flag
computed_style_extra_fields.json5 + friendsComputedStyle field storage

Consequences, in order of how often they bite:

  1. A CSS property's parsing/storage is declared, not written. Looking for where contain is handled? Start at css_properties.json5, not at a .cc file. The entry there tells you the property's inheritance, initial value, whether it is animatable, and which custom parsing function (if any) it uses.
  2. A Web API's entry point is its .idl. document.querySelector is declared in an .idl; the generated binding calls a Blink method whose name is derived by rule (querySelectorQuerySelector). Searching for "querySelector" in .cc files finds usage; searching .idl finds the definition.
  3. RuntimeEnabledFeatures::XEnabled() means the behaviour is conditional. When observed behaviour contradicts the source, check for a feature flag before concluding you misread. This is also how you find code that is written but not yet shipping.
  4. On a local checkout, generated files live under out/<dir>/gen/. They are real, readable C++ and reading them once — especially a generated binding — is worth an hour.

Technique 5 — History as documentation

The commit message on the CL that introduced a line is usually better documentation than any comment, because Chromium requires CLs to explain why, link a bug, and survive review.

  • In Code Search: use blame on the line, then open the CL.
  • Locally: git log -S'<exact string>' --oneline -- <path> finds the commit that introduced or removed that string. This is the single most useful git invocation in this track.
  • git log --follow survives renames — necessary in a tree that renamed every ng_* file.

The question "why does this weird special case exist" is nearly always answered by a bug link in a CL description. §46's notebook is largely fed by this technique.

Technique 6 — Tests as executable specification

Tests answer "what is this supposed to do" more reliably than the implementation, which only tells you what it currently does.

KindLocation shapeRuns
Unit testfoo_test.cc next to foo.cc (Blink), foo_unittest.cc (rest of Chromium)in-process, fast
Browser test*_browsertest.ccreal multi-process browser
Web testthird_party/blink/web_tests/content_shell, HTML+expectations
WPTthird_party/blink/web_tests/external/wpt/cross-browser contract

The Blink-vs-rest naming split (_test.cc vs _unittest.cc) is a real gotcha: searching only one form silently halves your results. Search both.

Reading order that works: find the test before you finish reading the implementation. The test names enumerate the edge cases the implementation exists to handle, which turns an opaque function into a checklist.

Technique 7 — DCHECK is the invariant, written down

DCHECK(x) is an assertion compiled out of release builds. Chromium uses it heavily, and it is the most honest documentation in the codebase: it states what the author believed must always be true.

When answering gate question 2 ("what invariant does it maintain?"), read the DCHECKs first. They are frequently a more precise answer than any prose you would write.

Related: CHECK is retained in release (a security or memory-safety invariant — treat it as load-bearing), and NOTREACHED() marks a state the author believed impossible. NOTREACHED() in a switch over an enum is a strong hint that the enum is exhaustive by contract.

Technique 8 — OWNERS and DEPS are the architecture diagram

  • OWNERS tells you who is accountable for a directory — and by proxy, where a subsystem boundary is. Two adjacent directories with disjoint OWNERS are two different teams and usually two different design philosophies.
  • DEPS declares legal include directions. Reading a DEPS file tells you the intended layering faster than reading any code, and an include_rules entry with a comment explaining an exception is a documented architectural compromise.

These are also the files you must consult before proposing a change (§24), so the reading you do now is not throwaway.


Deep dive: the generated-code surface, catalogued

Technique 4 said "if you grep for something that must exist and find nothing, it is generated." Here is the actual inventory, so you can recognise which declarative file to open. Blink alone contains 57 .json5 files (measured 2026-08-10).

The ones you will actually use

FileGeneratesOpen it when you want to know
core/css/css_properties.json5property IDs, parsing, computed-style storage, longhand expansionanything about a CSS property
core/css/css_value_keywords.json5every CSS keyword identifierwhy a keyword is/isn't recognised
core/css/computed_style_field_aliases.json5ComputedStyle field storagehow a property is stored
platform/runtime_enabled_features.json5RuntimeEnabledFeatures::XEnabled()whether a behaviour is flag-gated
core/html/html_tag_names.json5-style name tablestag/attribute AtomicString atomswhy tag comparison is a pointer compare
core/events/event_type_names.json5event type atomsthe canonical list of event names
core/events/event_target_names.json5event target nameswhat can be an event target
core/svg/svg_tag_names.json5, mathml_tag_names.json5foreign-content element tablesparser foreign-content behaviour
platform/fonts/font_family_names.json5generic family atomsfont fallback plumbing
core/probe/core_probes.json5DevTools instrumentation probeshow DevTools observes Blink

That last one is worth a detour on its own: DevTools instrumentation is generated from a declarative probe list. When you wonder "how does DevTools know a style recalc happened," the answer is a probe declared in core_probes.json5 and injected into Blink at generated call sites.

Anatomy of a css_properties.json5 entry

This is the single highest-value declarative file in Blink. A real entry:

{
  name: "contain",
  property_methods: ["ParseSingleValue", "CSSValueFromComputedStyleInternal"],
  field_group: "*",
  field_size: 5,
  field_template: "primitive",
  default_value: "kContainsNone",
  name_for_methods: "Contain",
  type_name: "unsigned",
  converter: "ConvertFlags<Containment>",
  keywords: ["none", "strict", "content", "size", "layout", "style", "paint", "inline-size"],
  typedom_types: ["Keyword"],
  invalidate: ["layout"],
  is_animation_affecting: true,
}

Read it field by field and notice how much you just learned without opening a .cc file: the legal keywords, that it is stored in 5 bits, its initial value, that it participates in animation, that custom parsing exists — and, crucially, that changing it invalidates layout.

invalidate: is a declarative, machine-readable statement of which pipeline stage a property change dirties. It turns "does this property cause layout?" from folklore into a grep.

bi-07 develops this properly. For navigation purposes the point is: a declarative file can answer a question you were about to answer by reading algorithms. Always check whether the subsystem has one.

Anatomy of a runtime_enabled_features.json5 entry

{ name: "ViewTransitionAsyncFinished", status: "stable" },
{ name: "ViewTransitionLongCallbackTimeoutForTesting", status: "test" },

The status vocabulary is exactly three values — valid_values: ["stable", "experimental", "test"] — and status may be per-platform, keyed by ["Android", "Win", "ChromeOS", "Mac", "Linux", "iOS", "Fuchsia"]. Omitting "default" in a per-platform dictionary means the feature is off on unlisted platforms.

Entries can also declare implied_by and depends_on, forming a small dependency graph between features.

What to do with this:

  • status: "stable" — shipping. Behaviour you read in the code is behaviour you get.
  • status: "experimental" — behind --enable-experimental-web-platform-features.
  • status: "test" — test-only; never assume it reflects product behaviour.
  • Per-platform status is why "it works in Chrome" is ambiguous. Desktop and Android can genuinely differ, declaratively, in this file.

When observed behaviour contradicts the source, check here before concluding you misread. This is also how you find code that is written but not yet shipping — a good source of context when evaluating whether to depend on a platform feature.


Deep dive: reading the build graph as architecture

BUILD.gn files are not just build plumbing; they encode component boundaries.

gn refs out/Default path/to/file.cc      # which targets contain this file
gn desc out/Default //some:target deps   # direct dependencies
gn path out/Default //a:b //c:d          # WHY does a depend on d
gn ls  out/Default | grep -i blink       # real target names, never guessed

gn path is the interesting one. It answers "why is this even linked in," which is an architecture question disguised as a build question. If gn path shows a surprising edge, you have found either a layering violation or an abstraction you did not know existed.

Also read the visibility and public_deps declarations in a BUILD.gn. A target with narrow visibility is telling you "this is internal to a component" more precisely than any comment.


Deep dive: git archaeology recipes

With a local checkout these are your highest-leverage commands. They work even when the tree does not compile.

# Which commit introduced or removed this exact string?
git log -S'CausesFosterParenting' --oneline -- third_party/blink/renderer/core/html/parser/

# Same, but regex, and show the patch
git log -G'FosterParent\(' -p --  <path>

# Who last touched each line, and in which CL?
git blame -L 100,140 -- <file>

# Follow a file across renames (essential in a tree that renamed every ng_* file)
git log --follow --oneline -- <file>

# What changed in a directory in the last 90 days?
git log --since=90.days --oneline -- third_party/blink/renderer/core/css/ | head -40

# Find the commit that changed a build requirement
git log -1 -S'mac_sdk_official_version = "26.5"' -- build/config/mac/mac_sdk.gni

That last recipe is not hypothetical — it is exactly how this track dated Chromium's Xcode 26.5 requirement to 2026-05-13, which turned "the build is broken" into "the toolchain floor moved on this date." A build failure with a date attached is a decision; without one it is a mystery.

Reading a CL description properly

Chromium requires CLs to explain why, link a bug, and survive review. So a commit message is usually better documentation than any comment. When you find the CL that introduced a puzzling special case, look for:

  • Bug: footer — the issue often contains the reproduction and the user impact.
  • Design-doc links — larger changes reference one.
  • Fixed: vs Bug: — whether this closed the issue or merely touched it.
  • The reviewers — OWNERS of the affected directories, i.e. the people who understand it.

Deep dive: reading a subsystem cold, in order

A repeatable procedure for a subsystem you have never opened. Roughly 45 minutes.

  1. README.md in the directory, if it exists. Blink's better subsystems have unusually good ones (core/css/, core/layout/, core/paint/, platform/scheduler/, platform/heap/).
  2. DEPS — what may this depend on? That is the layering, stated.
  3. OWNERS — who is accountable; adjacent directories with disjoint owners are different teams.
  4. BUILD.gn — what is the component, what is public, what is internal.
  5. The .json5 / .idl / .mojom inputs, if any — the declarative surface.
  6. The test files*_test.cc names enumerate the edge cases the code exists to handle. Read these before the implementation; they turn an opaque function into a checklist.
  7. Only now the implementation, entered from a specific question, never from the top.

Step 6 is the one people skip and the one that pays most. A test file is an enumeration of everything the author was afraid of.


Deep dive: how far down does the question go?

A practical stopping rule, because Chromium reading does not terminate on its own.

Before descending another layer, ask: would knowing this change what I do? Keep a written deferred list of names you chose not to follow. At the end, that list is your map of the subsystem — and reviewing it usually shows that two or three names recur, which are the ones actually worth learning.

The failure mode this prevents: depth-first reading that ends four hours later in //base/containers, having learned nothing about the original question. That is not diligence, it is a lack of a stopping rule.


Anti-Patterns

Reading top-down from main(). Chromium has no meaningful single entry point for feature work, and startup code is unrepresentative of everything else. Start from a behaviour, not from the root.

Searching for a concept instead of a token. "layout" returns tens of thousands of hits. symbol:UpdateLayout returns something you can read. Convert the concept to an identifier or a spec phrase before searching.

Concluding "this code is dead" too early. Between generated code, feature flags, platform #ifdefs, and Mojo indirection, apparent deadness is usually one of those four. Check runtime_enabled_features.json5 and search the .mojom before believing it.

Reading the implementation before the spec, for spec-defined behaviour. You will mistake a compatibility workaround for the design. Case B in the parsing lab is exactly this trap.

Answering "who calls this" by reading. Use the xref panel. Always.

Trusting an LLM's Chromium paths — including mine. Chromium moves faster than any model's training data. Every path in this repository has a date on it in the Verification Log. Re-check anything older than about six months; that is a habit, not a one-off.


Trade-offs

Code Search vs local grep. Code Search has cross-references, blame, and no setup; local grep has generated files, git log -S, and works offline at full speed. Use Code Search for structure and history, local grep for exhaustiveness. git grep -n on a checkout is dramatically faster than most people expect — it is the right tool for "find all 400 call sites," which Code Search paginates badly.

Depth-first vs breadth-first reading. Depth-first (follow every callee) does not terminate in Chromium. The discipline is: read one layer, write down the names of the things you deferred, and only descend into the one that actually matters for your question. The deferred list is itself a deliverable — it is your map of the subsystem.

Precision vs recall in search. Start precise (symbol:, quoted phrase). If you get zero hits, that is information — usually "generated" or "wrong subsystem" — not a reason to immediately broaden to a soup of common words.


Lab — the navigation drills, Navigation drills

See bi-01-navigating-chromium/docs/execution.md. Eight timed drills. The deliverable is the query log, not the answers.


Debugging Exercise

Without a local build, using only Code Search and a stock Chrome:

  1. Determine whether element.scrollIntoView({behavior: 'smooth'}) animation is driven by the main thread or the compositor. State your evidence. (Hint: rung 3 of the ladder beats rung 1 here — and noticing that is the actual lesson.)
  2. Find one behaviour in Chromium currently behind a runtime-enabled feature flag that is not yet enabled by default. Name the flag and the file that declares it.
  3. Pick any CSS property added to the web platform in the last three years. Find its declaration in css_properties.json5, and from that entry alone state: is it inherited? is it animatable? does it have custom parsing?

Testing & QA Considerations

Locating tests is a navigation skill, not a testing skill, and it is graded here:

  • For a given .cc file, find its unit test. Note the naming convention that applies.
  • For a given web-exposed behaviour, find both the Blink web test and the WPT.
  • Determine whether a WPT is currently expected to fail in Chromium, and where that expectation is recorded. (Blink records known failures declaratively rather than by deleting tests — finding that mechanism is the exercise.)

The last one matters disproportionately: a WPT that Chromium fails on purpose is a documented interop gap, and those are the richest source of tractable first contributions (§24 rung 3).


Further Reading (primary sources first)

Authoritative, in-tree — read these in the tree, not in a blog:

  • third_party/blink/renderer/README.md — the core/modules/platform split, threading, type conventions. The single best orientation document in Blink.
  • third_party/blink/renderer/SpecMapping.md — specification → directory.
  • docs/ at the repo root — build, contribution, debugging, testing.
  • third_party/blink/renderer/core/css/README.md, style-calculation.md, style-invalidation.md — unusually good subsystem docs; you will use these in bi-07.
  • Any DEPS and OWNERS file in a directory you are studying.

External

  • Chromium Code Search: source.chromium.org/chromium/chromium/src
  • Google Code Search operator reference (for symbol:, class:, function:, case:, content:, comment:, usage:, pcre:yes, - negation, AND/OR).
  • Life of a Pixel (Chromium team talk, refreshed roughly annually) — the standard orientation to the rendering pipeline. Watch it once now, again after bi-09.
  • chromium.googlesource.com/chromium/src/+/main/docs/README.md — index of in-tree docs.

Deliberately not on this list: blog posts explaining Blink internals. The half-life is under two years and the failure mode — a confidently stated stale path — is exactly what this module exists to inoculate against.


Principal Engineer Review

Answer in writing. Several have more than one defensible answer; "it depends" is acceptable only if you say on what.

  1. A bug report says "the page renders correctly but screenshots taken by our automation are blank." Name the three processes that could be responsible and the single cheapest piece of evidence that discriminates between them.

  2. You grep the entire tree for a CSS property name and get only test files. Give four distinct explanations, ranked by likelihood, and the check that eliminates each.

  3. An engineer on your team says "I read the Blink source and it clearly does X, but the browser does Y." Enumerate the ways both statements can be simultaneously true. Which do you check first, and why that one?

  4. You need to know whether a given DOM operation runs on the compositor thread. Compare answering this by (a) reading source, (b) tracing, (c) a debugger breakpoint. Rank by time-to-answer and by confidence in the answer, and explain why the rankings differ.

  5. Why does //third_party/blink/public exist at all, given that everything in it is compiled into the same binary as //third_party/blink/renderer? What class of bug does deleting the boundary reintroduce, and what would you expect to break first?

  6. Argue that Chromium's heavy use of generated code from .json5/.idl/.mojom is good architecture. Then argue it is a navigability tax paid by every newcomer. What would you actually change, if anything, and who bears the cost of that change?

  7. You are given 30 minutes to determine whether a rendering bug is in Blink or in the GPU process, on a machine with no Chromium checkout. Write the procedure.

  8. DCHECK is compiled out of release builds. Make the case that this is the right decision, then the case that it hides real bugs from real users. What does Chromium's choice of CHECK vs DCHECK for a given invariant tell you about how the author classified the failure?

  9. A teammate proposes documenting your team's "map of Chromium" as a wiki page of paths. You think this is a trap. Make the argument, and propose what to write instead that still transfers knowledge to a new hire.

  10. You find a function with 200 call sites that you believe has an off-by-one error. Before writing any code, what do you need to establish, and in what order? What is the strongest evidence that you have misread it — and how do you go looking for that evidence rather than for confirmation?