diff --git a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs index ab96696c2b..fb4f8712e7 100644 --- a/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/performance_runtime.rs @@ -228,12 +228,62 @@ impl ResourcePerformanceEntry { response: &crate::protocol_types::NavigationResponse, ) -> Self { let body_size = response.body_bytes().len() as f64; - let header_size = response - .headers + Self::from_response_parts( + name, + initiator_type, + start_unix_millis, + response.status, + &response.headers, + body_size, + ) + } + + pub(crate) fn from_streaming_network_response( + name: impl Into, + initiator_type: &'static str, + start_unix_millis: Option, + response: &moli_fetch::ResponseHead, + body_size: usize, + ) -> Self { + Self::from_response_parts( + name, + initiator_type, + start_unix_millis, + response.status, + &response.headers, + body_size as f64, + ) + } + + pub(crate) fn from_fetch_response( + name: impl Into, + initiator_type: &'static str, + start_unix_millis: Option, + response: &moli_fetch::Response, + ) -> Self { + Self::from_response_parts( + name, + initiator_type, + start_unix_millis, + response.status, + &response.headers, + response.body_bytes().len() as f64, + ) + } + + fn from_response_parts( + name: impl Into, + initiator_type: &'static str, + start_unix_millis: Option, + response_status: u16, + response_headers: &[(String, String)], + body_size: f64, + ) -> Self { + let header_size = response_headers .iter() .map(|(name, value)| name.len() + value.len() + 4) .sum::() as f64; - let content_type = moli_web_mime::response_content_type(&response.headers) + let content_type = moli_web_mime::response_content_type(response_headers) .and_then(|value| moli_web_mime::mime_essence(&value)) .unwrap_or_default(); Self { @@ -246,7 +296,7 @@ impl ResourcePerformanceEntry { // The fetch lifecycle does not yet retain render-blocking metadata. // Avoid claiming that a resource blocked rendering until it does. render_blocking_status: "non-blocking".to_owned(), - response_status: f64::from(response.status), + response_status: f64::from(response_status), content_type, } } diff --git a/moli-renderer-v8/src/network_host/xhr/send.rs b/moli-renderer-v8/src/network_host/xhr/send.rs index 1166fd6bcd..f03356cb93 100644 --- a/moli-renderer-v8/src/network_host/xhr/send.rs +++ b/moli-renderer-v8/src/network_host/xhr/send.rs @@ -428,6 +428,15 @@ fn send_synchronous_network_xhr( match result { Ok(response) => { + crate::context_bootstrap::record_resource_performance_entry( + scope, + crate::context_bootstrap::ResourcePerformanceEntry::from_fetch_response( + prepared.resolved_url.as_str(), + "xmlhttprequest", + None, + &response, + ), + ); let observable_headers = crate::network_host::filter_cors_exposed_response_headers( &prepared.request_origin, &response.head(), diff --git a/moli-renderer-v8/src/script_vm/subresource_fetch.rs b/moli-renderer-v8/src/script_vm/subresource_fetch.rs index 0364570a94..ee93862e51 100644 --- a/moli-renderer-v8/src/script_vm/subresource_fetch.rs +++ b/moli-renderer-v8/src/script_vm/subresource_fetch.rs @@ -3740,6 +3740,15 @@ impl ScriptVm { resolver.resolve(scope, response_obj.into()); } PendingSubresourceContinuation::Xhr(xhr) => { + crate::context_bootstrap::record_resource_performance_entry( + scope, + crate::context_bootstrap::ResourcePerformanceEntry::from_network_response( + pending.info.url.as_str(), + "xmlhttprequest", + None, + &observable_response, + ), + ); let xhr = v8::Local::new(scope, &xhr); let (head, body) = observable_response.into_body(); crate::network_host::apply_xhr_response_body_source_with_status_text( @@ -5542,6 +5551,7 @@ impl ScriptVm { let finish_body_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now); let response_body = streaming.body_writer.finish(); + let response_body_size = response_body.len(); trace_async_subresource_stage( "async_subresource_streaming_body_finished", trace_fields, @@ -5697,6 +5707,16 @@ impl ScriptVm { { let xhr_started = moli_trace::cdp_runtime_trace_enabled().then(Instant::now); + crate::context_bootstrap::record_resource_performance_entry( + scope, + crate::context_bootstrap::ResourcePerformanceEntry::from_streaming_network_response( + streaming.pending.info.url.as_str(), + "xmlhttprequest", + None, + &streaming.head, + response_body_size, + ), + ); let xhr = v8::Local::new(scope, &xhr); let mut observable_head = streaming.head; if !streaming diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs index 7d6d09805a..b9e515061e 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/xhr.rs @@ -83,6 +83,71 @@ async fn same_origin_window_fetch_and_xhr_post_send_origin_on_wire() { } } +#[tokio::test(flavor = "current_thread")] +async fn asynchronous_window_xhr_records_resource_timing_with_xmlhttprequest_initiator() { + let server = StaticHttpServer::spawn(1).await; + let base_url = server.base_url(); + let loader = static_http_loader(std::iter::empty::()); + let mut vm = new_page_task_executor_test_vm_with_loader( + base_url + .join("page.html") + .expect("page fixture URL") + .as_str(), + &loader, + ); + + vm.eval( + r#" +(() => { + globalThis.__xhrResourceTimingProbe = "pending"; + const xhr = new XMLHttpRequest(); + xhr.open("GET", "xhr-resource"); + xhr.onload = () => { globalThis.__xhrResourceTimingProbe = "done"; }; + xhr.onerror = () => { globalThis.__xhrResourceTimingProbe = "error"; }; + xhr.send(); + return "started"; +})() +"#, + ) + .expect("XHR resource timing probe should evaluate"); + + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(globalThis.__xhrResourceTimingProbe)", + "done", + "XHR resource timing probe", + ) + .await; + + let entry = vm + .eval( + r#" +(() => { + const url = new URL("xhr-resource", location.href).href; + const entries = performance.getEntriesByName(url, "resource"); + return JSON.stringify(entries.map(entry => ({ + name: entry.name, + initiatorType: entry.initiatorType, + isResourceTiming: entry instanceof PerformanceResourceTiming + }))); +})() +"#, + ) + .expect("XHR resource timing entry should be readable"); + assert_eq!( + entry, + format!( + r#"[{{"name":"{}","initiatorType":"xmlhttprequest","isResourceTiming":true}}]"#, + base_url.join("xhr-resource").expect("XHR resource URL") + ) + ); + + let requests = server.finish().await; + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].target, "/xhr-resource"); +} + fn pending_fetch_continuation<'s>( scope: &mut v8::PinScope<'s, '_>, resolver: v8::Local<'s, v8::PromiseResolver>,