From e32030e484721cd6bfdb4f531855ba58dabca04b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 10 Sep 2026 16:07:05 +0800 Subject: [PATCH] perf(fetch): avoid copying response bytes for module consumers Use byte-only decomposition for Response and NavigationResponse instead of discarding the text returned by into_parts(). Module, dedicated worker, and service worker consumers now transfer the existing exact byte allocation. Verify storage transfer for UTF-8 and non-UTF-8 payloads. --- moli-fetch/src/response.rs | 10 +++++++ moli-fetch/src/tests/mod.rs | 29 ++++++++++++++++++ moli-page-types/src/lib.rs | 30 +++++++++++++++++++ moli-renderer-v8/src/module_runtime/graph.rs | 2 +- .../service_worker_runtime/script_loading.rs | 2 +- moli-renderer-v8/src/worker/thread/mod.rs | 2 +- 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/moli-fetch/src/response.rs b/moli-fetch/src/response.rs index 4b8b4c9f99..05656a27f3 100644 --- a/moli-fetch/src/response.rs +++ b/moli-fetch/src/response.rs @@ -419,6 +419,16 @@ impl Response { (head, body) } + /// Consumes a materialized response and transfers its exact byte payload + /// without retaining the text view or copying UTF-8 text storage. + pub fn into_byte_parts(self) -> (ResponseHead, Vec) { + let (head, body) = self.into_body(); + let bytes = body + .try_into_materialized_bytes() + .expect("Response body should remain materialized"); + (head, bytes) + } + pub fn into_body(self) -> (ResponseHead, ResponseBody) { let head = ResponseHead { final_url: self.final_url, diff --git a/moli-fetch/src/tests/mod.rs b/moli-fetch/src/tests/mod.rs index cc2df9f15e..43595438fb 100644 --- a/moli-fetch/src/tests/mod.rs +++ b/moli-fetch/src/tests/mod.rs @@ -288,6 +288,35 @@ fn response_body_keeps_invalid_utf8_bytes_beside_lossy_text() { )); } +#[test] +fn response_byte_parts_transfer_utf8_storage_without_copying() { + let head = sample_response_head(); + let bytes = b"module source".to_vec(); + let storage = bytes.as_ptr(); + let response = Response::from_head_and_lossy_body_bytes(head.clone(), bytes); + + let (split_head, bytes) = response.into_byte_parts(); + + assert_eq!(split_head.final_url, head.final_url); + assert_eq!(split_head.status, head.status); + assert_eq!(split_head.headers, head.headers); + assert_eq!(bytes, b"module source"); + assert_eq!(bytes.as_ptr(), storage, "UTF-8 storage must be transferred"); +} + +#[test] +fn response_byte_parts_preserve_exact_non_utf8_storage() { + let bytes = vec![b'a', 0xff, b'b']; + let storage = bytes.as_ptr(); + let response = Response::from_head_and_lossy_body_bytes(sample_response_head(), bytes); + assert_eq!(response.body_text(), "a\u{fffd}b"); + + let (_, bytes) = response.into_byte_parts(); + + assert_eq!(bytes, [b'a', 0xff, b'b']); + assert_eq!(bytes.as_ptr(), storage, "exact bytes must be transferred"); +} + #[tokio::test] async fn response_body_materializes_streaming_text_source() { let (body_tx, body_rx) = mpsc::unbounded_channel(); diff --git a/moli-page-types/src/lib.rs b/moli-page-types/src/lib.rs index 901ca66480..5a536828b9 100644 --- a/moli-page-types/src/lib.rs +++ b/moli-page-types/src/lib.rs @@ -492,6 +492,16 @@ impl NavigationResponse { (head, body) } + /// Consumes a materialized response and transfers its exact byte payload + /// without retaining the text view or copying UTF-8 text storage. + pub fn into_byte_parts(self) -> (ResponseHead, Vec) { + let (head, body) = self.into_body(); + let bytes = body + .try_into_materialized_bytes() + .expect("NavigationResponse body should remain materialized"); + (head, bytes) + } + pub fn into_body(self) -> (ResponseHead, ResponseBody) { let head = ResponseHead { final_url: self.final_url, @@ -3419,6 +3429,26 @@ mod tests { Url::parse(&format!("https://example.test{path}")).expect("test URL should parse") } + #[test] + fn navigation_response_byte_parts_transfer_exact_storage() { + let template = + NavigationResponse::from_text_body(test_url("/module"), 200, Vec::new(), String::new()); + for bytes in [b"module source".to_vec(), vec![b'a', 0xff, b'b']] { + let expected = bytes.clone(); + let storage = bytes.as_ptr(); + let response = NavigationResponse::from_head_and_materialized_body( + template.head(), + ResponseBody::lossy_text_from_bytes(bytes), + ); + + let (head, bytes) = response.into_byte_parts(); + + assert_eq!(head.final_url, template.final_url); + assert_eq!(bytes, expected); + assert_eq!(bytes.as_ptr(), storage, "exact bytes must be transferred"); + } + } + #[test] fn script_execution_report_exposes_globals_snapshot_freshness() { let mut report = ScriptExecutionReport::default(); diff --git a/moli-renderer-v8/src/module_runtime/graph.rs b/moli-renderer-v8/src/module_runtime/graph.rs index d4f014c35a..d082d74c57 100644 --- a/moli-renderer-v8/src/module_runtime/graph.rs +++ b/moli-renderer-v8/src/module_runtime/graph.rs @@ -468,7 +468,7 @@ impl NativeModuleGraphFetchRequest { crate::referrer_policy::response_referrer_policy_from_headers( &response.headers, ); - let (head, _, body_bytes) = response.into_parts(); + let (head, body_bytes) = response.into_byte_parts(); crate::network_host::validate_cors_response_chain( &initiator_url, &head, diff --git a/moli-renderer-v8/src/service_worker_runtime/script_loading.rs b/moli-renderer-v8/src/service_worker_runtime/script_loading.rs index 321761c5b5..1def35185a 100644 --- a/moli-renderer-v8/src/service_worker_runtime/script_loading.rs +++ b/moli-renderer-v8/src/service_worker_runtime/script_loading.rs @@ -359,7 +359,7 @@ fn load_imported_script_resource_for_update_check( moli_fetch::ensure_http_status_success(response.final_url.as_str(), response.status, false) .map_err(|error| error.to_string())?; ensure_imported_script_resource_mime(kind, &response)?; - let (head, _body, body_bytes) = response.into_parts(); + let (head, body_bytes) = response.into_byte_parts(); let mut resource = ServiceWorkerScriptResource::from_response_parts( request_url_without_fragment, &head, diff --git a/moli-renderer-v8/src/worker/thread/mod.rs b/moli-renderer-v8/src/worker/thread/mod.rs index 2b93f6a3b6..d7805b1966 100644 --- a/moli-renderer-v8/src/worker/thread/mod.rs +++ b/moli-renderer-v8/src/worker/thread/mod.rs @@ -757,7 +757,7 @@ fn start_worker_module_graph_fetch( .elapsed() .as_millis() .min(u64::MAX as u128) as u64; - let (head, _body, body_bytes) = response.into_parts(); + let (head, body_bytes) = response.into_byte_parts(); let response_referrer_policy = crate::referrer_policy::response_referrer_policy_from_headers(&head.headers); let resource = WorkerScriptResource::from_response_parts(