Skip to content

apollo_batcher: skip spawn_blocking for the non-blocking is_done check#14854

Open
gkaempfer wants to merge 1 commit into
main-v0.14.3from
claude/perf/apollo-batcher-inline-is-done-73942
Open

apollo_batcher: skip spawn_blocking for the non-blocking is_done check#14854
gkaempfer wants to merge 1 commit into
main-v0.14.3from
claude/perf/apollo-batcher-inline-is-done-73942

Conversation

@gkaempfer

Copy link
Copy Markdown
Contributor

Summary

Follow-up perf optimization found while reviewing #14842 (merged), which split the block-builder loop's polling cadence so it sleeps only results_polling_interval_millis (default 10ms) instead of tx_polling_interval_millis (e.g. 200ms in prod) while transactions are in progress. That change makes the while loop in build_block_inner iterate roughly 20x more often per block whenever there are in-flight transactions.

On every iteration, the loop checked whether the block is full via:

let executor = self.executor.clone();
if !self.execution_params.is_validator
    && spawn_blocking(move || lock_executor(&executor).is_done())
        .await
        .expect("Checking completion should succeed.")

is_done() (TransactionExecutorTrait::is_doneConcurrentTransactionExecutor::is_doneScheduler::done()) is just a single atomic load (self.done_marker.load(Ordering::Acquire)) — no I/O, no meaningful CPU work, and the underlying mutex (lock_executor's try_lock) is uncontended by design since only this one task ever touches the executor. Routing this trivial, non-blocking read through spawn_blocking costs a full dispatch round-trip to Tokio's blocking thread pool (channel send, thread wake, context switch) for no benefit — overhead that is now paid ~20x more often per block after #14842.

This PR calls is_done() directly, inline, on the async task instead:

if !self.execution_params.is_validator && lock_executor(&self.executor).is_done() {

No other executor access changes — handle_executed_txs() (which does real, non-trivial work extracting execution outputs) still goes through spawn_blocking as before.

Expected impact

Removes one blocking-thread-pool dispatch per block-builder loop iteration (out of the two that existed per iteration), which now fires on the tightened ~10ms cadence rather than the previous ~200ms cadence during active execution — i.e. up to ~20x more frequently per block than before #14842.

Verification

  • cargo build -p apollo_batcher — succeeds.
  • SEED=0 cargo test -p apollo_batcher — all block_builder_test cases pass (24/24), including case_4_block_full_before_is_done and case_5_block_full_after_is_done, which exercise this exact code path. (One unrelated pre-existing failure, batcher_test::call_contract_success, reproduces identically on the base commit without this change — it fails trying to download the Cairo compiler, a sandbox network limitation, not a regression.)
  • Reviewed by an independent Opus pass focused on correctness of the "non-blocking" claim, panic/race risk from moving the check off the blocking pool, and test coverage — no blocking or suggestion-level findings.

🤖 Generated with Claude Code


Generated by Claude Code

is_done() only reads an atomic flag (Scheduler::done), so routing it
through spawn_blocking costs a full blocking-pool dispatch round-trip
for no benefit. PR #14842 tightened the block-builder loop's sleep to
results_polling_interval_millis (10ms default) while txs are in
progress, so this check now runs ~20x more often per block than
before. Call it inline instead.

Co-authored-by: Claude <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Single hot-path optimization with unchanged semantics; mutex use remains single-task as before, and block-full tests cover this path.

Overview
Proposer block-full detection in build_block_inner no longer routes is_done() through spawn_blocking. The check runs inline via lock_executor(&self.executor).is_done() on each loop iteration.

This matches the fact that completion is a cheap atomic read (Scheduler::done), while the loop now wakes on the tighter results_polling_interval_millis cadence—so avoiding a blocking-pool round-trip per iteration matters more. Heavier executor work (handle_executed_txs, add_txs_to_executor, close_block, etc.) still uses spawn_blocking unchanged.

A short comment documents why this path is treated differently from other executor accesses.

Reviewed by Cursor Bugbot for commit 5db378b. Bugbot is set up for automated code reviews on this repo. Configure here.

@reviewable-StarkWare

Copy link
Copy Markdown

This change is Reviewable

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.

4 participants