refactor(renderer): record parser suspension resume ownership

Keep the continuation owner in each parser suspension, so scheduler routing
does not infer it from document.write pending work. Require matching
ownership when consuming resume permits and cover both stylesheet paths
with regression tests.
This commit is contained in:
ldm0
2026-09-24 19:22:52 +08:00
committed by Donough Liu
parent 06d6a57d72
commit d3b336740c
7 changed files with 295 additions and 96 deletions
+6 -3
View File
@@ -61,7 +61,8 @@ use crate::{
frame_owner_model::FrameDocumentTaskOwner,
live_document_parser::{
DocumentParserLifetime, DocumentParserRunState, DocumentParserSession,
DocumentParserSessionControlHandle, ParserResumePermit, ParserSuspensionCause,
DocumentParserSessionControlHandle, ParserResumeOwner, ParserResumePermit,
ParserSuspensionCause,
},
module_runtime::ModuleMapKey,
network::ResourceRequestClient,
@@ -294,11 +295,13 @@ impl ParserConnectedScriptBridge {
}
pub(crate) fn suspend(&self, cause: ParserSuspensionCause) -> ParserResumePermit {
self.parser_control.suspend(cause)
self.parser_control
.suspend(cause, ParserResumeOwner::DocumentWrite)
}
pub(crate) fn resume(&self, permit: ParserResumePermit) -> bool {
self.parser_control.resume(permit)
self.parser_control
.resume(permit, ParserResumeOwner::DocumentWrite)
}
pub(crate) fn begin_pump(&self) -> crate::live_document_parser::DocumentParserPumpGuard {
@@ -1867,7 +1867,7 @@ impl DocumentRuntime {
let insertion = self.take_suspended_document_write_insertion(
document_handle,
parser_bridge,
ParserSuspensionCause::DocumentWriteExternalScript { script: node },
ParserSuspensionCause::ParserClassicSource { script: node },
);
self.start_document_write_external_script_load(
scope,
@@ -1958,7 +1958,7 @@ impl DocumentRuntime {
DocumentWriteScriptRunOutcome::Suspend(start) => {
Self::resuspend_document_write_insertion(
&mut insertion,
ParserSuspensionCause::DocumentWriteExternalScript { script: node },
ParserSuspensionCause::ParserClassicSource { script: node },
);
self.start_document_write_external_script_load(
scope,
@@ -2184,7 +2184,7 @@ impl DocumentRuntime {
let insertion = self.take_suspended_document_write_insertion(
document_handle,
parser_bridge,
ParserSuspensionCause::DocumentWriteExternalScript { script },
ParserSuspensionCause::ParserClassicSource { script },
);
self.start_document_write_external_script_load(
scope,
+89 -9
View File
@@ -206,13 +206,23 @@ pub(crate) enum ParserSuspensionCause {
ParserClassicSource { script: NativeNodeId },
ParserClassicStylesheets { script: NativeNodeId },
ParserCreatedStylesheet { owner: NativeNodeId },
DocumentWriteExternalScript { script: NativeNodeId },
}
/// The component retaining the continuation for one exact parser suspension.
/// The same script or stylesheet blocker can be reached by either component.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ParserResumeOwner {
/// The main or child document's ordinary parser driver.
ParserDriver,
/// A suspended insertion retained by `DocumentRuntime`.
DocumentWrite,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ParserSuspension {
id: ParserSuspensionId,
cause: ParserSuspensionCause,
resume_owner: ParserResumeOwner,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -237,6 +247,7 @@ pub(crate) enum DocumentParserRunState {
Suspended {
id: ParserSuspensionId,
cause: ParserSuspensionCause,
resume_owner: ParserResumeOwner,
},
Finishing,
Finished,
@@ -272,6 +283,7 @@ enum DocumentParserLifecycleState {
Suspended {
id: ParserSuspensionId,
cause: ParserSuspensionCause,
resume_owner: ParserResumeOwner,
},
Finishing,
Finished,
@@ -283,6 +295,7 @@ impl From<ParserSuspension> for DocumentParserLifecycleState {
Self::Suspended {
id: suspension.id,
cause: suspension.cause,
resume_owner: suspension.resume_owner,
}
}
}
@@ -290,7 +303,15 @@ impl From<ParserSuspension> for DocumentParserLifecycleState {
impl DocumentParserLifecycleState {
fn suspension(self) -> Option<ParserSuspension> {
match self {
Self::Suspended { id, cause } => Some(ParserSuspension { id, cause }),
Self::Suspended {
id,
cause,
resume_owner,
} => Some(ParserSuspension {
id,
cause,
resume_owner,
}),
_ => None,
}
}
@@ -299,7 +320,18 @@ impl DocumentParserLifecycleState {
match (self, pump_session_nesting_level) {
(Self::Ready, nesting_level @ 1..) => DocumentParserRunState::Pumping { nesting_level },
(Self::Ready, 0) => DocumentParserRunState::Ready,
(Self::Suspended { id, cause }, _) => DocumentParserRunState::Suspended { id, cause },
(
Self::Suspended {
id,
cause,
resume_owner,
},
_,
) => DocumentParserRunState::Suspended {
id,
cause,
resume_owner,
},
(Self::Finishing, _) => DocumentParserRunState::Finishing,
(Self::Finished, _) => DocumentParserRunState::Finished,
(Self::Stopped(reason), _) => DocumentParserRunState::Stopped(reason),
@@ -439,7 +471,11 @@ impl DocumentParserSessionControlHandle {
true
}
pub(crate) fn suspend(&self, cause: ParserSuspensionCause) -> ParserResumePermit {
pub(crate) fn suspend(
&self,
cause: ParserSuspensionCause,
resume_owner: ParserResumeOwner,
) -> ParserResumePermit {
let mut control = self.0.borrow_mut();
assert_eq!(
control.lifecycle_state,
@@ -454,6 +490,7 @@ impl DocumentParserSessionControlHandle {
let suspension = ParserSuspension {
id: suspension_id,
cause,
resume_owner,
};
if matches!(
control.finish_request_state,
@@ -478,7 +515,11 @@ impl DocumentParserSessionControlHandle {
})
}
pub(crate) fn resume(&self, permit: ParserResumePermit) -> bool {
pub(crate) fn resume(
&self,
permit: ParserResumePermit,
resume_owner: ParserResumeOwner,
) -> bool {
let mut control = self.0.borrow_mut();
if permit.session_id != control.session_id {
return false;
@@ -486,7 +527,7 @@ impl DocumentParserSessionControlHandle {
let Some(suspension) = control.lifecycle_state.suspension() else {
return false;
};
if suspension.id != permit.suspension_id {
if suspension.id != permit.suspension_id || suspension.resume_owner != resume_owner {
return false;
}
control.lifecycle_state = DocumentParserLifecycleState::Ready;
@@ -809,7 +850,7 @@ impl DocumentParserSession {
}
pub(crate) fn suspend(&mut self, cause: ParserSuspensionCause) -> ParserResumePermit {
self.control.suspend(cause)
self.control.suspend(cause, ParserResumeOwner::ParserDriver)
}
pub(crate) fn current_resume_permit(&self) -> Option<ParserResumePermit> {
@@ -817,7 +858,7 @@ impl DocumentParserSession {
}
pub(crate) fn resume(&mut self, permit: ParserResumePermit) -> bool {
self.control.resume(permit)
self.control.resume(permit, ParserResumeOwner::ParserDriver)
}
pub(crate) fn stop(&mut self, reason: ParserStopReason) {
@@ -1228,13 +1269,52 @@ mod session_state_tests {
assert!(!other.resume(first));
assert!(parser.resume(first));
let second = parser.suspend(ParserSuspensionCause::DocumentWriteExternalScript {
let second = parser.suspend(ParserSuspensionCause::ParserClassicSource {
script: NativeNodeId::new(6),
});
assert!(!parser.resume(first));
assert!(parser.resume(second));
}
#[test]
fn parser_resume_permits_cannot_cross_continuation_owners() {
let mut parser = session();
let bridge = crate::document_runtime::ParserConnectedScriptBridge::for_session(&parser)
.expect("HTML parser bridge");
for cause in [
ParserSuspensionCause::ParserClassicSource {
script: NativeNodeId::new(8),
},
ParserSuspensionCause::ParserClassicStylesheets {
script: NativeNodeId::new(8),
},
ParserSuspensionCause::ParserCreatedStylesheet {
owner: NativeNodeId::new(9),
},
] {
let driver_permit = parser.suspend(cause);
let driver_state = parser.run_state();
assert!(!bridge.resume(driver_permit));
assert_eq!(parser.run_state(), driver_state);
assert!(parser.resume(driver_permit));
let insertion_permit = bridge.suspend(cause);
let insertion_state = parser.run_state();
assert!(matches!(
insertion_state,
DocumentParserRunState::Suspended {
resume_owner: ParserResumeOwner::DocumentWrite,
..
}
));
assert!(!parser.resume(insertion_permit));
assert!(!bridge.resume(driver_permit));
assert_eq!(parser.run_state(), insertion_state);
assert!(bridge.resume(insertion_permit));
assert!(!bridge.resume(insertion_permit));
}
}
#[test]
fn close_defers_without_consuming_the_active_parser_suspension() {
let mut parser = DocumentParserSession::start_open_live_document(
@@ -1,7 +1,7 @@
use super::{
DocumentParserCloseDisposition, DocumentParserLifetime, DocumentParserRunState,
DocumentParserSessionControlHandle, LiveDocumentParserDiscoverySignals,
LiveDocumentParserOwner, LiveDocumentParserStepOutcome, ParserStopReason,
LiveDocumentParserOwner, LiveDocumentParserStepOutcome, ParserResumeOwner, ParserStopReason,
ParserSuspensionCause, advance_next_live_document_parser_step,
};
use std::{cell::RefCell, rc::Rc};
@@ -38,7 +38,7 @@ impl ParserInsertionHandle {
}
pub(crate) fn suspend(&self, cause: ParserSuspensionCause) {
let _ = self.control.suspend(cause);
let _ = self.control.suspend(cause, ParserResumeOwner::ParserDriver);
}
pub(crate) fn stop(&self, reason: ParserStopReason) {
@@ -4,7 +4,8 @@ use crate::{
frame_owner_model::FrameDocumentOwner,
live_document_parser::{
DocumentParserRunState, DocumentParserSession, DocumentParserSessionControlHandle,
ParserInsertionHandle, ParserResumePermit, ParserStopReason, ParserSuspensionCause,
ParserInsertionHandle, ParserResumeOwner, ParserResumePermit, ParserStopReason,
ParserSuspensionCause,
},
};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
@@ -153,7 +154,7 @@ impl ChildDocumentParserStore {
permit: ParserResumePermit,
) -> Option<bool> {
self.control_handle(owner)
.map(|parser| parser.resume(permit))
.map(|parser| parser.resume(permit, ParserResumeOwner::ParserDriver))
}
pub(in crate::native_bridge::context_host) fn is_suspended_on_parser_created_stylesheet(
@@ -7,6 +7,9 @@ use tokio::net::TcpListener;
use tokio::task::JoinHandle;
use url::Url;
use crate::live_document_parser::{
DocumentParserRunState, ParserResumeOwner, ParserSuspensionCause,
};
use crate::page_resource_completion::{
PageResourceCompletionDocumentEffect, PageResourceCompletionOutputEffect,
RendererPageResourceCompletion, RendererPageResourceCompletionOwner,
@@ -277,6 +280,30 @@ async fn start_standalone_document_write_page_for_page_id(
page_id: PageId,
html: String,
document_url: Url,
) -> PendingStandaloneDocumentWritePage {
let pending = start_standalone_parser_page(page_id, html, document_url).await;
let target = pending
.runtime
.page_vm
.vm()
.current_document_write_external_script_fetch_target()
.expect("pending document.write load should retain its exact target");
assert_eq!(
pending
.runtime
.page_vm
.vm()
.current_main_document_task_owner(),
Some(target.task_owner()),
"producer target must capture the current main Document owner"
);
pending
}
async fn start_standalone_parser_page(
page_id: PageId,
html: String,
document_url: Url,
) -> PendingStandaloneDocumentWritePage {
let loader_owner =
ResourceRequestClient::new(&FetchConfig::default()).expect("document.write test loader");
@@ -328,16 +355,6 @@ async fn start_standalone_document_write_page_for_page_id(
runtime
.page_vm
.retain_standalone_request_client_owner_for_test(loader_owner);
let target = runtime
.page_vm
.vm()
.current_document_write_external_script_fetch_target()
.expect("pending document.write load should retain its exact target");
assert_eq!(
runtime.page_vm.vm().current_main_document_task_owner(),
Some(target.task_owner()),
"producer target must capture the current main Document owner"
);
PendingStandaloneDocumentWritePage {
runtime,
started,
@@ -633,6 +650,125 @@ async fn evaluate_pending_on_owner_local_task(
.expect("pending document.write result should evaluate")
}
#[test]
fn parser_driver_retains_stylesheet_suspension_until_its_continuation() {
assert_stylesheet_suspension_resume_owner(ParserResumeOwner::ParserDriver);
}
#[test]
fn document_write_retains_stylesheet_suspension_until_its_continuation() {
assert_stylesheet_suspension_resume_owner(ParserResumeOwner::DocumentWrite);
}
fn assert_stylesheet_suspension_resume_owner(resume_owner: ParserResumeOwner) {
super::tests::run_phase_one_large_stack_test("stylesheet-suspension-owner", move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("current-thread runtime");
runtime.block_on(tokio::task::LocalSet::new().run_until(async move {
let _js_runtime = crate::JsRuntime::initialize();
let listener = TcpListener::bind("127.0.0.1:0").await.expect("stylesheet server");
let address = listener.local_addr().expect("server address");
let document_url = Url::parse(&format!("http://{address}/page.html")).unwrap();
let (release_tx, release_rx) = tokio::sync::oneshot::channel();
let server = tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.expect("stylesheet request");
let mut request = vec![0; 4096];
let read = stream.read(&mut request).await.expect("read stylesheet request");
assert!(String::from_utf8_lossy(&request[..read]).starts_with("GET /slow.css "));
release_rx.await.expect("release stylesheet response");
let css = "html { color: rgb(1, 2, 3); }";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/css\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{css}",
css.len(),
);
stream.write_all(response.as_bytes()).await.expect("stylesheet response");
stream.shutdown().await.expect("stylesheet response closed");
});
let blocked_html = r#"<link rel="stylesheet" href="/slow.css"><script>
__resumeEvents.push('script');
globalThis.__tailAtScript = !!document.getElementById('parser-tail');
</script>"#;
let html = match resume_owner {
ParserResumeOwner::ParserDriver => format!(
"<!doctype html><html><head><script>globalThis.__resumeEvents = ['outer'];</script>{blocked_html}</head><body><p id='parser-tail'>tail</p></body></html>",
),
ParserResumeOwner::DocumentWrite => format!(
"<!doctype html><html><head><script>globalThis.__resumeEvents = ['outer']; document.write(`{}`); __resumeEvents.push('outer-done');</script></head><body><p id='parser-tail'>tail</p></body></html>",
blocked_html.replace("</script>", "<\\/script>"),
),
};
let mut pending = start_standalone_parser_page(
PageId::new_for_testing(902), html, document_url,
).await;
let suspended_state = pending.runtime.state.parser_session.run_state();
assert!(matches!(suspended_state, DocumentParserRunState::Suspended {
cause: ParserSuspensionCause::ParserClassicStylesheets { .. },
resume_owner: actual_owner, ..
} if actual_owner == resume_owner));
let (next_pending, result) = evaluate_pending_on_owner_local_task(
pending,
"JSON.stringify({events: __resumeEvents, tail: !!document.getElementById('parser-tail')})",
).await;
pending = next_pending;
let expected_before = match resume_owner {
ParserResumeOwner::ParserDriver => r#"{"events":["outer"],"tail":false}"#,
ParserResumeOwner::DocumentWrite => r#"{"events":["outer","outer-done"],"tail":false}"#,
};
assert_eq!(result.get("value").and_then(serde_json::Value::as_str), Some(expected_before));
// A fresh parser turn must preserve the same blocker while CSS is pending.
let executor = pending.runtime.page_vm.local_executor.clone();
pending = super::access::run_named_owner_local_task(
executor,
"stylesheet-suspended parser turn channel closed",
async move {
pending.runtime.owner = ParseTimeOwner::Parser;
pending.runtime.pending_parsing_blocking_wait = PendingParsingBlockingWait::None;
assert_eq!(pending.runtime.drive_owner_step().await?, OwnerStepProgress::BlockedOnPageTask);
assert_eq!(pending.runtime.state.parser_session.run_state(), suspended_state);
Ok(pending)
},
).await.expect("parser should remain suspended");
release_tx.send(()).expect("release CSS");
tokio::time::timeout(
std::time::Duration::from_secs(2),
wait_for_standalone_stylesheet_completion(&mut pending),
).await.expect("CSS completion should arrive");
pending = run_standalone_selected_page_task(
pending,
crate::runtime::page_vm::PageSelectedTaskTestSelector::StylesheetCompletion,
).await;
assert_eq!(
pending.runtime.state.parser_session.run_state(), suspended_state,
"CSS completion must leave resumption to the selected parser continuation",
);
let PendingStandaloneDocumentWritePage { runtime, started, owner_wake_rx } = pending;
let (outcome, _) = resume_standalone_main_parser_continuation_if_ready(
ParseTimePageVmCreationOutcome::PendingPhaseOne(
PendingPhaseOneResidence::ClosedInputPageWork { runtime, started },
),
owner_wake_rx,
).await;
let ParseTimePageVmCreationOutcome::ContinuePhaseTwo { page_vm, .. } = outcome else {
panic!("the owning continuation should finish the parser");
};
let result = evaluate_on_owner_local_task(
page_vm,
"JSON.stringify({events: __resumeEvents, tailAtScript: __tailAtScript, tail: !!document.getElementById('parser-tail')})",
).await;
let expected_after = match resume_owner {
ParserResumeOwner::ParserDriver => r#"{"events":["outer","script"],"tailAtScript":false,"tail":true}"#,
ParserResumeOwner::DocumentWrite => r#"{"events":["outer","outer-done","script"],"tailAtScript":false,"tail":true}"#,
};
assert_eq!(result.get("value").and_then(serde_json::Value::as_str), Some(expected_after));
server.await.expect("stylesheet server should finish");
}));
});
}
#[test]
fn standalone_document_write_external_script_uses_typed_phase_one_route() {
super::tests::run_phase_one_large_stack_test("typed-document-write-phase-one", || {
@@ -19,8 +19,8 @@ use super::*;
use crate::document_runtime::parser_script_preparation_failure_page_owned_work;
use crate::dom::native::{Attribute, DomMutationEffects, NativeNodeId};
use crate::live_document_parser::{
DocumentParserFinishRequestState, LiveDocumentParserOwner, LiveDocumentParserStepOutcome,
ParserSuspensionCause,
DocumentParserFinishRequestState, DocumentParserRunState, LiveDocumentParserOwner,
LiveDocumentParserStepOutcome, ParserResumeOwner, ParserSuspensionCause,
};
use crate::parser::{
ParserDomMutation, ParserDomMutationConsumer, ParserDomReadConsumer,
@@ -538,22 +538,36 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
);
return Ok(owner_step_progress_after_current_document_stop(page_vm));
}
match self.parser_session.suspension_cause() {
Some(ParserSuspensionCause::ParserClassicStylesheets { .. })
if page_vm
.vm()
.document_runtime
.has_pending_document_write_stylesheet_blocked_script() =>
{
// A document.write() insertion owns this suspension and its
// script. Its stylesheet completion is admitted on the Page
// lane; the main parser runner has no pending script to resume.
match self.parser_session.run_state() {
DocumentParserRunState::Suspended {
resume_owner: ParserResumeOwner::DocumentWrite,
cause:
ParserSuspensionCause::ParserClassicStylesheets { .. }
| ParserSuspensionCause::ParserCreatedStylesheet { .. },
..
} => {
// The insertion retains this suspension's resume permit.
// Its continuation must run through the Page scheduler.
return Ok(suspend_parser_for_stylesheet_page_task(
owner,
pending_parsing_blocking_wait,
));
}
Some(ParserSuspensionCause::ParserCreatedStylesheet { .. }) => {
DocumentParserRunState::Suspended {
resume_owner: ParserResumeOwner::DocumentWrite,
cause: ParserSuspensionCause::ParserClassicSource { .. },
..
} => {
*owner = ParseTimeOwner::Document;
*pending_parsing_blocking_wait =
PendingParsingBlockingWait::PageNetworkingDocumentWriteExternalScript;
return Ok(OwnerStepProgress::BlockedOnPageTask);
}
DocumentParserRunState::Suspended {
resume_owner: ParserResumeOwner::ParserDriver,
cause: ParserSuspensionCause::ParserCreatedStylesheet { .. },
..
} => {
if !page_vm
.vm()
.document_runtime
@@ -573,31 +587,7 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
"the admitted stylesheet continuation must resume its exact parser suspension"
);
}
Some(ParserSuspensionCause::DocumentWriteExternalScript { .. }) => {
if page_vm
.vm()
.document_runtime
.has_pending_document_write_external_script_load()
{
*owner = ParseTimeOwner::Document;
*pending_parsing_blocking_wait =
PendingParsingBlockingWait::PageNetworkingDocumentWriteExternalScript;
return Ok(OwnerStepProgress::BlockedOnPageTask);
}
let permit = self
.parser_session
.current_resume_permit()
.expect("a document.write-suspended parser must retain its resume permit");
assert!(
self.parser_session.resume(permit),
"the admitted document.write continuation must resume its exact parser suspension"
);
}
Some(
ParserSuspensionCause::ParserClassicSource { .. }
| ParserSuspensionCause::ParserClassicStylesheets { .. },
)
| None => {}
_ => {}
}
debug_assert!(
!pending_parsing_blocking_wait.is_pending(),
@@ -1637,9 +1627,7 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
break ParserStepAdvanceOutcome::StoppedCurrentDocument;
}
ScriptHandoffOutcome::NoNavigation => {
if let Some(outcome) =
self.document_write_suspension_step_outcome(page_vm)
{
if let Some(outcome) = self.document_write_suspension_step_outcome() {
break outcome;
}
}
@@ -1759,9 +1747,7 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
break ParserStepAdvanceOutcome::StoppedCurrentDocument;
}
ScriptHandoffOutcome::NoNavigation => {
if let Some(outcome) =
self.document_write_suspension_step_outcome(page_vm)
{
if let Some(outcome) = self.document_write_suspension_step_outcome() {
break outcome;
}
}
@@ -1785,30 +1771,23 @@ impl<'loader, 'state> ParserDriver<'loader, 'state> {
Ok(outcome)
}
fn document_write_suspension_step_outcome(
&self,
page_vm: &PageVm,
) -> Option<ParserStepAdvanceOutcome> {
match self.parser_session.suspension_cause() {
Some(ParserSuspensionCause::ParserClassicStylesheets { .. })
if page_vm
.vm()
.document_runtime
.has_pending_document_write_stylesheet_blocked_script() =>
{
Some(ParserStepAdvanceOutcome::BlockedOnStylesheetParserPause)
fn document_write_suspension_step_outcome(&self) -> Option<ParserStepAdvanceOutcome> {
let DocumentParserRunState::Suspended {
resume_owner: ParserResumeOwner::DocumentWrite,
cause,
..
} = self.parser_session.run_state()
else {
return None;
};
Some(match cause {
ParserSuspensionCause::ParserClassicStylesheets { .. }
| ParserSuspensionCause::ParserCreatedStylesheet { .. } => {
ParserStepAdvanceOutcome::BlockedOnStylesheetParserPause
}
Some(ParserSuspensionCause::ParserCreatedStylesheet { .. }) => {
Some(ParserStepAdvanceOutcome::BlockedOnStylesheetParserPause)
ParserSuspensionCause::ParserClassicSource { .. } => {
ParserStepAdvanceOutcome::BlockedOnDocumentWriteExternalLoad
}
Some(ParserSuspensionCause::DocumentWriteExternalScript { .. }) => {
Some(ParserStepAdvanceOutcome::BlockedOnDocumentWriteExternalLoad)
}
Some(
ParserSuspensionCause::ParserClassicSource { .. }
| ParserSuspensionCause::ParserClassicStylesheets { .. },
)
| None => None,
}
})
}
}