5.3.2 Practical Graph Analysis
Graph analysis becomes useful when it changes a concrete engineering decision: remove an accidental edge, split an overloaded target, stabilize a widely used API, or narrow the evidence needed for a performance investigation. The method is to state the graph contract, measure one bounded question, inspect the edges behind an outlier, and then verify the predicted effect with evidence from the graph layer where that effect occurs.
Establish the Measurement Contract
Start by recording five choices with every result:
- Layer: unconfigured targets (
query), configured targets (cquery), or planned actions (aquery). - Roots and universe: the top-level target for a forward walk, or the set of potential consumers for a reverse walk.
- Edge policy: whether implicit dependencies, tool dependencies, source files, generated files, and external repositories are included.
- Configuration: flags, platform, and top-level context whenever
cqueryoraqueryis used. - Comparison point: revision, Bazel version, and the same query expression when a metric is tracked over time.
Without that contract, two numbers called “closure size” may describe different
graphs. query runs on the post-loading target graph and conservatively includes
all possible select() branches. cquery runs after analysis and resolves them
for the supplied configuration. aquery exposes the resulting actions and
artifacts, but not which actions executed or hit a cache.1 This applies the
graph-layer contract from 5.3.1 Graph Theory Foundations, builds on the
target-graph model from 2.1.1 Nodes, Edges & Acyclicity, and uses the operators
taught in 5.2.1 bazel query — Static Graph Analysis as diagnostic instruments rather than an API
catalog.
For target-graph measurements, publish Bazel's edge direction:
dependent -> dependency
//app:server -> //lib:protocol
With that convention, fan-out is the number of outgoing edges from a target: its direct dependencies. Fan-in is the number of incoming edges: its direct dependents. The terms reverse if an exported data set reverses its edges, so publish the convention with the metric.
Map the metric to one operator
The expressions below are enough to follow this investigation. See
5.2.1 bazel query — Static Graph Analysis for the full semantics, universe rules, output choices,
shell quoting, and failure modes.
| Measurement | Expression | Interpretation limit |
|---|---|---|
| Forward closure | deps(root) | includes the root. Not configured or executed work |
| Direct fan-out | deps(root, 1) except root | direct dependencies in the chosen edge projection |
| Direct fan-in | rdeps(universe, seed, 1) except seed | direct dependents only inside the chosen universe |
| Potential reverse impact | rdeps(universe, seed) | structural candidates, not guaranteed rebuilds |
| One explanation | somepath(from, to) | one witness, not necessarily shortest or unique |
| Complete path subgraph | allpaths(from, to) | all target-graph paths in scope, not runtime flow |
Choose implicit-edge filtering deliberately and keep it stable across comparisons. A source edit does not guarantee that every reverse-reachable target rebuilds or retests: configuration, action inputs, Skyframe pruning, produced bytes, and cache state determine observed work.2
Turn an Outlier into an Explanation
A high count is a triage result, not a repair. Inspect why the edges exist. Begin with one witness, then inspect the complete path subgraph before editing:
bazel query \
'somepath(//services/orders:server, //lib:protocol)' \
--noimplicit_deps \
--output=label
bazel query \
'allpaths(//services/orders:server, //lib:protocol)' \
--noimplicit_deps \
--output=label
Then inspect the suspicious target's loaded definition and provenance with
--output=build, or locate the BUILD declaration with --output=location.
Typical findings include a broad convenience library, a macro-added default,
an obsolete direct dependency, or one target that groups unrelated sources.
Build-file automation experience shows why the edge itself matters: missing
dependencies can force broader compiler inputs, while unused dependencies add
work and make downstream rules consume more than they need.3
The repair should match the evidence. Remove an unused edge, depend on a smaller public target, split a target only along real source/dependency boundaries, or make an intentional shared API explicit. After editing, rerun the same path and closure queries. A lower number is useful only if the unwanted paths disappeared and the build's semantics remain correct.
Visualize only when the path set needs a picture
For a human review, render the already-bounded allpaths() result:
bazel query \
'allpaths(//services/orders:server, //lib:protocol)' \
--noimplicit_deps \
--output=graph \
--nograph:factored | dot -Tsvg > /tmp/orders-protocol.svg
For exact automation, use a structured query format. DOT is presentation output.
The factoring, quoting, and output caveats are covered in
5.2.1 bazel query — Static Graph Analysis.4
Treat Closure Counts as Trends and Leads
A closure trend is most useful as a change detector. Store the count with its measurement contract, compare like with like, and alert on a sustained or review-sized delta rather than a universal threshold. Then diff the sets to identify the newly reachable nodes:
bazel query 'deps(//app:server)' --noimplicit_deps --output=label \
> /tmp/server-deps.txt
In durable automation, preserve structured output and compute the set diff in the consuming tool. The label file above is suitable for a quick human investigation, provided ordering is not treated as the result.
Interpret changes conservatively:
- A growing forward closure is a lead for dependency bloat, licensing or ownership exposure, and loading/analysis investigation.
- High fan-out is a lead for an over-broad consumer or aggregation target.
- High fan-in is a lead for a shared API whose changes deserve compatibility care and a broad validation universe.
- A path crossing an intended domain boundary is direct architecture evidence, even when the overall count is stable.
None of these observations automatically diagnoses poor cache performance or a
large rebuild. Bazel exposes different evidence for the target, configured
target, action, and observed execution layers.5 If select() or transitions
can change the answer, repeat the closure under the actual build context:
bazel cquery \
'deps(//app:server)' \
--universe_scope=//app:server \
--platforms=//platforms:linux \
--noimplicit_deps
Read every result as a label-plus-configuration node. One label may appear in
multiple configurations.6 If the hypothesis is “this structure registers too
much work,” use aquery 'deps(...)' --output=summary to inspect planned action
counts. If it is “this change caused more work or worse reuse,” compare matched
profiles and execution logs. Query topology proposes the next measurement. It
does not replace invocation evidence.
An Evidence-First Workflow
Use this sequence for a graph-health or dependency-bloat investigation:
- Write the observed problem and decision: unexpected dependency, risky API migration, analysis growth, or suspected action growth.
- Select the graph layer, roots or universe, configuration, and edge policy.
- Capture a baseline set and count. Retain the exact command and revision.
- Rank or shortlist candidates by direct fan-in, direct fan-out, or closure delta, without calling the metric a cause.
- Explain one candidate with
somepath(), then useallpaths()before claiming an edge removal breaks reachability. - Inspect the generating BUILD or macro definition and make the smallest semantics-preserving graph change.
- Rerun the same graph measurements and tests.
- Escalate to
cquery,aquery, profiles, or execution logs to verify the configured, planned-action, rebuild, or cache claim that motivated the work.
This workflow produces an audit trail from symptom to graph evidence, from graph evidence to a bounded edit, and from the edit to the layer-specific outcome.
If this analysis will decide which CI work may be omitted, hand its graph evidence to the versioned, fail-closed service contract in 6.5.1 Affected-Target Service Contract. The commands here explain topology within a stated universe; they do not by themselves reconcile two revisions or turn unknown coverage into an unaffected result.
Define Bazel target edges as dependent-to-dependency: fan-out then means direct
dependencies, while fan-in means direct dependents. Measure forward closure
with deps() and reverse impact candidates with universe-scoped rdeps(), then
explain outliers with paths and narrowly rendered DOT. Treat counts and trends
as signals only. Verify configuration claims with cquery, planned work with
aquery, and rebuild or cache claims with matched invocation evidence.
Check your understanding · 4 questions
1.rdeps(//services/payments/..., //lib:protocol) reports few consumers, but teams outside payments also use the library. What is the first issue to investigate?
Select one answer
2.somepath(A, B) returns one route. Before removing an edge and claiming that A can no longer reach B, what evidence is needed?
Select one answer
3.A repository-wide DOT rendering is unreadable, and a tool also needs exact graph data. Which responses follow the article's guardrails?
Select all that apply
4.Match each investigation claim to the evidence that can support it most directly:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
Footnotes
-
The Bazel Query Reference — target-graph semantics and the boundaries between query, cquery, and aquery ↩
-
Query quickstart — quoted dependency queries, implicit-edge filtering, and the DOT-to-Graphviz workflow ↩
-
Automating Build Files — practical costs of missing, unused, and over-broad dependencies ↩
-
The Bazel Query Reference — reflexive closures, universe-scoped reverse dependencies, graph factoring, and structured output formats ↩
-
Extracting build performance metrics — separate evidence sources for target, configured-target, action, profile, and execution analysis ↩
-
Configurable Query (cquery) — configured-target identity, resolved
select()branches, build options, and universe scope ↩