fix(lsp): fix CPU busy loop by creating tokio runtime before JsRuntime in TSC thread#35595
Conversation
|
This is correct and the right minimal fix for the LSP — entering the runtime context before One thing worth considering: this fixes the symptom at this call site, but the underlying footgun in An alternative (or complement) is to make // Lazily created only when a handle-less isolate actually posts a delayed task.
static DELAYED_TASK_RT: std::sync::LazyLock<tokio::runtime::Runtime> =
std::sync::LazyLock::new(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_time()
.build()
.unwrap()
});
fn spawn_delayed_task(key: usize, task: v8::Task, delay_in_seconds: f64) {
let map = ISOLATE_ENTRIES.lock().unwrap();
let Some(entry) = map.get(&key) else { return };
let handle = entry
.handle
.clone()
.unwrap_or_else(|| DELAYED_TASK_RT.handle().clone());
let tasks = entry.tasks.clone();
let waker = entry.waker.clone();
handle.spawn(async move {
tokio::time::sleep(Duration::from_secs_f64(delay_in_seconds)).await;
tasks.lock().unwrap().push(task);
waker.wake();
});
}This keeps the #35408 guarantee (no silently-dropped foreground tasks) while making it impossible for a handle-less isolate to spin — the delay is respected no matter how the isolate was created. Cost is one background thread, and only when a handle-less isolate actually schedules a delayed task. I'd suggest landing this PR as-is for the immediate LSP regression, and optionally hardening |
|
@bartlomieju Thank you for the detailed review and the suggestion. The global fallback runtime approach makes a lot of sense to me. |
|
Confirming this also affects stable Setup: Zed editor on Linux, Repro: Editing any small Diagnostics from Claue:
Confirms fix location: Downgraded to Happy to test a canary build with #35595's patch applied if that'd help get it merged. |
When an isolate is registered without a tokio runtime handle, the delay of a posted delayed task cannot be honored: queuing it immediately busy-loops the event loop on self-rescheduling V8 tasks (the bug this PR fixes in the LSP), and dropping it loses foreground work (denoland#35345). Fail loudly instead, with a message pointing at the misconfiguration. A regular panic can't be used because V8 calls this from C++ frames that Rust can't unwind through, which aborts with the message swallowed. Fix all call sites that created a JsRuntime outside a tokio runtime context: dcore's main (same latent bug as the LSP one), the checkin test harness, deno_core benches, snapshot creation in build scripts (create_snapshot now enters a private runtime), and two GC-heavy deno_core tests that trigger V8 memory-reducer delayed tasks.
|
I pushed two commits to this branch (thanks for enabling maintainer edits):
After more thought I moved away from the fallback-timer-runtime idea from my earlier comment. Instead, when an isolate registered without a tokio handle posts a delayed task, we now fail loudly (print a clear error and abort) rather than queue the task immediately: the immediate-queue fallback is exactly what turned V8's self-rescheduling GC tasks into the busy loop reported here, and dropping the task would reintroduce #35345-style hangs. A regular Making the failure loud immediately surfaced every other place that created a
Your LSP fix stays as-is and is still the important part — the ordering it establishes is now effectively mandatory. Verified locally: |
Fixes the CPU busy loop reported in #35408 (comment).
After #35408,
spawn_delayed_taskfalls back to immediate queuing whenhandleisNone. The LSP's TSC thread createsJsRuntimebefore the tokio runtime inrun_tsc_thread, so the isolate is registered withhandle: None. V8 internal delayed tasks that reschedule themselves then cause a busy loop.This PR moves the tokio runtime creation before
JsRuntime::new()so the isolate gets a proper handle.AI disclosure: Claude Code was used to help the PR description.