From 2edf3f9b284f88f0e26bb4dc3899ba6f74821ce0 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:02:32 +0800 Subject: [PATCH 01/10] perf(macOS): avoid copying large protocol bodies --- .changes/macos-protocol-body-nocopy.md | 5 ++++ Cargo.lock | 1 + Cargo.toml | 1 + src/wkwebview/class/url_scheme_handler.rs | 32 ++++++++++++++++------- 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 .changes/macos-protocol-body-nocopy.md diff --git a/.changes/macos-protocol-body-nocopy.md b/.changes/macos-protocol-body-nocopy.md new file mode 100644 index 000000000..59f6734e3 --- /dev/null +++ b/.changes/macos-protocol-body-nocopy.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +On macOS, avoid an extra copy for owned custom protocol response bodies of 128KB or larger by transferring the body buffer into `NSData`. diff --git a/Cargo.lock b/Cargo.lock index 7ca4fc451..5071efdc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1944,6 +1944,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ "bitflags 2.8.0", + "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", ] diff --git a/Cargo.toml b/Cargo.toml index 30996c574..f99618f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ objc2-core-foundation = { version = "0.3.0", default-features = false, features ] } objc2-foundation = { version = "0.3.0", default-features = false, features = [ "std", + "block2", "objc2-core-foundation", "NSURLRequest", "NSURL", diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index 254bbb65f..58caf2edf 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -26,6 +26,8 @@ use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask}; use crate::{wkwebview::WEBVIEW_STATE, RequestAsyncResponder, WryWebView}; +const NO_COPY_DATA_THRESHOLD: usize = 128 * 1024; + pub fn create(name: &str) -> &AnyClass { unsafe { // Include the address of WEBVIEW_STATE in the class name so that each dylib in the process @@ -215,6 +217,7 @@ extern "C" fn start_task( check_task_is_valid(&webview, task_key, task_uuid.clone())?; let content = sent_response.body(); + let content_len = content.len(); // default: application/octet-stream, but should be provided by the client let wanted_mime = sent_response.headers().get(CONTENT_TYPE); // default to 200 @@ -231,7 +234,7 @@ extern "C" fn start_task( } headers.insert( &*NSString::from_str(CONTENT_LENGTH.as_str()), - &*NSString::from_str(&content.len().to_string()), + &*NSString::from_str(&content_len.to_string()), ); // add headers @@ -264,15 +267,24 @@ extern "C" fn start_task( })) .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; - // Send data - let data = NSData::alloc(); - // MIGRATE NOTE: we copied the content to the NSData because content will be freed - // when out of scope but NSData will also free the content when it's done and cause doube free. - let data = NSData::initWithBytes_length( - data, - content.as_ptr() as *mut c_void, - content.len(), - ); + let data = if content_len < NO_COPY_DATA_THRESHOLD { + let data = NSData::alloc(); + // Keep small responses on the original copy path; no-copy deallocation costs more. + NSData::initWithBytes_length(data, content.as_ptr() as *mut c_void, content.len()) + } else { + match sent_response.into_body() { + Cow::Owned(content) => NSData::from_vec(content), + Cow::Borrowed(content) => { + let data = NSData::alloc(); + // Copy borrowed responses because NSData cannot take ownership. + NSData::initWithBytes_length( + data, + content.as_ptr() as *mut c_void, + content.len(), + ) + } + } + }; // Check validity again check_webview_id_valid(webview_id)?; From 3042ef1da2aab4a42c55ee3aebb14b1f81946832 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 9 May 2026 01:42:36 +0800 Subject: [PATCH 02/10] refatcor: mirage `NSData::initWithBytes_length` to `NSData::with_bytes` --- src/wkwebview/class/url_scheme_handler.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index 58caf2edf..3f9c22e10 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -4,7 +4,7 @@ use std::{ borrow::Cow, - ffi::{c_char, c_void, CStr}, + ffi::{c_char, CStr}, panic::AssertUnwindSafe, ptr::NonNull, }; @@ -268,20 +268,14 @@ extern "C" fn start_task( .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; let data = if content_len < NO_COPY_DATA_THRESHOLD { - let data = NSData::alloc(); // Keep small responses on the original copy path; no-copy deallocation costs more. - NSData::initWithBytes_length(data, content.as_ptr() as *mut c_void, content.len()) + NSData::with_bytes(content) } else { match sent_response.into_body() { Cow::Owned(content) => NSData::from_vec(content), Cow::Borrowed(content) => { - let data = NSData::alloc(); // Copy borrowed responses because NSData cannot take ownership. - NSData::initWithBytes_length( - data, - content.as_ptr() as *mut c_void, - content.len(), - ) + NSData::with_bytes(content) } } }; From 86243bceb7020c5d5a7b05e21fc582939da6c77a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sat, 9 May 2026 15:05:37 +0800 Subject: [PATCH 03/10] Refine WKURLSchemeTask body handling --- src/wkwebview/class/url_scheme_handler.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index 3f9c22e10..8541254f7 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -216,8 +216,7 @@ extern "C" fn start_task( check_webview_id_valid(webview_id)?; check_task_is_valid(&webview, task_key, task_uuid.clone())?; - let content = sent_response.body(); - let content_len = content.len(); + let content_len = sent_response.body().len(); // default: application/octet-stream, but should be provided by the client let wanted_mime = sent_response.headers().get(CONTENT_TYPE); // default to 200 @@ -267,11 +266,12 @@ extern "C" fn start_task( })) .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; + let content = sent_response.into_body(); let data = if content_len < NO_COPY_DATA_THRESHOLD { // Keep small responses on the original copy path; no-copy deallocation costs more. - NSData::with_bytes(content) + NSData::with_bytes(content.as_ref()) } else { - match sent_response.into_body() { + match content { Cow::Owned(content) => NSData::from_vec(content), Cow::Borrowed(content) => { // Copy borrowed responses because NSData cannot take ownership. From 589e5baeda4e426f2a943b9a13645d5f690a9680 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 11 May 2026 21:52:29 +0800 Subject: [PATCH 04/10] chore: comment on optimize buffer handling in start_task to reduce copying for larger responses --- src/wkwebview/class/url_scheme_handler.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index 8541254f7..be8051728 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -267,6 +267,9 @@ extern "C" fn start_task( .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; let content = sent_response.into_body(); + // It yields benefits by eliminating buffer copying starting from 88 bytes. + // However, sizes below 88 bytes cause regressions. 128 bytes as + // it aligns better with engineering standards and is more human-readable. let data = if content_len < NO_COPY_DATA_THRESHOLD { // Keep small responses on the original copy path; no-copy deallocation costs more. NSData::with_bytes(content.as_ref()) From 343eedce0f0c34a96f15dcf685dae6442100fc7b Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 13 May 2026 00:30:44 +0800 Subject: [PATCH 05/10] perf: reduce NO_COPY_DATA_THRESHOLD to 64 KB for improved buffer handling --- src/wkwebview/class/url_scheme_handler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index be8051728..ed4e72f0d 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -26,7 +26,7 @@ use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask}; use crate::{wkwebview::WEBVIEW_STATE, RequestAsyncResponder, WryWebView}; -const NO_COPY_DATA_THRESHOLD: usize = 128 * 1024; +const NO_COPY_DATA_THRESHOLD: usize = 64 * 1024; pub fn create(name: &str) -> &AnyClass { unsafe { @@ -267,8 +267,8 @@ extern "C" fn start_task( .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; let content = sent_response.into_body(); - // It yields benefits by eliminating buffer copying starting from 88 bytes. - // However, sizes below 88 bytes cause regressions. 128 bytes as + // It yields benefits by eliminating buffer copying starting from 64 KB. + // However, sizes below 64 bytes cause regressions(or noise). 64 KB as // it aligns better with engineering standards and is more human-readable. let data = if content_len < NO_COPY_DATA_THRESHOLD { // Keep small responses on the original copy path; no-copy deallocation costs more. From 226a006c07dcd7371b526040ffe4969f529c19d2 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 13 May 2026 00:35:44 +0800 Subject: [PATCH 06/10] perf: increase NO_COPY_DATA_THRESHOLD to 128 KB for improved buffer handling --- src/wkwebview/class/url_scheme_handler.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index ed4e72f0d..fb8b290aa 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -26,7 +26,7 @@ use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask}; use crate::{wkwebview::WEBVIEW_STATE, RequestAsyncResponder, WryWebView}; -const NO_COPY_DATA_THRESHOLD: usize = 64 * 1024; +const NO_COPY_DATA_THRESHOLD: usize = 128 * 1024; pub fn create(name: &str) -> &AnyClass { unsafe { @@ -267,8 +267,8 @@ extern "C" fn start_task( .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; let content = sent_response.into_body(); - // It yields benefits by eliminating buffer copying starting from 64 KB. - // However, sizes below 64 bytes cause regressions(or noise). 64 KB as + // It yields benefits by eliminating buffer copying starting from 128 KB. + // However, sizes below 64 bytes cause regressions(or noise). 128 KB as // it aligns better with engineering standards and is more human-readable. let data = if content_len < NO_COPY_DATA_THRESHOLD { // Keep small responses on the original copy path; no-copy deallocation costs more. From 327cb7b470041b246ee6f2f8b64ba3aa440226f6 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Wed, 13 May 2026 18:04:07 +0800 Subject: [PATCH 07/10] update url_scheme_handler.rs Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com> --- src/wkwebview/class/url_scheme_handler.rs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index fb8b290aa..4f4a6f41f 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -266,21 +266,9 @@ extern "C" fn start_task( })) .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; - let content = sent_response.into_body(); - // It yields benefits by eliminating buffer copying starting from 128 KB. - // However, sizes below 64 bytes cause regressions(or noise). 128 KB as - // it aligns better with engineering standards and is more human-readable. - let data = if content_len < NO_COPY_DATA_THRESHOLD { - // Keep small responses on the original copy path; no-copy deallocation costs more. - NSData::with_bytes(content.as_ref()) - } else { - match content { + let data = match sent_response.into_body() { Cow::Owned(content) => NSData::from_vec(content), - Cow::Borrowed(content) => { - // Copy borrowed responses because NSData cannot take ownership. - NSData::with_bytes(content) - } - } + Cow::Borrowed(content) => NSData::with_bytes(content), }; // Check validity again From 7203fde469e3aa75944d05e16baa149a7b9d4b03 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Wed, 13 May 2026 18:06:17 +0800 Subject: [PATCH 08/10] update macos-protocol-body-nocopy.md --- .changes/macos-protocol-body-nocopy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changes/macos-protocol-body-nocopy.md b/.changes/macos-protocol-body-nocopy.md index 59f6734e3..8774ab1c7 100644 --- a/.changes/macos-protocol-body-nocopy.md +++ b/.changes/macos-protocol-body-nocopy.md @@ -2,4 +2,4 @@ "wry": patch --- -On macOS, avoid an extra copy for owned custom protocol response bodies of 128KB or larger by transferring the body buffer into `NSData`. +On macOS, avoid an extra copy for owned custom protocol response bodies by transferring the body buffer into `NSData`. From 611f13068733ba2d926d393e14213ec52df12b52 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 13 May 2026 18:19:05 +0800 Subject: [PATCH 09/10] cargo fmt --- src/wkwebview/class/url_scheme_handler.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index 4f4a6f41f..c203dcae4 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -267,8 +267,8 @@ extern "C" fn start_task( .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?; let data = match sent_response.into_body() { - Cow::Owned(content) => NSData::from_vec(content), - Cow::Borrowed(content) => NSData::with_bytes(content), + Cow::Owned(content) => NSData::from_vec(content), + Cow::Borrowed(content) => NSData::with_bytes(content), }; // Check validity again From 6a3aa797ead6a0c985cdf5f57305c1310d4ec5d8 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 13 May 2026 18:19:40 +0800 Subject: [PATCH 10/10] Remove unused `NO_COPY_DATA_THRESHOLD` --- src/wkwebview/class/url_scheme_handler.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs index c203dcae4..c280b84c1 100644 --- a/src/wkwebview/class/url_scheme_handler.rs +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -26,8 +26,6 @@ use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask}; use crate::{wkwebview::WEBVIEW_STATE, RequestAsyncResponder, WryWebView}; -const NO_COPY_DATA_THRESHOLD: usize = 128 * 1024; - pub fn create(name: &str) -> &AnyClass { unsafe { // Include the address of WEBVIEW_STATE in the class name so that each dylib in the process