5.4.3 Measuring Granularity Trade-offs
Splitting one target into ten can make a build faster, slower, or merely more complicated. The result depends on what the rule turns those targets into: how much loading and analysis work Bazel performs, which actions can run concurrently, and which actions remain reusable after a typical edit. Target granularity is therefore a measurement problem, not a naming convention.
fewer units reduce analysis, scheduling, sandbox, or cache-lookup overhead.
the resulting actions consume more inputs, block useful concurrency, or rerun after more edits.
the rule produces smaller independent actions, better reuse, or useful concurrency.
more units and edges to analyze; often more actions to schedule and look up.
State the structural hypothesis, then measure it
Begin with a boundary whose interface and consumer purpose are already clear. 5.3.3 Target Boundary Design explains how to choose and inspect that structural boundary, while 2.4.3 What Makes a Cache Hit explains action identity. Now state the performance prediction precisely: will the change reduce loading or analysis, create useful action concurrency, or preserve more action results after a representative edit? These are different hypotheses and require different observations.
Hold the top-level target, flags, and configuration constant across the
comparison. Use 5.2.2 bazel cquery — Configured Graph to record and verify the configured closure
reached by each design, and 5.2.3 bazel aquery — Action Graph to record the planned actions
before and after. Then use BEP, profiles, and execution logs to measure what the
invocation actually analyzed, scheduled, executed, and reused.
The three costs move in different directions
Finer boundaries can reduce an edit's affected action set and expose concurrent
work, but can also increase loading, analysis, scheduling, sandbox, and cache
lookup overhead. Only nodes reached by the request matter. Bazel's BEP exposes
request-size metrics that make the experiment concrete: packages_loaded,
targets_configured, actions_created, and actions_executed.1
At the other extreme, a coarse target can reduce graph and scheduling overhead yet feed many sources into a long action. That action may serialize work the tool could otherwise perform independently, and an edit to any consumed source changes its inputs. Splitting such an action can help when it is slow or blocks the critical path. Bazel's profile guidance explicitly recommends considering smaller actions for that case.2
There is a third failure mode between those extremes: many actions that are so short that process startup, sandbox preparation, remote queueing, or cache lookup occupies much of their elapsed time. A graph with abundant theoretical parallelism can still lose to this fixed per-action cost. The timing profile shows action concurrency and idle regions, while execution metrics can separate processing from setup, fetch, and other overhead.2
The right boundary is therefore workload-specific. A directory, language package, or component can be a sensible initial boundary because it expresses an API and ownership boundary. It is not evidence that the boundary is performance-optimal.
Build a matched experiment matrix
Granularity affects clean and incremental builds differently. Compare them separately: clean builds expose available parallelism and graph-construction cost, while incremental builds expose the reuse achieved for edits developers actually make. Cache-dependent metrics are meaningful only when the cache state and invocation conditions are controlled.1
Choose one representative top-level target, one configuration, and two or three change scenarios—for example, editing a leaf implementation, editing a widely used interface, and changing no files. Run both boundary designs under the same Bazel version, flags, machine or worker class, remote-output policy, and declared cache condition. Repeat timing samples; do not compare one lucky run against one unlucky run. For each cell in the matrix, record:
| Question | Evidence |
|---|---|
| Did loading or analysis grow? | Packages reached and loaded for this request via BEP packages_loaded. Target-configuration instances analyzed via targets_configured, and the profile's runAnalysisPhase span |
| Did the registered action graph change as intended? | aquery --output=summary and BEP actions_created. Use actions_executed separately for the actions that actually ran |
| Did execution gain useful concurrency? | Profile action-count lane, idle gaps, and critical-path actions |
| Did the edit reuse more work? | Actions executed after the same edit. Use execution-log comparison when a miss is unexpected |
| Did users get a faster result? | Repeated wall time for the same scenario on a controlled machine |
The profile is important because sequential-looking actions do not prove a dependency between them. Resource constraints and scheduler decisions can also make independent actions run in series. Query tools reveal graph dependencies, whereas the profile reveals when work ran.3
Do not begin with:
bazel query 'deps(//...)' | wc -l
That number mixes rules, source files, generated files, and other nodes in an unconfigured transitive closure. It also says nothing about configurations, actions, cache hits, durations, or the critical path. It can serve as a coarse regression signal only if the expression, universe, query options, and Bazel version stay fixed. When it moves, use the query family to explain which nodes changed, then measure the phase or action metric tied to the suspected cost.
Make the decision from the predicted bottleneck
Start from the observed limiting cost:
- If loading and analysis dominate, first remove unnecessary dependencies or targets. Merging boundaries is justified only when it reduces that cost without materially enlarging common edit rebuilds.
- If a long action sits on the critical path while capacity is idle, test a split that creates genuinely independent actions. Then confirm that the new work overlaps in the profile.
- If incremental edits execute too much work, inspect which actions consume the edited input. Remove only inputs that are unnecessary or overdeclared, or redesign the action so it genuinely consumes fewer inputs, and verify a smaller affected set before and after. Every file or other input the action actually reads must remain declared. Deleting a real input breaks hermeticity and can make cache results incorrect.
- If many tiny actions spend most of their time in setup, queueing, or cache access, test batching or a coarser boundary and verify that the reduced overhead outweighs lost concurrency and reuse.
Make the result falsifiable. Before looking at the comparison, write down which metric should move, in which scenario, and why. Reject the performance claim if the relevant action shape does not change, the predicted metric stays flat, or end-to-end time moves only within the noise seen in repeated matched runs. A neutral performance result does not invalidate an interface boundary that is already useful; it only rejects the speed claim.
Keep maintainability and correctness as constraints on every experiment. A small wall-time win that multiplies BUILD metadata and dependency edges may not survive its ongoing cost. Preserve every real action input, run the relevant correctness checks, and record the final decision as: scenario, evidence, observed trade-off, and keep-or-reject verdict.
Treat a target split or merge as a controlled performance experiment. State
which loading, analysis, action-shape, concurrency, or reuse metric should move;
compare coarse and fine designs under the same target, configuration, Bazel
version, environment, cache condition, and edit scenario; and use cquery,
aquery, BEP, timing profiles, and paired execution evidence for the questions
each can answer. Preserve correctness and every real action input. Keep the
performance change only when repeated evidence moves the predicted bottleneck
and improves the user-visible scenario enough to justify its ongoing cost.
Check your understanding · 3 questions
1.Match each graph layer to what it represents:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
2.Which outcomes can result from splitting a coarse target into finer boundaries?
Select all that apply
3.A team wants to compare coarse and fine target designs for leaf edits. Which experiment gives the most useful evidence?
Select one answer
Footnotes
-
Breaking down build performance — clean versus incremental comparisons and BEP request-size metrics for packages, configured targets, created actions, and executed actions ↩1 ↩2
-
JSON Trace Profile — action concurrency, analysis spans, critical-path bottlenecks, and per-action setup or fetch overhead ↩1 ↩2
-
Bazel's Tracing and Logging Facilities — why timing order can reflect scheduler constraints rather than action dependencies, and when to switch to query tools ↩