mirror of
https://github.com/lexmount/moli.git
synced 2026-10-02 00:00:49 +00:00
fix(lifecycle): order defer timers before DOMContentLoaded
This commit is contained in:
@@ -6990,6 +6990,7 @@ html/syntax/parsing-html-fragments/the-input-byte-stream-034.html
|
||||
html/syntax/parsing-html-fragments/the-input-byte-stream-037.html
|
||||
html/syntax/parsing-html-fragments/the-input-byte-stream-038.html
|
||||
html/syntax/parsing-html-fragments/tokenizer-modes-001.html
|
||||
html/syntax/parsing/DOMContentLoaded-defer.html
|
||||
html/syntax/parsing/Document.getElementsByTagName-foreign-01.html
|
||||
html/syntax/parsing/Document.getElementsByTagName-foreign-02.html
|
||||
html/syntax/parsing/Element.getElementsByTagName-foreign-01.html
|
||||
|
||||
@@ -77,6 +77,7 @@ pub(crate) use devtools_mutations::{
|
||||
};
|
||||
pub(crate) use inspector_issues::PendingInspectorIssue;
|
||||
pub(crate) use meta_refresh::MetaRefreshNavigation;
|
||||
use moli_time::{TimerScheduleRange, TimerScheduleSnapshot};
|
||||
use parser_blocking::{ParserInsertionWork, PendingParserBlockingWork, PendingParserInsertion};
|
||||
pub(crate) use parser_modulepreload::MainDocumentModulepreloadFetchOutcome;
|
||||
pub(crate) use script_lifecycle::{
|
||||
@@ -757,6 +758,8 @@ pub(super) struct DocumentRuntime {
|
||||
script_lifecycle: DocumentScriptLifecycle,
|
||||
parser_script_start_positions: HashMap<DomHandle, ParserScriptStartPosition>,
|
||||
timeouts: HostTimeoutScheduler,
|
||||
classic_defer_timer_schedule_start: Option<TimerScheduleSnapshot>,
|
||||
classic_defer_timer_schedule_ranges: Vec<TimerScheduleRange>,
|
||||
events: HostEventTargetRegistry,
|
||||
mutations: MutationCoordinator,
|
||||
meta_refresh_scheduler: meta_refresh::MetaRefreshScheduler,
|
||||
|
||||
@@ -276,6 +276,51 @@ impl DocumentRuntime {
|
||||
self.timeouts.run_next_body(scope, selection)
|
||||
}
|
||||
|
||||
/// Starts the exact timer sequence range owned by one classic defer task.
|
||||
///
|
||||
/// Parser-time scripts still execute inside the parser's task and module
|
||||
/// evaluation has its own pending-completion semantics. Neither category
|
||||
/// should make all of its older timers precede DOMContentLoaded. A classic
|
||||
/// defer task, however, can queue a timer before the subsequent DCL task is
|
||||
/// queued; record only that task-local interval.
|
||||
pub(crate) fn begin_classic_defer_timer_schedule_range(&mut self) {
|
||||
debug_assert!(
|
||||
self.classic_defer_timer_schedule_start.is_none(),
|
||||
"classic defer timer range must not overlap another script task"
|
||||
);
|
||||
self.classic_defer_timer_schedule_start = Some(self.timeouts.schedule_snapshot());
|
||||
}
|
||||
|
||||
pub(crate) fn finish_classic_defer_timer_schedule_range(&mut self) {
|
||||
let Some(start) = self.classic_defer_timer_schedule_start.take() else {
|
||||
// A defer script may replace the document and clear the old
|
||||
// lifecycle state while it is executing.
|
||||
return;
|
||||
};
|
||||
let range = self.timeouts.schedule_range_since(start);
|
||||
if !range.is_empty() {
|
||||
self.classic_defer_timer_schedule_ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_classic_defer_timer_schedule_ranges(&mut self) {
|
||||
self.classic_defer_timer_schedule_start = None;
|
||||
self.classic_defer_timer_schedule_ranges.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn run_next_timeout_queued_by_classic_defer_script(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> HostTimeoutRunResult {
|
||||
self.timeouts
|
||||
.run_next_from_schedule_ranges(scope, &self.classic_defer_timer_schedule_ranges)
|
||||
}
|
||||
|
||||
pub(crate) fn has_ready_timeout_queued_by_classic_defer_script(&self) -> bool {
|
||||
self.timeouts
|
||||
.has_ready_from_schedule_ranges(&self.classic_defer_timer_schedule_ranges)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_ready_timeout(&self) -> bool {
|
||||
self.timeouts.has_ready_timer()
|
||||
|
||||
@@ -286,6 +286,7 @@ impl DocumentRuntime {
|
||||
self.document_write_script_preload_scanner = None;
|
||||
self.main_document_script_preloads = Default::default();
|
||||
self.document_write_script_preloads.clear();
|
||||
self.clear_classic_defer_timer_schedule_ranges();
|
||||
self.pending_parser_blocking_work = None;
|
||||
self.root_document_parser = None;
|
||||
self.delivered_meta_content_security_policies
|
||||
@@ -381,6 +382,7 @@ impl DocumentRuntime {
|
||||
|
||||
pub(crate) fn note_dom_content_loaded_dispatched(&mut self) {
|
||||
self.dom_content_loaded_dispatched = true;
|
||||
self.clear_classic_defer_timer_schedule_ranges();
|
||||
}
|
||||
|
||||
pub(crate) fn dom_content_loaded_dispatched(&self) -> bool {
|
||||
|
||||
@@ -111,6 +111,8 @@ impl DocumentRuntime {
|
||||
),
|
||||
parser_script_start_positions: HashMap::new(),
|
||||
timeouts: HostTimeoutScheduler::default(),
|
||||
classic_defer_timer_schedule_start: None,
|
||||
classic_defer_timer_schedule_ranges: Vec::new(),
|
||||
events: HostEventTargetRegistry::default(),
|
||||
mutations: MutationCoordinator,
|
||||
meta_refresh_scheduler: super::meta_refresh::MetaRefreshScheduler::default(),
|
||||
@@ -232,6 +234,8 @@ impl DocumentRuntime {
|
||||
script_lifecycle: _,
|
||||
parser_script_start_positions: _,
|
||||
timeouts: _,
|
||||
classic_defer_timer_schedule_start: _,
|
||||
classic_defer_timer_schedule_ranges: _,
|
||||
events: _,
|
||||
mutations: _,
|
||||
meta_refresh_scheduler: _,
|
||||
|
||||
@@ -26,6 +26,10 @@ impl DocumentRuntime {
|
||||
.iter()
|
||||
.position(PostParsePageOwnedWork::starts_after_domcontentloaded_boundary)
|
||||
.unwrap_or(work.len());
|
||||
// This is an owner-queue boundary marker, not yet the HTML event-loop
|
||||
// queue point for DOMContentLoaded. When it reaches the front after all
|
||||
// defer-like and parser-owned pre-DCL work has drained, the driver first
|
||||
// runs ready timer tasks recorded during classic defer execution.
|
||||
work.insert(
|
||||
domcontentloaded_index,
|
||||
PostParsePageOwnedWork::main_document_domcontentloaded(owner),
|
||||
|
||||
@@ -36,6 +36,10 @@ pub(crate) trait PageOwnedDocumentScriptHooks {
|
||||
|
||||
fn perform_pre_script_checkpoint(&mut self, script_url: &Url) -> Result<()>;
|
||||
|
||||
fn begin_classic_defer_timer_schedule_range(&mut self) {}
|
||||
|
||||
fn finish_classic_defer_timer_schedule_range(&mut self) {}
|
||||
|
||||
fn execute_prepared_script<'a>(
|
||||
&'a mut self,
|
||||
script: PreparedScript,
|
||||
|
||||
@@ -238,10 +238,18 @@ where
|
||||
let dynamic_script_owner_id = runtime_script_claim
|
||||
.as_ref()
|
||||
.map(DynamicScriptPageTaskClaim::id);
|
||||
let records_classic_defer_timer_range =
|
||||
matches!(lane, DocumentScriptExecutionLane::ClassicDefer);
|
||||
if records_classic_defer_timer_range {
|
||||
self.hooks.begin_classic_defer_timer_schedule_range();
|
||||
}
|
||||
let execution = self
|
||||
.hooks
|
||||
.execute_prepared_script(*script, runtime_script_claim)
|
||||
.await;
|
||||
if records_classic_defer_timer_range {
|
||||
self.hooks.finish_classic_defer_timer_schedule_range();
|
||||
}
|
||||
let document_owner_token_after_body = self.hooks.current_document_owner_token();
|
||||
let script_elapsed_ms = script_started.elapsed().as_millis();
|
||||
Ok(PageOwnedDocumentScriptResult {
|
||||
|
||||
@@ -22,7 +22,9 @@ use crate::{
|
||||
get_private_value, script_nonce_from_host_defined_options,
|
||||
},
|
||||
};
|
||||
use moli_time::{TimerId, TimerReadyAllowance, TimerScheduler};
|
||||
use moli_time::{
|
||||
TimerId, TimerReadyAllowance, TimerScheduleRange, TimerScheduleSnapshot, TimerScheduler,
|
||||
};
|
||||
use moli_webapi_declare::WebApiObject;
|
||||
|
||||
#[derive(WebApiObject)]
|
||||
@@ -747,12 +749,43 @@ impl HostTimeoutScheduler {
|
||||
self.run_timer(scope, timer)
|
||||
}
|
||||
|
||||
pub(crate) fn run_next_from_schedule_ranges(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
ranges: &[TimerScheduleRange],
|
||||
) -> HostTimeoutRunResult {
|
||||
let Some(timer) = self.scheduler.take_next_ready_from_schedule_ranges(
|
||||
ranges,
|
||||
Instant::now(),
|
||||
min_delay_ready_allowance(),
|
||||
) else {
|
||||
return HostTimeoutRunResult::Idle;
|
||||
};
|
||||
self.run_timer(scope, timer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn has_ready_timer(&self) -> bool {
|
||||
self.scheduler
|
||||
.has_ready_timer(Instant::now(), min_delay_ready_allowance())
|
||||
}
|
||||
|
||||
pub(crate) fn has_ready_from_schedule_ranges(&self, ranges: &[TimerScheduleRange]) -> bool {
|
||||
self.scheduler.has_ready_from_schedule_ranges(
|
||||
ranges,
|
||||
Instant::now(),
|
||||
min_delay_ready_allowance(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn schedule_snapshot(&self) -> TimerScheduleSnapshot {
|
||||
self.scheduler.schedule_snapshot()
|
||||
}
|
||||
|
||||
pub(crate) fn schedule_range_since(&self, start: TimerScheduleSnapshot) -> TimerScheduleRange {
|
||||
self.scheduler.schedule_range_since(start)
|
||||
}
|
||||
|
||||
pub(crate) fn next_ready_timer_deadline(
|
||||
&self,
|
||||
selection: RendererPageTimerSelection,
|
||||
|
||||
@@ -162,6 +162,9 @@ pub(crate) enum PageTask {
|
||||
// into "script loop followed by immediate host callback".
|
||||
// - Keeping it in the queue means the owner lane sees one explicit task:
|
||||
// pre-task checkpoint -> dispatch DOMContentLoaded -> post-task effects.
|
||||
// - Its initial placement is a lifecycle sentinel. Reaching the queue
|
||||
// front after defer-like work establishes the actual DCL task queue
|
||||
// point; ready timers recorded during classic defer tasks run first.
|
||||
//
|
||||
DispatchDomContentLoaded,
|
||||
DispatchConnectedStyleLoad(ReadyConnectedStyleLoad),
|
||||
|
||||
@@ -412,6 +412,17 @@ impl PageVm {
|
||||
)
|
||||
.await
|
||||
}
|
||||
PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
|
||||
self.run_classic_defer_timer_before_domcontentloaded(&request_client)
|
||||
.await?;
|
||||
self.outcome_after_exact_post_parse_action(
|
||||
pending_document_lifecycle_turn,
|
||||
document,
|
||||
stage,
|
||||
DocumentLifecycleTurnAction::Progressed,
|
||||
false,
|
||||
)
|
||||
}
|
||||
PostParseLifecycleAdvance::NeedsContinuation => self
|
||||
.outcome_after_exact_post_parse_action(
|
||||
pending_document_lifecycle_turn,
|
||||
|
||||
@@ -3204,6 +3204,12 @@ impl PageVm {
|
||||
*task,
|
||||
))))
|
||||
}
|
||||
PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
|
||||
let request_client = self.request_client.clone();
|
||||
self.run_classic_defer_timer_before_domcontentloaded(&request_client)
|
||||
.await?;
|
||||
Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)))
|
||||
}
|
||||
PostParseLifecycleAdvance::NeedsContinuation => {
|
||||
Ok(PostParseLifecycleLoopAdvance::Continue(Box::new(None)))
|
||||
}
|
||||
|
||||
@@ -71,6 +71,20 @@ impl PageOwnedDocumentScriptHooks for MainPageOwnedDocumentScriptHooks<'_, '_> {
|
||||
.perform_script_task_checkpoint(Some(script_url))
|
||||
}
|
||||
|
||||
fn begin_classic_defer_timer_schedule_range(&mut self) {
|
||||
self.page_vm
|
||||
.vm_mut()
|
||||
.document_runtime
|
||||
.begin_classic_defer_timer_schedule_range();
|
||||
}
|
||||
|
||||
fn finish_classic_defer_timer_schedule_range(&mut self) {
|
||||
self.page_vm
|
||||
.vm_mut()
|
||||
.document_runtime
|
||||
.finish_classic_defer_timer_schedule_range();
|
||||
}
|
||||
|
||||
fn execute_prepared_script<'a>(
|
||||
&'a mut self,
|
||||
script: PreparedScript,
|
||||
|
||||
@@ -24,6 +24,21 @@ pub(in crate::runtime) enum PageTimerTurnAction {
|
||||
}
|
||||
|
||||
impl PageVm {
|
||||
/// Run the exact ready timer selected by the post-parse lifecycle's
|
||||
/// classic-defer schedule ranges, then commit the ordinary callback task
|
||||
/// completion once.
|
||||
pub(in crate::runtime) async fn run_classic_defer_timer_before_domcontentloaded(
|
||||
&mut self,
|
||||
loader: &crate::network::ResourceRequestClient,
|
||||
) -> Result<()> {
|
||||
let body = self.vm_mut().run_next_classic_defer_timer_callback_body()?;
|
||||
ensure!(
|
||||
body.consumed_heap_head(),
|
||||
"a ready classic-defer timer selected before DOMContentLoaded must consume its heap entry"
|
||||
);
|
||||
self.finish_selected_page_callback_task(loader).await
|
||||
}
|
||||
|
||||
/// Execute one already-selected timer body without committing its
|
||||
/// task-end checkpoint.
|
||||
///
|
||||
|
||||
@@ -170,6 +170,16 @@ impl ParserCompletion {
|
||||
// terminal reactions first; the lifecycle coordinator then owns DCL's
|
||||
// separate task-end checkpoint.
|
||||
Self::finish_task(page_vm, task_effect)?;
|
||||
let request_client = page_vm.request_client.clone();
|
||||
while page_vm
|
||||
.vm()
|
||||
.document_runtime
|
||||
.has_ready_timeout_queued_by_classic_defer_script()
|
||||
{
|
||||
page_vm
|
||||
.run_classic_defer_timer_before_domcontentloaded(&request_client)
|
||||
.await?;
|
||||
}
|
||||
let run = super::main_document_lifecycle_completion::execute_parser_exact_domcontentloaded_on_owner_local_task(
|
||||
page_vm,
|
||||
successor_owner,
|
||||
@@ -408,6 +418,14 @@ impl PageVm {
|
||||
};
|
||||
if let Some(dcl_task) = claimed_dcl {
|
||||
ParserCompletion::finish_task_with_replacement_admission(self, task_effect)?;
|
||||
while self
|
||||
.vm()
|
||||
.document_runtime
|
||||
.has_ready_timeout_queued_by_classic_defer_script()
|
||||
{
|
||||
self.run_classic_defer_timer_before_domcontentloaded(loader)
|
||||
.await?;
|
||||
}
|
||||
let replacement_lifecycle_snapshot =
|
||||
self.document_replacement_lifecycle_action_snapshot();
|
||||
let execution = self
|
||||
|
||||
@@ -304,6 +304,11 @@ impl ParserClassicDocumentScriptExecutionHooks
|
||||
Box::pin(async move {
|
||||
owner.record_network_result(&action.script, action.source_network_result.as_ref());
|
||||
let expected_owner = action.owner;
|
||||
owner
|
||||
.page_vm
|
||||
.vm_mut()
|
||||
.document_runtime
|
||||
.begin_classic_defer_timer_schedule_range();
|
||||
let execution = owner
|
||||
.page_vm
|
||||
.execute_main_parser_deferred_classic_script_body_on_current_lane(
|
||||
@@ -311,6 +316,11 @@ impl ParserClassicDocumentScriptExecutionHooks
|
||||
action.script,
|
||||
)
|
||||
.await;
|
||||
owner
|
||||
.page_vm
|
||||
.vm_mut()
|
||||
.document_runtime
|
||||
.finish_classic_defer_timer_schedule_range();
|
||||
let (run, navigation_triggered, completion, prepared_activity) = execution.into_parts();
|
||||
let owner_replaced = !owner.owner_is_current(expected_owner);
|
||||
Ok(ParserClassicDocumentScriptExecutionResult::new(
|
||||
|
||||
@@ -1135,3 +1135,74 @@ async fn main_parser_after_parsing_queue_orders_module_before_ready_classic_defe
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn main_parser_classic_defer_records_its_timer_range_for_domcontentloaded() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let mut page_vm = test_page_vm_with_loader_and_document_url(
|
||||
&loader,
|
||||
Vec::new(),
|
||||
Url::parse("https://example.com/defer-timer.html").expect("document URL"),
|
||||
);
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.eval(
|
||||
"globalThis.__mainParserDeferTimerEvents = []; \
|
||||
setTimeout(() => __mainParserDeferTimerEvents.push('outside'), 0);",
|
||||
)
|
||||
.expect("defer timer state should initialize");
|
||||
let script = append_parser_owned_external_classic_defer_for_page_vm_test(
|
||||
&mut page_vm,
|
||||
1,
|
||||
"timer-defer",
|
||||
Url::parse("https://example.com/timer-defer.js").expect("script URL"),
|
||||
ScriptSource::Loaded(
|
||||
"setTimeout(() => __mainParserDeferTimerEvents.push('defer'), 0);".to_owned(),
|
||||
),
|
||||
("onload", ""),
|
||||
);
|
||||
let task_owner = page_vm
|
||||
.vm()
|
||||
.current_main_document_task_owner()
|
||||
.expect("defer timer test requires a document owner");
|
||||
assert!(
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.claim_main_parser_deferred_script(
|
||||
task_owner,
|
||||
script,
|
||||
None,
|
||||
None,
|
||||
Default::default(),
|
||||
)
|
||||
.expect("loaded classic defer should be accepted")
|
||||
);
|
||||
page_vm
|
||||
.seal_main_parser_deferred_scripts(task_owner)
|
||||
.expect("defer timer queue should seal");
|
||||
|
||||
run_ready_parser_deferred_body_for_test(&mut page_vm, &loader, "timer classic defer").await;
|
||||
assert!(
|
||||
page_vm
|
||||
.vm()
|
||||
.document_runtime
|
||||
.has_ready_timeout_queued_by_classic_defer_script(),
|
||||
"the specialized main-parser defer executor must retain its task-local timer range"
|
||||
);
|
||||
page_vm
|
||||
.run_classic_defer_timer_before_domcontentloaded(&loader)
|
||||
.await
|
||||
.expect("the recorded defer timer should be selected");
|
||||
assert_eq!(
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.eval("__mainParserDeferTimerEvents.join('|')")
|
||||
.expect("defer timer events should evaluate"),
|
||||
"defer",
|
||||
"the older unrelated timer must not enter the pre-DOMContentLoaded range"
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -4121,6 +4121,139 @@ fn drive_post_parse_lifecycle_loop_returns_at_domcontentloaded_before_trailing_t
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defer_queued_timer_runs_before_domcontentloaded_without_draining_nested_timer() {
|
||||
run_page_vm_local_runtime_async_test("page-vm-defer-timer-before-dcl", || async move {
|
||||
let mut page_vm = test_page_vm();
|
||||
let local_executor = page_vm.local_executor.clone();
|
||||
|
||||
let (completion, before_later_timers, after_outside_timer, after_nested_timer) =
|
||||
local_executor
|
||||
.run(async move {
|
||||
page_vm.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.__deferDclTaskOrder = [];
|
||||
setTimeout(() => {
|
||||
globalThis.__deferDclTaskOrder.push("timer-outside-defer");
|
||||
}, 0);
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
globalThis.__deferDclTaskOrder.push("dcl");
|
||||
});
|
||||
"installed";
|
||||
"#,
|
||||
)?;
|
||||
let body = page_vm
|
||||
.vm()
|
||||
.document_runtime
|
||||
.snapshot_document()
|
||||
.document_body_handle()
|
||||
.expect("test document should have a body");
|
||||
let defer_script_node = page_vm
|
||||
.vm_mut()
|
||||
.document_runtime
|
||||
.dom_host_mut()
|
||||
.create_parser_element_without_attributes(
|
||||
"script".to_owned(),
|
||||
"http://www.w3.org/1999/xhtml".to_owned(),
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
page_vm
|
||||
.vm_mut()
|
||||
.document_runtime
|
||||
.dom_host_mut()
|
||||
.append_child(body, defer_script_node),
|
||||
"defer script test node should attach to the document"
|
||||
);
|
||||
let mut defer_script = prepared_loaded_classic_for_page_vm_test(
|
||||
&page_vm,
|
||||
9100,
|
||||
r#"
|
||||
globalThis.__deferDclTaskOrder.push("defer");
|
||||
setTimeout(() => {
|
||||
globalThis.__deferDclTaskOrder.push("timer-before-dcl");
|
||||
setTimeout(() => {
|
||||
globalThis.__deferDclTaskOrder.push("timer-after-dcl");
|
||||
}, 0);
|
||||
}, 0);
|
||||
"#,
|
||||
);
|
||||
defer_script.mode = ScriptMode::Defer;
|
||||
defer_script.source_kind = ScriptSourceKind::External;
|
||||
defer_script.node_id = defer_script_node;
|
||||
|
||||
let lifecycle_driver = {
|
||||
let PageVm {
|
||||
vm,
|
||||
page_task_queue,
|
||||
report,
|
||||
..
|
||||
} = &mut page_vm;
|
||||
vm.as_mut()
|
||||
.expect("page vm must retain a live ScriptVm until drop")
|
||||
.start_post_parse_lifecycle_round(
|
||||
PageVmInitStage::DomContentLoaded,
|
||||
page_task_queue,
|
||||
report,
|
||||
vec![classic_defer_work(defer_script)],
|
||||
)
|
||||
.await
|
||||
};
|
||||
let completion = page_vm
|
||||
.drive_post_parse_lifecycle_loop_on_named_owner_lane(
|
||||
PageVmInitStage::DomContentLoaded,
|
||||
lifecycle_driver,
|
||||
)
|
||||
.await?;
|
||||
let before_later_timers = page_vm
|
||||
.vm_mut()
|
||||
.eval("JSON.stringify(globalThis.__deferDclTaskOrder)")?;
|
||||
|
||||
let loader = page_vm.request_client.clone();
|
||||
assert!(
|
||||
page_vm
|
||||
.run_one_due_timer_selected_task_for_test(&loader)
|
||||
.await?,
|
||||
"the timer queued outside defer must remain runnable after DCL"
|
||||
);
|
||||
let after_outside_timer = page_vm
|
||||
.vm_mut()
|
||||
.eval("JSON.stringify(globalThis.__deferDclTaskOrder)")?;
|
||||
assert!(
|
||||
page_vm
|
||||
.run_one_due_timer_selected_task_for_test(&loader)
|
||||
.await?,
|
||||
"the timer queued by the first timer must remain runnable after DCL"
|
||||
);
|
||||
let after_nested_timer = page_vm
|
||||
.vm_mut()
|
||||
.eval("JSON.stringify(globalThis.__deferDclTaskOrder)")?;
|
||||
Ok::<_, anyhow::Error>((
|
||||
completion,
|
||||
before_later_timers,
|
||||
after_outside_timer,
|
||||
after_nested_timer,
|
||||
))
|
||||
})
|
||||
.await
|
||||
.expect("driver loop should preserve the DCL task boundary");
|
||||
|
||||
assert!(matches!(
|
||||
completion,
|
||||
PostParseLifecycleCompletionAction::ReturnAtStage("DOMContentLoaded")
|
||||
));
|
||||
assert_eq!(before_later_timers, r#"["defer","timer-before-dcl","dcl"]"#);
|
||||
assert_eq!(
|
||||
after_outside_timer,
|
||||
r#"["defer","timer-before-dcl","dcl","timer-outside-defer"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
after_nested_timer,
|
||||
r#"["defer","timer-before-dcl","dcl","timer-outside-defer","timer-after-dcl"]"#
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drive_post_parse_lifecycle_loop_returns_at_domcontentloaded_before_listener_work() {
|
||||
run_page_vm_local_runtime_async_test("page-vm-dcl-before-listener-work", || async move {
|
||||
|
||||
@@ -6002,6 +6002,18 @@ impl ScriptVm {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Execute one ready timer whose scheduling sequence belongs to a classic
|
||||
/// defer script, without committing its task-end callback completion.
|
||||
pub(crate) fn run_next_classic_defer_timer_callback_body(
|
||||
&mut self,
|
||||
) -> Result<HostTimeoutRunResult> {
|
||||
let result = self.run_next_timeout_queued_by_classic_defer_script_body()?;
|
||||
if let HostTimeoutRunResult::CallbackError(error) = &result {
|
||||
self.record_runtime_warning(format_args!("timer callback dispatch failed: {error}"));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Complete one timer in a standalone ScriptVm fixture.
|
||||
///
|
||||
/// Production and PageVm behavior tests must use the selected Page-task
|
||||
|
||||
@@ -819,6 +819,30 @@ impl ScriptVm {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Execute one ready timer selected from the closed schedule ranges
|
||||
/// recorded around classic defer script execution.
|
||||
///
|
||||
/// Like the ordinary timer body, this does not checkpoint or reconcile
|
||||
/// callback effects. The PageVm lifecycle owner commits the exact selected
|
||||
/// callback completion after this method returns.
|
||||
pub(super) fn run_next_timeout_queued_by_classic_defer_script_body(
|
||||
&mut self,
|
||||
) -> Result<HostTimeoutRunResult> {
|
||||
#[cfg(test)]
|
||||
if let Some(message) = self.test_next_timeout_failure.take() {
|
||||
return Err(anyhow!("{message}"));
|
||||
}
|
||||
|
||||
let document_runtime: *mut DocumentRuntime = &mut *self.document_runtime;
|
||||
self.with_default_context_scope(|scope, _host_ptr| {
|
||||
// SAFETY: this is the same synchronous, non-escaping
|
||||
// document-runtime borrow used by `run_next_timeout_body`; the
|
||||
// schedule ranges only narrow which timer may be selected.
|
||||
Ok(unsafe { &mut *document_runtime }
|
||||
.run_next_timeout_queued_by_classic_defer_script(scope))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn fail_next_timeout_for_testing(&mut self, message: impl Into<String>) {
|
||||
self.test_next_timeout_failure = Some(message.into());
|
||||
|
||||
@@ -961,6 +961,10 @@ impl ScriptVm {
|
||||
report: &mut ScriptExecutionReport,
|
||||
work: Vec<PostParsePageOwnedWork>,
|
||||
) -> PostParseLifecycleRound {
|
||||
if !self.document_runtime.dom_content_loaded_dispatched() {
|
||||
self.document_runtime
|
||||
.clear_classic_defer_timer_schedule_ranges();
|
||||
}
|
||||
let queue_stats = post_parse_lifecycle_queue_stats(&work);
|
||||
self.queue_initial_connected_style_loads_for_current_owner();
|
||||
self.document_runtime.drain_document_processing_wakes();
|
||||
@@ -1836,6 +1840,21 @@ impl ScriptVm {
|
||||
reached_boundary,
|
||||
invalidation_policy,
|
||||
} = PostParseProcessingAction::from_document_processing_action(action);
|
||||
if work.is_domcontentloaded_task()
|
||||
&& self
|
||||
.document_runtime
|
||||
.has_ready_timeout_queued_by_classic_defer_script()
|
||||
{
|
||||
// The lifecycle plan pre-seeds this boundary marker so owner work
|
||||
// stays ordered. It has now reached the front after defer-like and
|
||||
// parser-owned pre-DCL work, which is the point at which HTML queues
|
||||
// the actual DOMContentLoaded global task. Give only ready timers
|
||||
// scheduled by classic defer scripts their intervening task turn.
|
||||
page_task_queue.enqueue_front_post_parse_work_preserving_order(vec![work]);
|
||||
return PostParseDriverStep::Ready(Box::new(
|
||||
ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded,
|
||||
));
|
||||
}
|
||||
if !(work.is_domcontentloaded_task() || work.is_window_load_task()) {
|
||||
return PostParseDriverStep::Ready(Box::new(ReadyPostParseAction::Processing(
|
||||
Box::new(PostParseProcessingAction {
|
||||
@@ -1958,6 +1977,11 @@ impl ScriptVm {
|
||||
ReadyPostParseAction::Processing(action) => {
|
||||
return Ok(PostParseProcessingStep::Action(action));
|
||||
}
|
||||
ReadyPostParseAction::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
|
||||
return Ok(
|
||||
PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded,
|
||||
);
|
||||
}
|
||||
},
|
||||
PostParseDriverStep::NeedsContinuation => {
|
||||
return Ok(PostParseProcessingStep::NeedsContinuation);
|
||||
@@ -1996,6 +2020,11 @@ impl ScriptVm {
|
||||
execution.into_page_owned_task(),
|
||||
)));
|
||||
}
|
||||
PostParseProcessingStep::TimerQueuedByClassicDeferBeforeDomContentLoaded => {
|
||||
return Ok(
|
||||
PostParseLifecycleAdvance::TimerQueuedByClassicDeferBeforeDomContentLoaded,
|
||||
);
|
||||
}
|
||||
PostParseProcessingStep::NeedsContinuation => {
|
||||
return Ok(PostParseLifecycleAdvance::NeedsContinuation);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) enum PostParseDrainResult {
|
||||
|
||||
pub(crate) enum PostParseLifecycleAdvance {
|
||||
PageOwnedTask(Box<PostParsePageOwnedTask>),
|
||||
TimerQueuedByClassicDeferBeforeDomContentLoaded,
|
||||
NeedsContinuation,
|
||||
AwaitProgress,
|
||||
Complete(PostParseLifecycleCompletionAction),
|
||||
@@ -322,6 +323,7 @@ impl PostParseProcessingAction {
|
||||
|
||||
pub(crate) enum ReadyPostParseAction {
|
||||
Processing(Box<PostParseProcessingAction>),
|
||||
TimerQueuedByClassicDeferBeforeDomContentLoaded,
|
||||
}
|
||||
|
||||
pub(crate) enum PostParseRuntimeDriverStep {
|
||||
@@ -338,6 +340,7 @@ pub(crate) enum PostParseDriverStep {
|
||||
|
||||
pub(crate) enum PostParseProcessingStep {
|
||||
Action(Box<PostParseProcessingAction>),
|
||||
TimerQueuedByClassicDeferBeforeDomContentLoaded,
|
||||
NeedsContinuation,
|
||||
AwaitProgress,
|
||||
Idle,
|
||||
|
||||
@@ -6,7 +6,11 @@ use std::{
|
||||
};
|
||||
|
||||
mod timers;
|
||||
pub use timers::{ReadyTimer, TimerId, TimerReadyAllowance, TimerScheduler};
|
||||
|
||||
pub use timers::{
|
||||
ReadyTimer, TimerId, TimerReadyAllowance, TimerScheduleRange, TimerScheduleSnapshot,
|
||||
TimerScheduler,
|
||||
};
|
||||
|
||||
pub fn unix_epoch_millis() -> f64 {
|
||||
SystemTime::now()
|
||||
|
||||
+191
-2
@@ -23,6 +23,31 @@ pub struct TimerReadyAllowance {
|
||||
pub allowance: Duration,
|
||||
}
|
||||
|
||||
/// A scheduling-sequence boundary from which an exact timer range can be
|
||||
/// recorded.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TimerScheduleSnapshot {
|
||||
next_sequence: u64,
|
||||
}
|
||||
|
||||
/// A half-open scheduling-sequence range for timers queued during one owner
|
||||
/// operation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct TimerScheduleRange {
|
||||
inclusive_sequence: u64,
|
||||
exclusive_sequence: u64,
|
||||
}
|
||||
|
||||
impl TimerScheduleRange {
|
||||
pub fn is_empty(self) -> bool {
|
||||
self.inclusive_sequence == self.exclusive_sequence
|
||||
}
|
||||
|
||||
fn contains(self, sequence: u64) -> bool {
|
||||
self.inclusive_sequence <= sequence && sequence < self.exclusive_sequence
|
||||
}
|
||||
}
|
||||
|
||||
impl TimerReadyAllowance {
|
||||
pub const NONE: Self = Self {
|
||||
max_delay_ms: 0,
|
||||
@@ -169,12 +194,71 @@ impl<T> TimerScheduler<T> {
|
||||
&mut self,
|
||||
now: Instant,
|
||||
allowance: TimerReadyAllowance,
|
||||
predicate: F,
|
||||
mut predicate: F,
|
||||
) -> Option<ReadyTimer<T>>
|
||||
where
|
||||
F: FnMut(&T) -> bool,
|
||||
{
|
||||
let selected = self.next_ready_matching_timer(now, allowance, predicate)?;
|
||||
self.take_next_ready_matching_scheduled(
|
||||
now,
|
||||
allowance,
|
||||
|timer| predicate(&timer.payload),
|
||||
|_| true,
|
||||
)
|
||||
}
|
||||
|
||||
/// Takes the next ready timer scheduled inside one of `ranges`.
|
||||
///
|
||||
/// Timers scheduled by a callback that runs while draining the ranges are
|
||||
/// outside those closed ranges and remain pending for a later task turn.
|
||||
pub fn take_next_ready_from_schedule_ranges(
|
||||
&mut self,
|
||||
ranges: &[TimerScheduleRange],
|
||||
now: Instant,
|
||||
allowance: TimerReadyAllowance,
|
||||
) -> Option<ReadyTimer<T>> {
|
||||
self.take_next_ready_matching_scheduled(
|
||||
now,
|
||||
allowance,
|
||||
|timer| schedule_ranges_contain(ranges, timer.sequence),
|
||||
|timer| schedule_ranges_contain(ranges, timer.sequence),
|
||||
)
|
||||
}
|
||||
|
||||
fn take_next_ready_matching_scheduled<P, B>(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
allowance: TimerReadyAllowance,
|
||||
mut predicate: P,
|
||||
mut blocks_if_not_ready: B,
|
||||
) -> Option<ReadyTimer<T>>
|
||||
where
|
||||
P: FnMut(&ScheduledTimer<T>) -> bool,
|
||||
B: FnMut(&ScheduledTimer<T>) -> bool,
|
||||
{
|
||||
let mut first_non_ready = None;
|
||||
let mut selected = None;
|
||||
for timer in &self.pending {
|
||||
if !self.active.contains(&timer.id) {
|
||||
continue;
|
||||
}
|
||||
if !timer_ready(timer.run_at, timer.delay_ms, now, allowance) {
|
||||
if blocks_if_not_ready(timer)
|
||||
&& first_non_ready.is_none_or(|current| timer_precedes(timer, current))
|
||||
{
|
||||
first_non_ready = Some(timer);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if predicate(timer) && selected.is_none_or(|current| timer_precedes(timer, current)) {
|
||||
selected = Some(timer);
|
||||
}
|
||||
}
|
||||
|
||||
let selected = selected?;
|
||||
if first_non_ready.is_some_and(|barrier| timer_precedes(barrier, selected)) {
|
||||
return None;
|
||||
}
|
||||
let selected_id = selected.id;
|
||||
let selected_sequence = selected.sequence;
|
||||
|
||||
@@ -259,6 +343,32 @@ impl<T> TimerScheduler<T> {
|
||||
.map(|timer| timer.run_at)
|
||||
}
|
||||
|
||||
pub fn has_ready_from_schedule_ranges(
|
||||
&self,
|
||||
ranges: &[TimerScheduleRange],
|
||||
now: Instant,
|
||||
allowance: TimerReadyAllowance,
|
||||
) -> bool {
|
||||
self.pending.iter().any(|timer| {
|
||||
self.active.contains(&timer.id)
|
||||
&& schedule_ranges_contain(ranges, timer.sequence)
|
||||
&& timer_ready(timer.run_at, timer.delay_ms, now, allowance)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn schedule_snapshot(&self) -> TimerScheduleSnapshot {
|
||||
TimerScheduleSnapshot {
|
||||
next_sequence: self.next_sequence,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn schedule_range_since(&self, start: TimerScheduleSnapshot) -> TimerScheduleRange {
|
||||
TimerScheduleRange {
|
||||
inclusive_sequence: start.next_sequence,
|
||||
exclusive_sequence: self.next_sequence,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_deadline(&self) -> Option<Instant> {
|
||||
self.pending
|
||||
.iter()
|
||||
@@ -342,6 +452,10 @@ impl<T> TimerScheduler<T> {
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_ranges_contain(ranges: &[TimerScheduleRange], sequence: u64) -> bool {
|
||||
ranges.iter().any(|range| range.contains(sequence))
|
||||
}
|
||||
|
||||
fn timer_precedes<T>(left: &ScheduledTimer<T>, right: &ScheduledTimer<T>) -> bool {
|
||||
match left.run_at.cmp(&right.run_at) {
|
||||
Ordering::Less => true,
|
||||
@@ -657,4 +771,79 @@ mod tests {
|
||||
scheduler.finish_running(ready.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schedule_ranges_select_only_timers_queued_inside_owner_operations() {
|
||||
let now = Instant::now();
|
||||
let mut scheduler = TimerScheduler::default();
|
||||
let earlier = scheduler.schedule_after("earlier", 0, now);
|
||||
let first_start = scheduler.schedule_snapshot();
|
||||
let first = scheduler.schedule_after("first", 0, now);
|
||||
let first_range = scheduler.schedule_range_since(first_start);
|
||||
let between = scheduler.schedule_after("between", 0, now);
|
||||
let second_start = scheduler.schedule_snapshot();
|
||||
let second = scheduler.schedule_after("second", 0, now);
|
||||
let second_range = scheduler.schedule_range_since(second_start);
|
||||
let later = scheduler.schedule_after("later", 0, now);
|
||||
let ranges = [first_range, second_range];
|
||||
|
||||
assert!(scheduler.has_ready_from_schedule_ranges(&ranges, now, TimerReadyAllowance::NONE));
|
||||
for (expected_id, expected_payload) in [(first, "first"), (second, "second")] {
|
||||
let ready = scheduler
|
||||
.take_next_ready_from_schedule_ranges(&ranges, now, TimerReadyAllowance::NONE)
|
||||
.expect("timer scheduled inside an owner range should be ready");
|
||||
assert_eq!(ready.id, expected_id);
|
||||
assert_eq!(ready.payload, expected_payload);
|
||||
scheduler.finish_running(ready.id);
|
||||
}
|
||||
|
||||
assert!(!scheduler.has_ready_from_schedule_ranges(&ranges, now, TimerReadyAllowance::NONE));
|
||||
assert!(
|
||||
scheduler
|
||||
.take_next_ready_from_schedule_ranges(&ranges, now, TimerReadyAllowance::NONE)
|
||||
.is_none(),
|
||||
"timers outside the owner ranges must remain for later task turns"
|
||||
);
|
||||
for (expected_id, expected_payload) in
|
||||
[(earlier, "earlier"), (between, "between"), (later, "later")]
|
||||
{
|
||||
let ready = scheduler
|
||||
.take_next_ready(now, TimerReadyAllowance::NONE)
|
||||
.expect("timer outside the ranges should remain in the ordinary queue");
|
||||
assert_eq!(ready.id, expected_id);
|
||||
assert_eq!(ready.payload, expected_payload);
|
||||
scheduler.finish_running(ready.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interval_rescheduled_after_range_is_not_drained_twice() {
|
||||
let now = Instant::now();
|
||||
let mut scheduler = TimerScheduler::default();
|
||||
let start = scheduler.schedule_snapshot();
|
||||
let interval = scheduler.schedule_after("tick", 0, now);
|
||||
let range = scheduler.schedule_range_since(start);
|
||||
|
||||
let ready = scheduler
|
||||
.take_next_ready_from_schedule_ranges(&[range], now, TimerReadyAllowance::NONE)
|
||||
.expect("initial interval task should belong to the range");
|
||||
assert_eq!(ready.id, interval);
|
||||
assert!(scheduler.reschedule_running_after(ready.id, ready.payload, 1, now));
|
||||
|
||||
assert!(
|
||||
scheduler
|
||||
.take_next_ready_from_schedule_ranges(
|
||||
&[range],
|
||||
now + Duration::from_millis(1),
|
||||
TimerReadyAllowance::NONE,
|
||||
)
|
||||
.is_none(),
|
||||
"an interval's newly scheduled task must not re-enter the closed range"
|
||||
);
|
||||
let ready = scheduler
|
||||
.take_next_ready(now + Duration::from_millis(1), TimerReadyAllowance::NONE)
|
||||
.expect("rescheduled interval should remain in the ordinary queue");
|
||||
assert_eq!(ready.id, interval);
|
||||
scheduler.finish_running(ready.id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user