Skip to content

fix(job): resolve 100% CPU usage when idling on future timeouts#4926

Closed
ashnaaseth2325-oss wants to merge 4 commits intoboa-dev:mainfrom
ashnaaseth2325-oss:fix/timeout-busy-spin
Closed

fix(job): resolve 100% CPU usage when idling on future timeouts#4926
ashnaaseth2325-oss wants to merge 4 commits intoboa-dev:mainfrom
ashnaaseth2325-oss:fix/timeout-busy-spin

Conversation

@ashnaaseth2325-oss
Copy link

Summary

This PR fixes a performance bug where the SimpleJobExecutor pins a CPU core at 100% while waiting for a setTimeout or setInterval delay. The issue occurs because the executor doesn't have a way to "sleep" when there are no active async jobs to poll, causing it to spin in a tight loop until the timer expires.

Steps to Reproduce

Run any script with a setTimeout delay:

setTimeout(() => console.log("Done"), 5000);

During the 5 second wait, the engine burns 100% of a CPU core despite having no work to do.

Problem

The current run_jobs_async loop has a logic gap. We prevent the loop from terminating (because a timeout is pending), but we only yield execution if there are active async futures. If the async group is empty, the engine falls into a "tight spin" ; it stays awake but has nothing to execute, leading to unnecessary high CPU usage.

Fix

I updated the loop guards to recognize the "idle wait" state. If the sync queues are empty and no async work is active, the executor now calculates the time remaining until the next scheduled TimeoutJob and parks the thread until that deadline.

Result

  • CPU usage during timer delays drops from 100% to effectively 0%.
  • Periodic tasks (like heartbeats) now consume negligible resources between ticks.
  • The engine is now stable for long running server or embedded usage.

This fix significantly improves the efficiency of the SimpleJobExecutor for any host environment using standard JS timers.

ashnaaseth2325-oss added 3 commits March 7, 2026 14:01
When only future-scheduled timeout jobs remain in the queue and no
async futures are in-flight, the executor looped tightly through
yield_now().await, consuming 100% CPU for the full wait duration.

Fix by sleeping on a oneshot channel until the earliest timeout is
due before re-entering the loop.  Also correct the split_off boundary
from &now to &(now + 1ns) so that jobs whose deadline equals the
current instant are dispatched immediately rather than deferred.

Signed-off-by: ashnaaseth2325-oss <ashnaaseth2325@gmail.com>
@github-actions
Copy link

github-actions bot commented Mar 7, 2026

Test262 conformance changes

Test result main count PR count difference
Total 52,963 52,963 0
Passed 49,687 49,687 0
Ignored 2,262 2,262 0
Failed 1,014 1,014 0
Panics 0 0 0
Conformance 93.81% 93.81% 0.00%

Tested main commit: b3d820369072123ded8defc927405b99b9012648
Tested PR commit: 3c6081e269e1f317bcfc2b4256749b7747299d5f
Compare commits: b3d8203...3c6081e

Replace the bare `rx.await` sleep with a `poll_fn`-based future that
re-checks `context.clock().now() >= deadline` on every poll.  This
lets test-controlled clocks (FixedClock) advance the deadline without
waiting for real wall-clock time, while still parking the thread in
production via the oneshot channel and background thread::sleep.

Also collapse the nested `if` to satisfy clippy::collapsible_if.

Signed-off-by: ashnaaseth2325-oss <ashnaaseth2325@gmail.com>
@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

❌ Patch coverage is 96.29630% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 57.66%. Comparing base (6ddc2b4) to head (3c6081e).
⚠️ Report is 825 commits behind head on main.

Files with missing lines Patch % Lines
core/engine/src/job.rs 96.29% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #4926       +/-   ##
===========================================
+ Coverage   47.24%   57.66%   +10.41%     
===========================================
  Files         476      556       +80     
  Lines       46892    60952    +14060     
===========================================
+ Hits        22154    35147    +12993     
- Misses      24738    25805     +1067     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ashnaaseth2325-oss
Copy link
Author

Hello @jedel1043 @hansl
This PR fixes a busy spin in SimpleJobExecutor when waiting for future timeouts. The executor now waits for the next timeout instead of continuously polling.
All CI checks are passing. Thank You!

@ashnaaseth2325-oss
Copy link
Author

Hello @jedel1043 @hansl @nekevss
Just a gentle follow up.
Kindly view the pr whenever you have time . Thanks!

Comment on lines +793 to +796
std::thread::spawn(move || {
std::thread::sleep(dur);
let _ = tx.send(());
});
Copy link
Member

Choose a reason for hiding this comment

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

This is very inefficient. We are essentially creating a whole thread to just wait for dur milliseconds.

I don't think this is something we can fix on the SimpleJobExecutor. This executor should be kept as simple as possible, but to properly support this we need an async executor to sleep using an async timer.

@jedel1043
Copy link
Member

Related: #4994. Uses no additional threads by integrating with smol

@ashnaaseth2325-oss
Copy link
Author

Hello @jedel1043 Thanks for the feedback! That’s a fair point! Spawning a thread just to wait for dur isn’t ideal for a simple executor. I agree this is probably better handled by a proper async executor.

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