perf(cdp): scope the V8 lock per connection to run connections in parallel

With every connection on its own thread (previous commit), the process-wide V8
lock served no safety purpose across connections yet forced all of them to
serialize on one mutex, so throughput did not scale with concurrency. Move the
lock into CdpContext so each connection has its own: a connection's nav task and
command dispatch still coordinate on one page's isolate at a time on its thread,
but different connections no longer block each other. Delete the now-unused
process-wide obscura_js::v8_lock.

Measured on a local CPU-bound fixture, page throughput stays flat (~75/s from
concurrency 1 to 8) with the global lock and scales to about 2.5x at concurrency
8 with the per-connection lock, with no crash at any concurrency. Obstacle
course 33/33, obscura-cdp 76/76.

Refs #430.
This commit is contained in:
sycatle committed 2026-07-20 16:37:50 +02:00
1 parent 6c7b2b0cec
commit 9065f383de
4 files changed
+21 -34

No files matched your search

-1
View File
@@ -6,7 +6,6 @@ pub mod module_loader;
pub mod runtime;
pub mod ops;
pub mod v8_flags;
pub mod v8_lock;
pub mod markdown;
pub use markdown::HTML_TO_MARKDOWN_JS;
-27
View File
@@ -1,27 +0,0 @@
//! Process-wide async lock that serializes V8 work across all `JsRuntime`s.
//!
//! V8's invariant: on any given OS thread, only one `Isolate` may be entered
//! at a time, and HandleScope/ContextScope stacks must unwind on the Isolate
//! they belong to. Obscura's CDP server runs every `JsRuntime` (one per Page)
//! on a single OS thread via `tokio::task::LocalSet` + `spawn_local`. As soon
//! as two pages' V8-touching futures interleave across an `.await`, V8 trips
//! its `heap->isolate() == Isolate::TryGetCurrent()` check and aborts the
//! whole process (no Rust panic; `V8_Fatal` calls `abort(3)`).
//!
//! Acquiring this lock around any block that calls `JsRuntime::execute_script`
//! or `JsRuntime::run_event_loop` keeps that block contiguous on the thread:
//! V8 fully exits the prior Isolate before the next page is allowed in. This
//! converts the abort into latency. It is the issue-19 "Option 1" fix.
//!
//! The properly concurrent fix is to pin each `JsRuntime` to its own OS
//! thread (issue-19 "Option 2"); that's a larger refactor tracked separately.
use std::sync::OnceLock;
use tokio::sync::Mutex;
static V8_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
/// Returns the process-wide V8 serialization lock.
pub fn global() -> &'static Mutex<()> {
V8_LOCK.get_or_init(|| Mutex::new(()))
}