Skip to content

Rewatch: replace wave scheduler with DAG + critical-path priority#8374

Open
jfrolich wants to merge 2 commits intorescript-lang:masterfrom
jfrolich:no-waves
Open

Rewatch: replace wave scheduler with DAG + critical-path priority#8374
jfrolich wants to merge 2 commits intorescript-lang:masterfrom
jfrolich:no-waves

Conversation

@jfrolich
Copy link
Copy Markdown
Collaborator

Summary

  • Replace rewatch's wave-based topological scheduler (par_iter per wave) with a work-stealing DAG dispatcher. Waves stall on the slowest file per round; DAG scheduling lets a module start as soon as its own dependencies finish.
  • Order the ready queue by critical-path priority — the length of a module's longest downstream dependency chain. Bottlenecks start first instead of being picked arbitrarily from within a wave.
  • All changes are in rewatch/src/build/compile.rs; no changes to bsc, the wire protocol, or rescript.json semantics.

Design

  • Workers run inside rayon::in_place_scope, bounded to rayon::current_num_threads() concurrent tasks so the BinaryHeap ordering actually decides dispatch (spawning everything would lose priority to rayon's deque).
  • Completions flow back over std::sync::mpsc. The dispatcher on the main thread:
    • decrements in-universe pending_deps counters for dependents,
    • propagates the dirty flag locally when a module's cmi changed,
    • pushes newly-ready modules onto the priority heap.
  • Workers only read &BuildState; all BuildState mutations (compile_state, timestamps, compile_dirty) happen after the scope exits, keeping worker reads and main-thread writes disjoint without any interior mutability.
  • Cycle detection triggers when the heap drains with in-flight == 0 and the universe incomplete, then delegates to the existing dependency_cycle::find / format path — same error message as before.
  • Critical-path priorities are computed once per build with a reverse-topological sweep; modules caught in a cycle get priority 0 and are surfaced by the stall check.

Test plan

  • cargo build --manifest-path rewatch/Cargo.toml
  • cargo clippy --manifest-path rewatch/Cargo.toml --all-targets --all-features -- -D warnings
  • cargo fmt --check --manifest-path rewatch/Cargo.toml
  • cargo test --manifest-path rewatch/Cargo.toml — 68 unit tests pass
  • make test-rewatch — full integration suite passes, including:
    • 09-dependency-cycle (cycle snapshot unchanged)
    • 13-no-infinite-loop-with-cycle
    • watch-mode tests (warning persistence, atomic saves, new-file pickup, config-change rebuild)
    • snapshot tests (16-snapshots-unchanged)

Notes

  • Log/error output ordering is now determined by module completion order rather than wave boundaries. Existing snapshot tests still pass because they assert on content rather than relative ordering of messages from independent modules.
  • clean_modules (which the old code built but never read) is gone.

🤖 Generated with Claude Code

Instead of compiling modules in topologically-sorted waves (which stall
on the slowest file per wave), schedule them from a priority queue on a
work-stealing dispatcher. Priority is the length of the module's longest
downstream dependency chain, so bottlenecks start first.

Workers run inside a `rayon::in_place_scope` bounded to
`rayon::current_num_threads()` so the heap ordering actually decides
dispatch. Completions flow back over `std::sync::mpsc`; the dispatcher
updates pending-dep counters, propagates the dirty flag, and pushes
newly-ready modules back onto the heap. `BuildState` mutations happen
after the scope exits so worker reads and main-thread writes stay
disjoint. Cycle detection now triggers when the heap drains without
completing the universe.

Signed-Off-By: Jaap Frolich <jaap@tella.com>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cknitt
Copy link
Copy Markdown
Member

cknitt commented Apr 19, 2026

@codex review

- Restore the `.unwrap()` on `package.namespace.to_suffix()` for mlmap
  dispatch so a missing namespace suffix panics loudly instead of being
  silently papered over.
- Drop the defensive `.unwrap_or_default()` on the post-completion
  dependents lookup and the `if *count == 0 { continue }` guard on the
  pending-deps decrement; both can only fire when the graph itself is
  inconsistent, and hiding that would mask bugs. Align with the
  unwrap-style assertions the old scheduler used.
- Sort `results_buffer` by module name before applying results so the
  returned `compile_errors` / `compile_warnings` strings and the per-
  package `.compiler.log` writes are deterministic across runs despite
  non-deterministic completion order.

Signed-Off-By: Jaap Frolich <jaap@tella.com>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a22a9437d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +395 to +397
if !is_clean {
dirty_set.insert(dep.clone());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist propagated dirty flags before aborting on errors

When a dependency finishes with !is_clean, this code only updates the local dirty_set; it does not immediately set module.compile_dirty on the dependent. Because the dispatcher stops spawning new work after the first compile error (has_errors), any dependents that were marked dirty but never scheduled in this run can remain compile_dirty = false in build_state, so a subsequent build may skip recompiling stale modules even though an upstream .cmi changed. The previous scheduler wrote compile_dirty = true directly during propagation, which avoided this incremental-build inconsistency after failed builds.

Useful? React with 👍 / 👎.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 19, 2026

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@8374

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@8374

@rescript/darwin-x64

npm i https://pkg.pr.new/@rescript/darwin-x64@8374

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@8374

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@8374

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@8374

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@8374

commit: 6bc57a0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants