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