fix(lifecycle): order defer timers before DOMContentLoaded

This commit is contained in:
ldm0
2026-09-27 07:50:13 +08:00
parent 4840ded1f6
commit 98e8340164
24 changed files with 650 additions and 4 deletions
@@ -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
+3
View File
@@ -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 {
+34 -1
View File
@@ -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 {
+12
View File
@@ -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,
+5 -1
View File
@@ -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
View File
@@ -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);
}
}