mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 08:00:45 +00:00
226 lines
9.2 KiB
Rust
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;
|
|
});
|
|
}
|
|
}
|