fix(modules): release async graphs independently

Execute ready async module scripts without waiting for earlier pending
modules. Keep ordered modules in sequence and leave parser-retained
terminals under parser ownership.

Cover successful and failed graphs, readiness before watching, and mixed
async/in-order delivery in the shared document scheduler.
This commit is contained in:
ldm0
2026-09-27 06:43:24 +08:00
parent 1650643271
commit 982bc94149
2 changed files with 211 additions and 7 deletions
@@ -550,6 +550,7 @@ pub(super) struct ParserModuleScriptRunner<
struct DocumentParserModulePendingScript<T> {
node_id: NodeId,
state: ParserPendingModuleScriptState<T>,
is_async: bool,
retained_by_parser_order: bool,
blocking_stylesheet_signatures:
HashSet<crate::stylesheet_blocking::DocumentBlockingStylesheetSignature>,
@@ -560,6 +561,7 @@ impl<T> DocumentParserModulePendingScript<T> {
Self {
node_id: script.node_id,
state: ParserPendingModuleScriptState::new(),
is_async: script.mode == crate::types::ScriptMode::Async,
retained_by_parser_order: false,
blocking_stylesheet_signatures: HashSet::new(),
}
@@ -641,7 +643,7 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
return ParserModulePendingScriptWatchResult::WaitingForTree;
}
let ready = self.take_ready_terminals_in_document_order();
let ready = self.take_ready_terminals(key);
if ready.is_empty() {
ParserModulePendingScriptWatchResult::WaitingForTree
} else {
@@ -659,7 +661,7 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
if self.is_retained_by_parser_order(key) {
return Some(Vec::new());
}
Some(self.take_ready_terminals_in_document_order())
Some(self.take_ready_terminals(key))
}
pub(super) fn notify_module_tree_load_failed(
@@ -672,7 +674,7 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
if self.is_retained_by_parser_order(key) {
return Some(Vec::new());
}
Some(self.take_ready_terminals_in_document_order())
Some(self.take_ready_terminals(key))
}
pub(super) fn blocking_stylesheet_signatures(
@@ -726,9 +728,21 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
Some(())
}
fn take_ready_terminals_in_document_order(
fn take_ready_terminals(
&mut self,
key: ParserPendingScriptKey,
) -> Vec<ParserModuleGraphTerminalWork<Target, ParserModuleGraphFailure>> {
// Async modules belong to the execute-as-soon-as-possible set, so only
// their own readiness controls delivery, regardless of document order.
if let Some(pending) = self.pending.get_mut(&key)
&& pending.is_async
{
let Some(terminal) = pending.state.take_ready_terminal() else {
return Vec::new();
};
self.remove_pending(key);
return vec![terminal];
}
let mut ready = Vec::new();
while let Some(key) = self.next_pending_key_in_document_order() {
let Some(pending) = self.pending.get(&key) else {
@@ -747,9 +761,9 @@ impl<Target, ParserModuleGraphFailure> ParserModuleScriptRunner<Target, ParserMo
}
fn next_pending_key_in_document_order(&self) -> Option<ParserPendingScriptKey> {
self.pending
.iter()
.find_map(|(key, pending)| (!pending.retained_by_parser_order).then_some(*key))
self.pending.iter().find_map(|(key, pending)| {
(!pending.retained_by_parser_order && !pending.is_async).then_some(*key)
})
}
fn remove_pending(
@@ -1187,6 +1187,196 @@ mod tests {
assert_eq!(parser_ready.script().node_id, first.node_id);
}
#[test]
fn async_module_readiness_bypasses_earlier_pending_modules() {
for earlier_mode in [ScriptMode::Async, ScriptMode::ModuleInOrder] {
for watch_before_ready in [false, true] {
let mut store: DocumentScriptSchedulerStore<u64, MainLikeModuleReadyTarget> =
DocumentScriptSchedulerStore::default();
let target = MainLikeModuleReadyTarget {
owner: 42,
route: 1,
};
let mut slow =
parser_module_script("https://document-scripts.test/slow.js", 10, 10);
slow.mode = earlier_mode;
let mut fast =
parser_module_script("https://document-scripts.test/fast.js", 20, 20);
fast.mode = ScriptMode::Async;
assert!(
store
.register_and_watch_module_script(target.owner, &slow)
.watched()
);
let fast_id = store.register_module_script(target.owner, &fast);
if watch_before_ready {
assert!(store.watch_module_script(fast_id).watched());
}
assert_eq!(
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
target.clone(),
fast.clone(),
vec![ModuleEntryId::from_raw(20)],
)),
watch_before_ready,
);
if !watch_before_ready {
assert!(!store.has_ready_work());
assert!(store.watch_module_script(fast_id).watched());
}
let ready = store
.take_next_ready_work()
.expect("ready async module must not wait for an earlier module")
.into_module_script_graph_ready();
assert_eq!(ready.script().node_id, fast.node_id);
assert_eq!(
store.pending_parser_module_script_count_for_test(target.owner),
1
);
assert!(!store.has_ready_work());
assert!(
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
target.clone(),
slow.clone(),
vec![ModuleEntryId::from_raw(10)],
))
);
let ready = store
.take_next_ready_work()
.expect("slow module completes later")
.into_module_script_graph_ready();
assert_eq!(ready.script().node_id, slow.node_id);
assert_eq!(
store.pending_parser_module_script_count_for_test(target.owner),
0
);
assert!(!store.has_ready_work());
}
}
}
#[test]
fn async_module_failure_bypasses_earlier_pending_modules() {
for watch_before_failure in [false, true] {
let mut store: DocumentScriptSchedulerStore<
u64,
std::convert::Infallible,
std::convert::Infallible,
TestModuleGraphFailure,
> = DocumentScriptSchedulerStore::default();
let owner = 42;
let mut slow = parser_module_script("https://document-scripts.test/slow.js", 10, 10);
slow.mode = ScriptMode::Async;
let mut failed =
parser_module_script("https://document-scripts.test/failed.js", 20, 20);
failed.mode = ScriptMode::Async;
assert!(
store
.register_and_watch_module_script(owner, &slow)
.watched()
);
let failed_id = store.register_module_script(owner, &failed);
if watch_before_failure {
assert!(store.watch_module_script(failed_id).watched());
}
assert_eq!(
store.notify_module_script_graph_failed_action(TestModuleGraphFailure::new(
owner,
&failed,
"fetch failed",
)),
watch_before_failure,
);
if !watch_before_failure {
assert!(!store.has_ready_work());
assert!(store.watch_module_script(failed_id).watched());
}
let failure = store
.take_next_ready_work()
.expect("async failure must not wait for an earlier module")
.into_module_script_graph_failed();
assert_eq!(failure.script_node_id(), failed.node_id);
assert_eq!(failure.message, "fetch failed");
assert_eq!(store.pending_parser_module_script_count_for_test(owner), 1);
assert!(!store.has_ready_work());
}
}
#[test]
fn ordered_modules_preserve_order_without_waiting_for_async_modules() {
let mut store: DocumentScriptSchedulerStore<u64, MainLikeModuleReadyTarget> =
DocumentScriptSchedulerStore::default();
let target = MainLikeModuleReadyTarget {
owner: 42,
route: 1,
};
let mut first = parser_module_script("https://document-scripts.test/first.js", 10, 10);
first.mode = ScriptMode::ModuleInOrder;
let mut middle = parser_module_script("https://document-scripts.test/async.js", 20, 20);
middle.mode = ScriptMode::Async;
let mut last = parser_module_script("https://document-scripts.test/last.js", 30, 30);
last.mode = ScriptMode::ModuleInOrder;
for script in [&first, &middle, &last] {
assert!(
store
.register_and_watch_module_script(target.owner, script)
.watched()
);
}
assert!(
!store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
target.clone(),
last.clone(),
vec![ModuleEntryId::from_raw(30)],
))
);
assert!(
!store.has_ready_work(),
"ordered modules must wait for earlier ordered modules"
);
assert!(
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
target.clone(),
first.clone(),
vec![ModuleEntryId::from_raw(10)],
))
);
for expected in [&first, &last] {
let ready = store
.take_next_ready_work()
.expect("pending async module must not block ready ordered modules")
.into_module_script_graph_ready();
assert_eq!(ready.script().node_id, expected.node_id);
}
assert_eq!(
store.pending_parser_module_script_count_for_test(target.owner),
1
);
assert!(!store.has_ready_work());
assert!(
store.notify_module_script_graph_ready_work(main_like_graph_ready_work(
target.clone(),
middle.clone(),
vec![ModuleEntryId::from_raw(20)],
))
);
let ready = store
.take_next_ready_work()
.expect("async module completes independently")
.into_module_script_graph_ready();
assert_eq!(ready.script().node_id, middle.node_id);
assert_eq!(
store.pending_parser_module_script_count_for_test(target.owner),
0
);
assert!(!store.has_ready_work());
}
#[test]
fn document_script_scheduler_store_routes_graph_failure_by_payload_owner() {
let mut store: DocumentScriptSchedulerStore<