5.4.2 Critical Path
A slow build is not necessarily a build with too much total work. It may be a build whose result is gated by one timed chain of dependent actions. That chain is the critical path: under the observed action graph and durations, it sets the execution-time floor that more parallel capacity alone cannot cross.1
Read a timed dependency graph
Suppose two routes join at one final packaging action:
4 s observed
7 s observed
3 s observed
from Link service
Compile assets · 5 s
2 s observed
The routes can overlap, but actions connected by an artifact dependency cannot. The service route reaches the join after 14 seconds, later than the five-second asset route. Including the shared two-second package action, the service route is therefore the 16-second critical path. The asset work still consumes CPU and may matter for cost or contention. It simply does not set the ideal completion time in this observation.
This is an action-level result, not merely the longest target dependency path.
Targets may create several actions, and an action can wait for artifacts from
several producers. 5.2.3 bazel aquery — Action Graph shows the planned actions, inputs, and
outputs, but not their measured duration. A timing profile supplies that missing
execution evidence.2
5.4.1 Timing Profile Analysis develops the profile-reading mechanics used by this critical-path workflow.
The model has an important boundary. A profile describes one invocation with its cache state, machine load, execution strategy, remote queueing, and chosen configuration. Loading, analysis, garbage collection, downloads, scheduler contention, and other overhead can make elapsed time exceed the displayed action chain. Treat the critical path as a diagnosis of the recorded execution, not a timeless property of a target.
Timing Profile Analysis explains how to capture, preserve, and compare the
representative invocation. For this workflow, include primary outputs when you
need to correlate critical-path events to aquery actions:
bazel build //app:release \
--profile=release-profile.json.gz \
--experimental_profile_include_primary_output
The flag is experimental and version-sensitive. Verify it for the producing
Bazel version and keep that version with the artifact. The runnable case below
pins Bazel 9.0.0.3 In the trace viewer, find the Critical Path lane and use
the action-count and worker lanes around it to distinguish a dependent chain
from a period of general resource saturation.4
Diagnose the gate, not just the largest rectangle
Start at the end of the critical-path lane and walk backward. For each block, record:
- its mnemonic, description, duration, and primary output.
- whether most of its span is processing, remote setup or queueing, fetching, or another category.
- the next dependent block and the artifact that connects them.
- the concurrency around it in the action-count and worker lanes.
The longest action deserves attention, but it is not automatically the best intervention. A five-second code generator followed by ten small sequential compiles may expose a dependency waterfall. A three-second link surrounded by idle executors may be inherently serial. A remote action whose span is mostly queue or fetch time points toward execution infrastructure rather than compiler work. Official profiling guidance therefore calls out both individual slow critical-path actions and intervals where little parallel work is available.4
Correlate an event to the action plan by its primary output, then inspect that
action with aquery for its command, inputs, and producer/consumer shape.2
That keeps two questions separate:
- Profile: which recorded span gated completion, and where did its time go?
- Action graph: what command and artifact dependency made that span exist there?
Do not infer an artifact dependency solely because two trace blocks appear one after another. Resource limits and Bazel's scheduler can serialize actions that could run concurrently in the graph. The trace is primarily a time view, not a complete edge view.5 When the dependency itself is the question, continue with 5.4.6 Execution Log Graph.
Reproduce a joined profile and one intervention
The critical-path-profile snippet
uses six genrules: two independent action chains join at final_package. Its
BUILD graph makes
the artifact edges explicit. The verification script captures baseline and
optimized JSON profiles in temporary locations with primary outputs enabled, extracts complete action
events and critical-path components, and reports whether the two independent
first actions happened to overlap. It uses aquery to verify the deterministic
action structure: both branches' input/output relationships lead to
final_package, every action has the expected target, output, and mnemonic, and
the selected command has the exact baseline/optimized difference. The profile's
critical lane need only be a valid dependency path ending at final_package.
which branch wins is an observation, not a test invariant. The
structural extraction and aquery
retain the target, output, mnemonic, input, and command correlations. The saved baseline-aquery.txt and
optimized-aquery.txt files retain that action-graph evidence beside the
profiles.
The intervention is deliberately narrow and structural: the selected
generate_api command changes its simulated work from sleep 0.35 to
sleep 0.08, while the action dependency graph is unchanged. One local Bazel
9.0.0 observation is preserved in the
captured output.
In that observation, both critical lanes followed generate API → compile
service → link service → final package, and the optimized capture had a shorter
generate_api span and critical-path sum. This is evidence about those two
captures, not a performance threshold or a promise that every rerun improves.
Timing is observational: a single sample is not a performance test,
and the verifier does not require the optimized sample to be faster. It asserts
the action DAG and join, target/output/mnemonic correlations, exact commands,
and that any reported critical lane follows a valid path to final_package.
In the checked-in captures, API generation overlapped asset compilation.
that describes those runs rather than guaranteeing scheduler behavior. The
action graph proves that final_package consumes both terminal branch outputs.
Run python3 verify_profile.py to make fresh, comparable temporary captures.
the script reports the locally observed spans without replacing the checked-in
observation. Maintainers can deliberately refresh all snapshot files together
with python3 verify_profile.py --update-snapshots.
Decide: The current critical path is 60 seconds. An unrelated 45-second branch runs at the same time. Does reducing that branch to 20 seconds guarantee zero improvement in elapsed time?
Reveal
No. In an ideal fixed graph with unlimited resources, shortening the 45-second branch leaves the 60-second lower bound unchanged. In the recorded system, that branch may contend for CPU, memory, network, remote workers, or downloads with the critical chain. Changing it can change the critical chain's observed durations. Measure the same scenario again instead of treating path membership as proof of no effect.
Check the paths waiting behind it
Criticality is comparative. If the longest route is 60 seconds and another is 59.5 seconds, removing two seconds from the first route can improve the build by only half a second before the second route becomes critical. With measurement noise, the identity of the apparent winner may alternate between runs.
This is why “optimize only actions on the critical path” is a prioritization heuristic, not a law. Under the narrow mathematical model—a fixed graph, fixed durations, and unlimited processors—shortening an action outside every critical path cannot reduce the makespan. Real interventions may also:
- release a contested resource earlier.
- turn a cache miss into a hit.
- remove or reorder action dependencies.
- change action granularity or scheduling overhead.
- improve a near-critical route that becomes decisive under another edit.
Execution-graph data can quantify an action's critical-path contribution and its drag, the time potentially saved by removing that node. It also exposes the dependency structure needed to compare near-critical alternatives.6,7 Use that structural view before funding a large rewrite based on one colored lane.
Run a controlled optimization loop
A defensible critical-path experiment is small and repeatable:
- Define the user scenario: clean, no-op, or a named incremental edit. Do not substitute a clean build for an incremental complaint.
- Record the revision, Bazel version, targets, flags, cache state, execution environment, and several baseline runs.
- Identify the gating chain and at least one plausible near-critical route.
- Form one causal hypothesis: reduce an action's work, remove an unnecessary edge, improve its cacheability, or reduce evidenced queue/fetch time.
- Change one variable and repeat the same runs.
- Compare median elapsed time, critical-path duration and identity, and the relevant action spans. Also confirm correctness and the intended build mode.
Splitting a large action can expose parallelism, but it can also add analysis, scheduling, and transfer overhead. The target-boundary trade-off is developed in 5.4.3 Measuring Granularity Trade-offs. Likewise, adding machines helps only when the trace shows runnable work waiting for capacity. A serialized dependency chain cannot consume infinite workers.8
The critical path is the longest timed chain of dependent actions in one recorded build. It is the right place to start when execution gates elapsed time, but it is neither the target graph nor a permanent list of slow actions. Read the critical-path lane with concurrency and wait categories, correlate suspicious spans to action outputs and dependencies, and estimate how much near-critical alternatives limit the benefit of an intervention.
Treat “off-path work has zero effect” as true only for the ideal fixed-graph, fixed-duration, unlimited-resource model. In practice, inspect near-critical routes and contention, change one causal variable, and accept an optimization only when repeated end-to-end measurements move the user-visible scenario.
Check your understanding · 4 questions
1.Two independent action branches each contain 20 seconds of work and join at a 2-second packaging action. If both branches can run concurrently with unlimited resources, what sets the ideal execution time?
Select one answer
2.Match each evidence source to the question it can answer directly:
Drag each answer onto the matching prompt, or click an answer and then click a prompt
3.A measured 60-second critical route has a 59.5-second near-critical alternative. What is the best prediction for an intervention that removes 2 seconds only from the current critical route?
Select one answer
4.Which practices belong in a controlled critical-path optimization experiment?
Select all that apply
Footnotes
-
Migrating a Mobile Super App to Bazel — the timed dependency-chain definition and unlimited-parallelism lower bound ↩
-
Action Graph Query (aquery) — correlating profile events and planned actions through primary outputs, and the boundary between action shape and execution timing ↩1 ↩2
-
Command-Line Reference — current
--profile,--slim_profile, and profile-retention flag semantics ↩ -
JSON Trace Profile — automatic and explicit profile capture, retained profile location, viewers, special lanes, and common performance diagnoses ↩1 ↩2
-
Bazel's Tracing and Logging Facilities — practical critical-path navigation and the warning that trace order does not expose all action dependencies ↩
-
Extracting build performance metrics — execution graph logs, action interdependencies, and drag ↩
-
Bazel Build Data: Avoiding Pitfalls in Debugging and Optimizing Builds — action contribution and multiple near-critical-path interpretation ↩
-
Dynamic Execution — profiling resource contention and strategy behavior rather than assuming every mnemonic benefits from more concurrent execution ↩