mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 12:52:13 +02:00
perf(extensions): reuse owned UTF-8 buffers at text boundaries
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user