Repository navigation
perf(executor): start each target as soon as its dependencies finish - #155
Merged
Merged
Conversation
The executor ran one DAG level at a time, so a level began only after every target of the previous level ended and a run took the sum of the per-level maxima. Schedule from a dependency-ready queue instead: a target starts when its last dependency completes. Exclusive-resource locks now span the whole run, failure blocking and result ordering are unchanged, and levels remain the display grouping and tie-break. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
calvin-archastro
marked this pull request as draft
October 16, 2026 19:58
…esource locks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
calvin-archastro
marked this pull request as ready for review
October 16, 2026 20:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Review on ArchCode
Problem and author intent
asterran targets one DAG level at a time (compute_target_levelsgives level = 1 + max dep level; the loop inexecute_target_setwaited for every target in a level before starting the next). A target whose dependencies were done still waited for unrelated slower targets at lower levels, so lane wall time was the sum of per-level maxima, not the critical path.Seen in firstlanding CI on 2026-10-15 (Aster 0.19.0):
services/rust/archdev:build(level 1, 3m13s) held back:test-ci,cli-foundations:build,developer-platform-sdk:build, which started the second it ended.cross-project-reads-1(6m24s) only started afterdevboxctl:test-ci(3m54s) at a lower level finished, although it depended on neither.:buildtargets,ensure-node-ptyandweb:buildran only afterbuild-rust-archdev(5m47s) although nothing depended on it.Goal: start every target as soon as its dependencies have finished, with no change to exclusive resources, failure semantics, output or caching.
What changed (backend-only,
src/executor/runner.rs)Before:
levels = compute_target_levels(..); per level: block targets with failed deps, spawn the rest, drain the channel, join.After:
ReadyQueuetracks unmet in-run dependencies per target. The loop launches everything ready, then blocks on the result channel (no polling); each completion releases dependents.exclusive_resourcesBlocked: prerequisite target(s) failed: ..once their level startsCancelled)Unchanged: caching,
--jsonschema, logs, progress output, test capture/retry/quarantine, partition balancing.compute_target_levelsstays and is still used for dry-run plans (aster affected --dry-run), result grouping and tie-breaking. There is no existing jobs/parallelism limit in the executor (every ready target already got its own thread); I did not add one. Peak concurrency can now be higher than before because ready targets from different levels overlap.Testing
Canonical end-to-end proof:
dependent_starts_while_unrelated_lower_level_target_still_runsinsrc/executor/runner.rs. It drivesExecutor::execute_targetswith realshchild processes: slow target A waits (cap 5s) for a marker file that only C writes, where C depends on fast B. A passes only if C started while A was still running; under the level barrier A times out and fails. No wall-clock threshold.Other new tests (same module; run by CI's
cargo test --locked --all-targets --all-features):failure_blocks_transitive_dependents_but_not_independent_targetsresults_are_grouped_by_level_with_blocked_targets_firstdependency_cycle_still_runs_every_targetexclusive_resource_sections_never_interleave_across_levels: holders at levels 0/1/2 append start/end events to a log;firstholds its section untilsecond(ready via a fast gate) is runnable, so contention is guaranteed. Asserts every target ran and the log reads strict start/end pairs.without_the_shared_resource_the_same_sections_do_overlap: positive control, same graph without the resource;firstwaits forsecondto start, so the log must interleave.targets_holding_several_resources_do_not_deadlock_or_overlap: targets with[res_b,res_a],[res_a,res_b], subsets and a dependent; the run ends and sections of targets sharing a resource never overlap.panicking_target_is_reported_blocks_dependents_and_frees_its_resource: injected panic (#[cfg(test)]hook) gives a failed "panicked" result, blocks its dependent, releases its resource, run terminates.cache_hit_and_skipped_target_each_release_their_dependents.tests/integration.rs::terminating_aster_mid_run_launches_nothing_more_and_exits: SIGTERM mid-run; the dependent never launches, running target drains, exit 143, no hang.different_exclusive_resources_run_at_the_same_time(rendezvous via marker files)ready_queue_releases_targets_as_dependencies_completeReplaced the two timing-based
execute_target_levelresource tests with the marker-based ones above. Local:cargo fmt --check,clippy -D warnings,cargo doc -D warnings, fullcargo test --all-targets --all-featurespass excepttests/dev_services.rs::dynamic_port_bundles_are_distinct_propagated_and_released, which fails identically on unmodifiedorigin/mainon this machine (port/environment, unrelated to the executor).Simulated savings (simulation, not a measured run)
Method: per-target START/PASS timestamps from firstlanding CI logs (run 38942230866 jobs, plus the 2026-10-15 Go and CLI e2e jobs above), dependency edges from
aster --json graphon current firstlanding main, unlimited parallelism (as Aster has today). "Level model" replays the old barrier and matches the logged wall time within ~1 min on every lane (validation); "ready model" is the critical path. Ignoresexclusive_resourcescontention and slowdown from extra concurrent load, so real savings will be somewhat lower. Lanes with fewer than 4 targets in the main invocation are omitted.Risk: low-medium
depends_on) were previously separated by accident of level ordering and may now overlap. Such hidden ordering would show up as new flakes.User impact
Faster runs on graphs with uneven level durations; no CLI/config change. Progress lines (START/PASS) for later-level targets can now appear before earlier-level targets finish.
Rollout caveat: cache decision moves earlier
The cache-hit check now runs when a target becomes ready, which can be earlier than before (previously after every target of the lower levels had finished). A generator that writes into a target's inputs without being declared as one of its dependencies could therefore be missed: the target can get a stale cache hit where the level barrier used to give the generator time to finish first. Declare such generators in
depends_onbefore bumping Aster.Follow-ups
ASTER_VERSIONin.github/workflows/ci.ymlafter a release (not part of this PR).--jobslimit and longest-duration-first ordering using recorded durations.🤖 Generated with Claude Code