diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index ebac85fd28..dcb869034c 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs index 7acaf99a23..02b661eeae 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs index 76a397ca80..bf54dd50fa 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime/install.rs @@ -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); diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/async_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/async_policy.rs index f74d300c4f..e5c0057944 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/async_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/async_policy.rs @@ -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( diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs index 64b4c0b068..1a539f8393 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs @@ -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()) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs index 0a48263142..0fcfded8fd 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/snapshots.rs @@ -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(), "".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) { diff --git a/moli-renderer-v8/src/native_bridge/context_host/fixture_support.rs b/moli-renderer-v8/src/native_bridge/context_host/fixture_support.rs new file mode 100644 index 0000000000..aad4e02195 --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/fixture_support.rs @@ -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 { + #[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()); + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs index 4373522946..1d0ddfdef4 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs @@ -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); diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 4cff7952fe..67963ee14a 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -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; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs index 279eccaec4..9d3e729c34 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/navigation.rs @@ -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; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index b72d484819..a9043fbba3 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -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/"); diff --git a/moli-renderer-v8/tests/fixtures/lightpanda/upstream/frames/support/form_navigation_child.html b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/frames/support/form_navigation_child.html new file mode 100644 index 0000000000..5c60ff5332 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/frames/support/form_navigation_child.html @@ -0,0 +1,3 @@ + + +form navigation diff --git a/moli-renderer-v8/tests/fixtures/lightpanda/upstream/frames/support/target_fragment_child.html b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/frames/support/target_fragment_child.html new file mode 100644 index 0000000000..25edeab0be --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/frames/support/target_fragment_child.html @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/moli-renderer-v8/tests/fixtures/lightpanda/upstream/navigation-timing/resources/blank_page_green.html b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/navigation-timing/resources/blank_page_green.html new file mode 100644 index 0000000000..30b90f5b9a --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/navigation-timing/resources/blank_page_green.html @@ -0,0 +1,2 @@ + +green diff --git a/moli-renderer-v8/tests/fixtures/lightpanda/upstream/navigation-timing/resources/blank_page_yellow.html b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/navigation-timing/resources/blank_page_yellow.html new file mode 100644 index 0000000000..eb5a2f0c2b --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/lightpanda/upstream/navigation-timing/resources/blank_page_yellow.html @@ -0,0 +1,2 @@ + +yellow