perf(extensions): reuse owned UTF-8 buffers at text boundaries

This commit is contained in:
zarzet
2026-09-17 18:20:27 +07:00
parent a9a395b9f8
commit e984c1ddba
3 changed files with 105 additions and 7 deletions
@@ -107,7 +107,7 @@ fn call<'js>(
return Err(READ_LIMIT_ERROR.into());
}
result
.set("data", crate::host::decode_go_utf8(&bytes))
.set("data", crate::host::decode_go_utf8_owned(bytes))
.map_err(js_error)?;
} else {
input
+59 -5
View File
@@ -160,17 +160,14 @@ pub(crate) fn register<'js>(
}
})
.unwrap_or_default();
decode_go_utf8(&decoded)
decode_go_utf8_owned(decoded)
})?,
)?;
host.set(
"encode",
Function::new(ctx.clone(), |value: String| value.into_bytes())?,
)?;
host.set(
"decode",
Function::new(ctx.clone(), |value: Vec<u8>| decode_go_utf8(&value))?,
)?;
host.set("decode", Function::new(ctx.clone(), decode_go_utf8_owned)?)?;
host.set(
"md5",
Function::new(ctx.clone(), |value: String| {
@@ -250,6 +247,15 @@ fn storage_result(result: Result<(), StorageError>) -> String {
// Go's UTF-8 decoder consumes one byte for every invalid rune. Rust's lossy
// conversion groups some invalid prefixes, which would change extension text.
pub(crate) fn decode_go_utf8_owned(bytes: Vec<u8>) -> String {
// Text responses/files already own their buffer. Reuse it for valid UTF-8
// instead of holding a second full copy while QuickJS creates its string.
match String::from_utf8(bytes) {
Ok(text) => text,
Err(error) => decode_go_utf8(error.as_bytes()),
}
}
pub(crate) fn decode_go_utf8(mut bytes: &[u8]) -> String {
let mut decoded = String::with_capacity(bytes.len());
while !bytes.is_empty() {
@@ -272,6 +278,54 @@ pub(crate) fn decode_go_utf8(mut bytes: &[u8]) -> String {
#[cfg(test)]
mod tests {
#[test]
#[ignore = "manual release-process peak-memory measurement"]
fn benchmark_owned_text_memory() {
let bytes = vec![b'x'; spotiflac_network::MAX_RESPONSE_BYTES];
match std::env::var("SPOTIFLAC_TEXT_MEMORY_MODE").as_deref() {
Ok("copy") => {
let text = super::decode_go_utf8(&bytes);
assert_eq!(text.len(), bytes.len());
std::hint::black_box((&bytes, &text));
}
Ok("reuse") => {
let pointer = bytes.as_ptr();
let text = super::decode_go_utf8_owned(bytes);
assert_eq!(text.as_ptr(), pointer);
assert_eq!(text.len(), spotiflac_network::MAX_RESPONSE_BYTES);
std::hint::black_box(&text);
}
_ => panic!("set SPOTIFLAC_TEXT_MEMORY_MODE to copy or reuse"),
}
}
#[test]
fn owned_text_reuses_large_buffers_and_preserves_invalid_byte_decoding() {
let bytes = "Music 音楽 🎵\0".repeat(128 * 1024).into_bytes();
let pointer = bytes.as_ptr();
let capacity = bytes.capacity();
let text = super::decode_go_utf8_owned(bytes);
assert_eq!(text.as_ptr(), pointer);
assert_eq!(text.capacity(), capacity);
assert_eq!(text, "Music 音楽 🎵\0".repeat(128 * 1024));
for bytes in [
&b""[..],
b"a\xffb",
b"\xe2\x82",
b"\xed\xa0\x80",
b"\xf0\x9f\x8e\xb5\0\xff",
] {
assert_eq!(
super::decode_go_utf8_owned(bytes.to_vec()),
super::decode_go_utf8(bytes),
);
}
assert_eq!(
super::decode_go_utf8_owned(vec![0xe2, 0x82]),
"\u{fffd}\u{fffd}"
);
}
#[test]
fn digest_upgrade_preserves_extension_hash_and_hmac_encodings() {
let runtime = crate::ExtensionRuntime::load(
@@ -172,7 +172,7 @@ fn response_object<'js>(
object.set("bytes", TypedArray::new_copy(ctx.clone(), response.body)?)?;
} else {
object.set("statusCode", response.status)?;
object.set("body", decode_go_utf8(&response.body))?;
object.set("body", crate::host::decode_go_utf8_owned(response.body))?;
}
Ok(object)
}
@@ -192,3 +192,47 @@ fn encode_buffer(bytes: &TypedArray<'_, u8>) -> String {
let bytes = unsafe { bytes.as_bytes() }.unwrap_or_default();
base64::engine::general_purpose::STANDARD.encode(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_response_preserves_status_headers_unicode_and_invalid_bytes() {
let runtime = rquickjs::Runtime::new().unwrap();
let context = rquickjs::Context::full(&runtime).unwrap();
context.with(|ctx| {
for body in ["Music 音楽 🎵\0".as_bytes(), &b"a\xe2\x82b"[..]] {
let response = response_object(
&ctx,
HttpResponse {
status: 201,
status_text: "Created".into(),
url: "https://example.invalid/metadata".into(),
headers: BTreeMap::from([(
"Content-Type".into(),
vec!["text/plain".into()],
)]),
body: body.to_vec(),
},
false,
)
.unwrap();
assert_eq!(
response.get::<_, String>("body").unwrap(),
decode_go_utf8(body)
);
assert_eq!(response.get::<_, u16>("statusCode").unwrap(), 201);
assert!(response.get::<_, bool>("ok").unwrap());
assert_eq!(
response
.get::<_, Object>("headers")
.unwrap()
.get::<_, String>("Content-Type")
.unwrap(),
"text/plain",
);
}
});
}
}