Files
2026-08-11 00:10:12 +08:00

226 lines
9.2 KiB
Rust

//! Local executor ownership tracking for JavaScript-affine work.
//!
//! The types here identify which local executor lane currently owns execution
//! and provide guarded access paths for code that must stay on the JS owner
//! thread. The crate does not own a JS runtime; it only models executor-lane
//! affinity and routing.
mod domain;
mod executor;
mod tls;
pub use domain::{
JS_LOCAL_EXECUTOR_SCAFFOLD_LANE_ID, JsLocalExecutionDomain, JsLocalExecutorAccessContext,
JsLocalExecutorDomainRelation, JsOwnerLocalRuntimeAccessPath, JsOwnerLocalRuntimeEntryPath,
};
pub use executor::JsLocalExecutor;
pub use tls::{
current_js_local_execution_domain, current_js_local_executor_lane_id,
is_on_current_thread_outside_js_local_lane, is_on_js_local_executor,
is_on_named_owner_execution_lane_for, is_on_scoped_js_local_executor_lane,
is_on_script_execution_lane_for, scope_on_scaffold_js_local_executor,
};
// Run renderer-owned JS work inside a dedicated single-thread execution lane.
//
// This helper exists because "task-driven" does *not* mean "run page JS on
// background Tokio worker threads".
//
// The browser-shaped constraint is stricter than that:
// - preload / fetch / completion notification may happen off the page owner chain
// - but actual JS execution, DOM runtime mutation, V8 isolate access, and
// microtask checkpoints must stay on one page-owned execution lane
// - many of the values involved are better treated as `!Send`, even if the
// type system does not yet force every boundary
//
// The render_runtime uses `tokio::runtime::LocalRuntime` (via `build_local()`)
// which natively supports `spawn_local` without `LocalSet`. V8 foreground tasks
// are routed to this runtime via the custom V8 Platform implementation.
//
// `JsLocalExecutor` provides lane tracking and ownership:
// - `tokio::spawn` remains appropriate for background work such as network
// preload and fetch
// - `JsLocalExecutor::run(...)` is where renderer-owned JS work lives
// - later page/document task queues can schedule turns *onto* this local owner
// lane without pretending that JS itself became multi-threaded
//
// This helper is intentionally narrow:
// - it does *not* change visible script timing by itself
// - it does *not* implement browser task queues by itself
// - it does *not* make JS execution multi-threaded
// - it only makes the owner lane explicit so later task-driven work is built
// on a stable single-thread base instead of ordinary Tokio task interleaving
#[cfg(test)]
mod tests {
use crate::{
JS_LOCAL_EXECUTOR_SCAFFOLD_LANE_ID, JsLocalExecutionDomain, JsLocalExecutor,
JsLocalExecutorAccessContext, JsLocalExecutorDomainRelation,
current_js_local_execution_domain, current_js_local_executor_lane_id,
is_on_scoped_js_local_executor_lane, scope_on_scaffold_js_local_executor,
};
#[test]
fn current_domain_relation_distinguishes_matching_different_and_scaffold_lanes() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime should build");
let first_executor = JsLocalExecutor::new();
let second_executor = JsLocalExecutor::new();
runtime.block_on(async move {
assert_eq!(
first_executor.current_domain_relation(),
JsLocalExecutorDomainRelation::Outside
);
scope_on_scaffold_js_local_executor(async {
assert_eq!(
first_executor.current_domain_relation(),
JsLocalExecutorDomainRelation::ScaffoldLane
);
})
.await;
let first_executor_for_lane = first_executor.clone();
let second_executor_for_lane = second_executor.clone();
first_executor
.run(async move {
assert_eq!(
first_executor_for_lane.current_domain_relation(),
JsLocalExecutorDomainRelation::MatchingNamedLane
);
assert_eq!(
second_executor_for_lane.current_domain_relation(),
JsLocalExecutorDomainRelation::DifferentNamedLane
);
let first_executor_for_scaffold = first_executor_for_lane.clone();
scope_on_scaffold_js_local_executor(async move {
assert_eq!(
first_executor_for_scaffold.current_domain_relation(),
JsLocalExecutorDomainRelation::MatchingNamedLane
);
})
.await;
})
.await;
});
}
#[test]
fn current_access_context_distinguishes_current_thread_outside_lane() {
let current_thread_runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime should build");
let multi_thread_runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("multi-thread runtime should build");
let executor = JsLocalExecutor::new();
current_thread_runtime.block_on(async {
assert_eq!(
executor.current_access_context(),
JsLocalExecutorAccessContext::CurrentThreadOutsideLane
);
});
multi_thread_runtime.block_on(async {
assert_eq!(
executor.current_access_context(),
JsLocalExecutorAccessContext::Outside
);
});
}
#[test]
fn nested_run_on_different_executor_enters_different_lane() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime should build");
let first_executor = JsLocalExecutor::new();
let second_executor = JsLocalExecutor::new();
runtime.block_on(async move {
first_executor
.run(async move {
let first_lane = current_js_local_executor_lane_id()
.expect("first executor should bind a local lane");
second_executor
.run(async move {
let second_lane = current_js_local_executor_lane_id()
.expect("second executor should bind a local lane");
assert_ne!(first_lane, second_lane);
})
.await;
assert_eq!(current_js_local_executor_lane_id(), Some(first_lane));
})
.await;
});
}
#[test]
fn scope_binds_scaffold_lane_when_no_executor_lane_is_active() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime should build");
runtime.block_on(async move {
assert_eq!(current_js_local_executor_lane_id(), None);
scope_on_scaffold_js_local_executor(async move {
assert_eq!(
current_js_local_executor_lane_id(),
Some(JS_LOCAL_EXECUTOR_SCAFFOLD_LANE_ID)
);
assert_eq!(
current_js_local_execution_domain(),
JsLocalExecutionDomain::ScaffoldLane
);
assert!(is_on_scoped_js_local_executor_lane());
})
.await;
assert_eq!(current_js_local_executor_lane_id(), None);
assert_eq!(
current_js_local_execution_domain(),
JsLocalExecutionDomain::Outside
);
});
}
#[test]
fn scope_preserves_active_executor_lane() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime should build");
let executor = JsLocalExecutor::new();
runtime.block_on(async move {
executor
.run(async move {
let active_lane = current_js_local_executor_lane_id()
.expect("executor run should bind a lane");
assert_eq!(
current_js_local_execution_domain(),
JsLocalExecutionDomain::NamedLane(active_lane)
);
assert!(!is_on_scoped_js_local_executor_lane());
scope_on_scaffold_js_local_executor(async move {
assert_eq!(current_js_local_executor_lane_id(), Some(active_lane));
assert_eq!(
current_js_local_execution_domain(),
JsLocalExecutionDomain::NamedLane(active_lane)
);
assert!(!is_on_scoped_js_local_executor_lane());
})
.await;
assert_eq!(current_js_local_executor_lane_id(), Some(active_lane));
})
.await;
});
}
}