refactor(renderer): unify child Window realm state

This commit is contained in:
ldm0
2026-09-07 23:04:11 +08:00
parent acb59e7107
commit eced42d4f9
15 changed files with 273 additions and 3 deletions
+2 -1
View File
@@ -347,7 +347,8 @@ pub(crate) use self::performance_runtime::{
ResourcePerformanceEntry, bind_window_performance_seed, current_performance_time_origin,
increment_performance_event_count, record_performance_dom_content_loaded_event_end,
record_performance_dom_content_loaded_event_start, record_performance_load_event_end,
record_performance_load_event_start, record_resource_performance_entry,
record_performance_load_event_end_for_window, record_performance_load_event_start,
record_performance_load_event_start_for_window, record_resource_performance_entry,
run_resource_timing_buffer_full_task,
};
use self::range::callback_arg_node_object;
@@ -193,6 +193,43 @@ pub(crate) fn current_performance_time_origin(scope: &mut v8::PinScope<'_, '_>)
.unwrap_or(0.0)
}
pub(crate) fn record_performance_load_event_start_for_window(
scope: &mut v8::PinScope<'_, '_>,
window: v8::Local<'_, v8::Object>,
) {
record_performance_load_event_for_window(
scope,
window,
install::record_performance_load_event_start,
);
}
pub(crate) fn record_performance_load_event_end_for_window(
scope: &mut v8::PinScope<'_, '_>,
window: v8::Local<'_, v8::Object>,
) {
record_performance_load_event_for_window(
scope,
window,
install::record_performance_load_event_end,
);
}
fn record_performance_load_event_for_window(
scope: &mut v8::PinScope<'_, '_>,
window: v8::Local<'_, v8::Object>,
record: fn(&mut v8::PinScope<'_, '_>),
) {
let Some(relevant_context) = window.get_creation_context(scope) else {
return;
};
if relevant_context == scope.get_current_context() {
record(scope);
return;
}
let scope = &mut v8::ContextScope::new(scope, relevant_context);
record(scope);
}
pub(crate) struct ResourcePerformanceEntry {
name: String,
initiator_type: String,
@@ -785,6 +785,12 @@ pub(crate) fn record_performance_dom_content_loaded_event_start(scope: &mut v8::
super::window_state::record_pending_dom_content_loaded_start(scope);
return;
};
if performance_lifecycle_timestamp(scope, performance, DOM_CONTENT_LOADED_START_INDEX)
.unwrap_or(0.0)
> 0.0
{
return;
}
let previous = 0.0;
let timestamp = monotonic_lifecycle_timestamp(scope, performance, previous);
apply_dom_content_loaded_start(scope, performance, timestamp);
@@ -819,6 +825,12 @@ pub(crate) fn record_performance_dom_content_loaded_event_end(scope: &mut v8::Pi
super::window_state::record_pending_dom_content_loaded_end(scope);
return;
};
if performance_lifecycle_timestamp(scope, performance, DOM_CONTENT_LOADED_END_INDEX)
.unwrap_or(0.0)
> 0.0
{
return;
}
let previous =
performance_lifecycle_timestamp(scope, performance, DOM_CONTENT_LOADED_START_INDEX)
.unwrap_or(0.0);
@@ -853,6 +865,9 @@ pub(crate) fn record_performance_load_event_start(scope: &mut v8::PinScope<'_, '
super::window_state::record_pending_load_start(scope);
return;
};
if performance_lifecycle_timestamp(scope, performance, LOAD_START_INDEX).unwrap_or(0.0) > 0.0 {
return;
}
let previous =
performance_lifecycle_timestamp(scope, performance, DOM_CONTENT_LOADED_END_INDEX)
.unwrap_or(0.0);
@@ -1,3 +1,4 @@
use super::super::fixture_support::child_upstream_fixture_text;
use super::super::{ChildBrowsingContextBootstrap, JsContextHost};
use crate::document_runtime::DomHandle;
use url::Url;
@@ -7,7 +8,7 @@ impl JsContextHost {
&self,
url: &Url,
) -> bool {
matches!(url.scheme(), "http" | "https")
matches!(url.scheme(), "http" | "https") && child_upstream_fixture_text(url).is_none()
}
pub(in crate::native_bridge::context_host::child_documents) fn child_document_bootstrap_requires_async_load(
@@ -9,6 +9,8 @@ use crate::{
context_bootstrap::{
construct_original_event, dispatch_beforeunload_for_runtime_owner,
dispatch_pagehide_for_runtime_owner, dispatch_unload_for_runtime_owner,
record_performance_dom_content_loaded_event_end,
record_performance_dom_content_loaded_event_start,
},
detached_event_target::dispatch_detached_simple_event,
document_runtime::DomHandle,
@@ -712,6 +714,7 @@ impl JsContextHost {
{
return FrameDocumentLifecycleTaskEffect::NotApplied;
}
record_performance_dom_content_loaded_event_start(scope);
let event_dispatched = self
.child_browsing_context_document_wrapper(scope, child_handle)
.is_some_and(|document| {
@@ -725,6 +728,7 @@ impl JsContextHost {
false,
)
});
record_performance_dom_content_loaded_event_end(scope);
if self
.frame_owner_store
.child_document_task_owner_is_current(child_handle, action.owner())
@@ -1,3 +1,4 @@
use super::super::fixture_support::child_upstream_fixture_text;
use super::super::{ChildBrowsingContextBootstrap, ChildBrowsingContextSnapshot, JsContextHost};
use super::configure_child_document_navigation_request;
use crate::document_runtime::{DocumentPolicyContainer, DomHandle};
@@ -46,7 +47,13 @@ impl JsContextHost {
url.clone(),
"<!DOCTYPE html><html><head></head><body></body></html>".into(),
)),
"http" | "https" => None,
"http" | "https" => child_upstream_fixture_text(url).map(|markup| {
ChildBrowsingContextSnapshot::new(
url.clone(),
markup,
child_document_content_type_for_url(url),
)
}),
"blob" => {
let (body, mime_type) = crate::blob::object_url_body_and_type(url.as_str())?;
if !mime_type.is_empty() && !is_html_document_mime(&mime_type) {
@@ -0,0 +1,44 @@
use url::Url;
/// Resolve upstream browser-test child documents from the renderer fixture
/// corpus. This is intentionally available only to tests and debug builds;
/// production HTTP(S) navigations must use the network loader.
pub(super) fn child_upstream_fixture_text(url: &Url) -> Option<String> {
#[cfg(any(test, debug_assertions))]
{
let path = url.path().strip_prefix("/src/browser/tests/")?;
let relative_path = path.replace("%20", " ");
let fixture_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/lightpanda/upstream")
.join(relative_path);
std::fs::read_to_string(fixture_path).ok()
}
#[cfg(not(any(test, debug_assertions)))]
{
let _ = url;
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn child_upstream_fixture_lookup_is_path_scoped() {
let fixture = Url::parse(
"https://example.test/src/browser/tests/frames/support/target_fragment_child.html",
)
.unwrap();
assert!(
child_upstream_fixture_text(&fixture)
.is_some_and(|source| source.contains("id=\"target\""))
);
let unrelated = Url::parse(
"https://example.test/src/browser/test/frames/support/target_fragment_child.html",
)
.unwrap();
assert!(child_upstream_fixture_text(&unrelated).is_none());
}
}
@@ -3,6 +3,8 @@ use crate::{
context_bootstrap::{
LocationNavigationKind, construct_original_event, construct_original_page_transition_event,
meta_refresh_navigation_kind, navigate_location_object_with_child_navigate_event,
record_performance_load_event_end_for_window,
record_performance_load_event_start_for_window,
},
document_runtime::{DomHandle, EventTargetHandle, MetaRefreshNavigation},
frame_owner_model::{
@@ -400,13 +402,20 @@ impl JsContextHost {
action: FrameDocumentLoadDeliveryAction,
) -> ChildFrameLoadDeliveryPhaseResult {
let handle = action.child_handle();
let performance_window = self.existing_child_browsing_context_window_wrapper(scope, handle);
let Some(event) = construct_original_event(scope, "load") else {
self.abort_and_requeue_child_load_delivery(action);
return ChildFrameLoadDeliveryPhaseResult::without_callback(None);
};
if let Some(window) = performance_window {
record_performance_load_event_start_for_window(scope, window);
}
self.enter_child_browsing_context_host_load_dispatch(handle);
self.dispatch_child_window_event(scope, handle, "load", event);
self.leave_child_browsing_context_host_load_dispatch(handle);
if let Some(window) = performance_window {
record_performance_load_event_end_for_window(scope, window);
}
let progress = self
.frame_owner_store
.finish_current_child_document_load_delivery(action);
@@ -92,6 +92,7 @@ mod element_toggle_events;
mod event_callbacks;
mod file_chooser;
mod file_entry_file_callbacks;
mod fixture_support;
mod focus;
mod frame_document_ready_routes;
mod hash_changes;
@@ -1054,6 +1054,61 @@ fn performance_navigation_timing_updates_at_load_event_end() {
);
}
#[test]
fn performance_navigation_lifecycle_timestamps_are_write_once_per_navigation() {
let mut vm = new_storage_test_vm("https://performance-navigation-write-once.test/");
vm.dispatch_document_lifecycle_event("DOMContentLoaded")
.expect("first DOMContentLoaded should update navigation timing");
vm.dispatch_window_load_event()
.expect("first load should update navigation timing");
vm.eval(
r#"
globalThis.__initialNavigationLifecycleTiming = (() => {
const navigation = performance.getEntriesByType("navigation")[0];
return [
navigation.domInteractive,
navigation.domContentLoadedEventStart,
navigation.domContentLoadedEventEnd,
navigation.domComplete,
navigation.loadEventStart,
navigation.loadEventEnd
];
})();
"#,
)
.expect("initial navigation lifecycle timing should be captured");
vm.dispatch_document_lifecycle_event("DOMContentLoaded")
.expect("repeated DOMContentLoaded should remain dispatchable");
vm.dispatch_window_load_event()
.expect("repeated load should remain dispatchable");
assert_eq!(
vm.eval(
r#"
(() => {
const navigation = performance.getEntriesByType("navigation")[0];
const current = [
navigation.domInteractive,
navigation.domContentLoadedEventStart,
navigation.domContentLoadedEventEnd,
navigation.domComplete,
navigation.loadEventStart,
navigation.loadEventEnd
];
return current.every(
(value, index) =>
value === globalThis.__initialNavigationLifecycleTiming[index]
);
})()
"#,
)
.expect("repeated navigation lifecycle timing should be compared"),
"true"
);
}
#[test]
fn materialized_legacy_performance_timing_uses_integer_milliseconds() {
const TIME_ORIGIN: f64 = 1_700_000_000_000.75;
@@ -6381,6 +6381,89 @@ async fn element_matches_delegates_loaded_child_document_elements() {
);
}
#[tokio::test]
async fn child_navigation_performance_name_updates_after_iframe_src_change() {
let mut vm = new_storage_test_vm("https://child-navigation-performance.test/page.html");
vm.eval(
r#"
(() => {
globalThis.__childNavigationLoadCount = 0;
const frame = document.createElement("iframe");
globalThis.__childNavigationFrame = frame;
frame.onload = () => {
globalThis.__childNavigationLoadCount++;
};
frame.src = "/src/browser/tests/navigation-timing/resources/blank_page_green.html";
(document.body || document.documentElement || document).appendChild(frame);
})()
"#,
)
.expect("child navigation performance setup should evaluate");
let loads_before_initial_dispatch = vm
.eval("String(globalThis.__childNavigationLoadCount)")
.expect("initial child navigation load count should evaluate");
assert_eq!(loads_before_initial_dispatch, "0");
run_child_navigation_commit_and_host_load_for_test(
&mut vm,
"initial child navigation performance load",
)
.await;
let first = vm
.eval(
r#"
(() => {
const frame = __childNavigationFrame;
const entry = frame.contentWindow.performance.getEntriesByType("navigation")[0];
const timing = frame.contentWindow.performance.timing;
return [
entry.name === frame.contentWindow.location.href,
entry.name.endsWith("/blank_page_green.html"),
globalThis.__childNavigationLoadCount,
timing.domInteractive > timing.navigationStart,
timing.loadEventStart >= timing.domInteractive,
timing.loadEventEnd >= timing.loadEventStart,
frame.contentWindow.performance.now() >=
timing.loadEventEnd - timing.navigationStart
].join(":");
})()
"#,
)
.expect("first child navigation performance probe should evaluate");
assert_eq!(first, "true:true:1:true:true:true:true");
vm.eval(
r#"__childNavigationFrame.src = "/src/browser/tests/navigation-timing/resources/blank_page_yellow.html";"#,
)
.expect("second child navigation should queue");
let loads_before_second_dispatch = vm
.eval("String(globalThis.__childNavigationLoadCount)")
.expect("second child navigation pre-HostLoad load count should evaluate");
assert_eq!(loads_before_second_dispatch, "1");
run_child_navigation_commit_and_host_load_for_test(
&mut vm,
"second child navigation performance load",
)
.await;
let result = vm
.eval(
r#"
(() => {
const frame = __childNavigationFrame;
const entry = frame.contentWindow.performance.getEntriesByType("navigation")[0];
return [
entry.name === frame.contentWindow.location.href,
entry.name.endsWith("/blank_page_yellow.html"),
globalThis.__childNavigationLoadCount
].join(":");
})()
"#,
)
.expect("second child navigation performance probe should evaluate");
assert_eq!(result, "true:true:2");
}
#[tokio::test]
async fn child_content_document_getter_does_not_enumerate_script_wrappers_after_load() {
let mut vm = new_storage_test_vm("https://child-content-document-getter-script-state.test/");
@@ -0,0 +1,3 @@
<!doctype html>
<script>parent.postMessage("form navigation", "*");</script>
form navigation
@@ -0,0 +1,6 @@
<!doctype html>
<html>
<body>
<div id="target"></div>
</body>
</html>
@@ -0,0 +1,2 @@
<!doctype html>
<html><body>green</body></html>
@@ -0,0 +1,2 @@
<!doctype html>
<html><body>yellow</body></html>