mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 12:52:13 +02:00
perf(extensions): validate JSON without allocating argument trees
This commit is contained in:
@@ -6,6 +6,8 @@ use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
mod json_input;
|
||||
|
||||
const MAX_INPUT_BYTES: usize = 8 * 1024 * 1024;
|
||||
const MAX_TIMEOUT_MS: u64 = 300_000;
|
||||
const QUEUE_CAPACITY: usize = 8;
|
||||
@@ -1010,9 +1012,7 @@ impl Vm {
|
||||
context,
|
||||
_runtime: runtime,
|
||||
};
|
||||
let empty_settings = validate_json(settings)?
|
||||
.as_object()
|
||||
.is_some_and(|settings| settings.is_empty());
|
||||
let empty_settings = validate_json(settings)?.is_empty_object();
|
||||
if services.load_mode == LoadMode::Initialize
|
||||
&& (services.initialize_empty_settings || !empty_settings)
|
||||
{
|
||||
@@ -1218,7 +1218,7 @@ fn validate_size(value: &str) -> Result<(), ExtensionError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_json(value: &str) -> Result<serde_json::Value, ExtensionError> {
|
||||
fn validate_json(value: &str) -> Result<json_input::Shape, ExtensionError> {
|
||||
validate_size(value)?;
|
||||
serde_json::from_str(value).map_err(|error| ExtensionError::InvalidInput(error.to_string()))
|
||||
}
|
||||
@@ -1227,6 +1227,28 @@ fn validate_json(value: &str) -> Result<serde_json::Value, ExtensionError> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn argument_validation_rejects_before_execution_and_keeps_runtime_usable() {
|
||||
let runtime = ExtensionRuntime::load(
|
||||
"let calls=0; registerExtension({echo(value){return {calls:++calls,value}}});",
|
||||
"{}",
|
||||
RuntimeLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
for input in ["{}", "[1e400]", "[\"\\uD800\"]", "[] true"] {
|
||||
assert!(matches!(
|
||||
runtime.call("echo", input, None, 0),
|
||||
Err(ExtensionError::InvalidInput(_))
|
||||
));
|
||||
}
|
||||
let input = "[{\"title\":\"音楽 🎵\",\"nested\":[true,null,{\"key\":\"value\"}]}]";
|
||||
let output: serde_json::Value =
|
||||
serde_json::from_str(&runtime.call("echo", input, None, 0).unwrap()).unwrap();
|
||||
assert_eq!(output["calls"], 1);
|
||||
assert_eq!(output["value"]["title"], "音楽 🎵");
|
||||
assert_eq!(output["value"]["nested"][2]["key"], "value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_source_still_obeys_each_vms_memory_and_stack_limits() {
|
||||
let source = "registerExtension({allocate(){const a=[];while(true)a.push(new Uint8Array(65536));},recurse(){function f(){return 1+f()}return f()}});";
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Validate caller JSON without retaining a second object tree before QuickJS
|
||||
//! parses it. Use normal Serde visits rather than IgnoredAny/RawValue so number
|
||||
//! range, string escape, nesting, and trailing-data errors remain unchanged.
|
||||
|
||||
use serde::de::{Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum Shape {
|
||||
Scalar,
|
||||
Array,
|
||||
Object { empty: bool },
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub(super) fn is_array(&self) -> bool {
|
||||
matches!(self, Self::Array)
|
||||
}
|
||||
|
||||
pub(super) fn is_object(&self) -> bool {
|
||||
matches!(self, Self::Object { .. })
|
||||
}
|
||||
|
||||
pub(super) fn is_empty_object(&self) -> bool {
|
||||
matches!(self, Self::Object { empty: true })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Shape {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
struct ShapeVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for ShapeVisitor {
|
||||
type Value = Shape;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("any valid JSON value")
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, _: bool) -> Result<Shape, E> {
|
||||
Ok(Shape::Scalar)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, _: i64) -> Result<Shape, E> {
|
||||
Ok(Shape::Scalar)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, _: u64) -> Result<Shape, E> {
|
||||
Ok(Shape::Scalar)
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, _: f64) -> Result<Shape, E> {
|
||||
Ok(Shape::Scalar)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, _: &str) -> Result<Shape, E> {
|
||||
Ok(Shape::Scalar)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Shape, E> {
|
||||
Ok(Shape::Scalar)
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, mut values: A) -> Result<Shape, A::Error> {
|
||||
while values.next_element::<Shape>()?.is_some() {}
|
||||
Ok(Shape::Array)
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut values: A) -> Result<Shape, A::Error> {
|
||||
let mut empty = true;
|
||||
while values.next_entry::<Shape, Shape>()?.is_some() {
|
||||
empty = false;
|
||||
}
|
||||
Ok(Shape::Object { empty })
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(ShapeVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn compare(input: &str) {
|
||||
let expected = serde_json::from_str::<serde_json::Value>(input);
|
||||
let actual = serde_json::from_str::<Shape>(input);
|
||||
match (expected, actual) {
|
||||
(Ok(value), Ok(shape)) => {
|
||||
assert_eq!(shape.is_array(), value.is_array(), "{input}");
|
||||
assert_eq!(shape.is_object(), value.is_object(), "{input}");
|
||||
assert_eq!(
|
||||
shape.is_empty_object(),
|
||||
value.as_object().is_some_and(|value| value.is_empty()),
|
||||
"{input}",
|
||||
);
|
||||
}
|
||||
(Err(expected), Err(actual)) => {
|
||||
assert_eq!(actual.to_string(), expected.to_string(), "{input}");
|
||||
}
|
||||
(expected, actual) => {
|
||||
panic!("validation differs for {input:?}: {expected:?} / {actual:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_matches_value_shapes_numbers_unicode_and_errors() {
|
||||
for input in [
|
||||
"null",
|
||||
"true",
|
||||
"false",
|
||||
"0",
|
||||
"-0",
|
||||
"1.25e-100",
|
||||
"1e400",
|
||||
"[1e400,0]",
|
||||
"{\"x\":1e400,\"x\":0}",
|
||||
"18446744073709551615",
|
||||
"18446744073709551616",
|
||||
"-9223372036854775809",
|
||||
"NaN",
|
||||
"Infinity",
|
||||
"[]",
|
||||
"{}",
|
||||
"{\"x\":null}",
|
||||
" { }\n",
|
||||
"[1,{\"a\":[true,null,\"text\"]}]",
|
||||
"\"音楽 🎵\"",
|
||||
"\"\\uD83C\\uDFB5\"",
|
||||
"\"\\uD800\"",
|
||||
"\"\\uDC00\"",
|
||||
"\"\\x20\"",
|
||||
"\"line\nfeed\"",
|
||||
"{\"a\\tb\":\"\\u0000\"}",
|
||||
"{\"a\":1,\"a\":2}",
|
||||
"{1:true}",
|
||||
"[01]",
|
||||
"[1,]",
|
||||
"[] true",
|
||||
"[",
|
||||
"{\"x\":",
|
||||
"",
|
||||
] {
|
||||
compare(input);
|
||||
}
|
||||
for depth in [1, 100, 127, 128, 129] {
|
||||
compare(&format!("{}0{}", "[".repeat(depth), "]".repeat(depth)));
|
||||
compare(&format!(
|
||||
"{}0{}",
|
||||
"{\"key\":".repeat(depth),
|
||||
"}".repeat(depth)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_matches_value_for_truncated_and_mutated_payloads() {
|
||||
let input = r#"[{"track":"a\\b\n\uD83C\uDFB5","options":{"count":123.4e-3,"ids":[1,2,3],"ok":true,"nil":null}}]"#;
|
||||
for index in 0..input.len() {
|
||||
compare(&input[..index]);
|
||||
for &replacement in b" \"\\0,]}" {
|
||||
let mut bytes = input.as_bytes().to_vec();
|
||||
bytes[index] = replacement;
|
||||
compare(std::str::from_utf8(&bytes).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "manual release benchmark: JSON validation only, not device latency"]
|
||||
fn benchmark_shape_validation() {
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
for count in [1, 100, 1000, 10000] {
|
||||
let row = serde_json::json!({"id":"track-123","title":"音楽 🎵","artists":["Artist"],"duration_ms":240000,"options":{"quality":"lossless","enabled":true}});
|
||||
let input = serde_json::json!([vec![row; count]]).to_string();
|
||||
let mut old = Vec::new();
|
||||
let mut new = Vec::new();
|
||||
for round in 0..44 {
|
||||
for baseline in if round % 2 == 0 {
|
||||
[true, false]
|
||||
} else {
|
||||
[false, true]
|
||||
} {
|
||||
let start = Instant::now();
|
||||
if baseline {
|
||||
black_box(
|
||||
serde_json::from_str::<serde_json::Value>(black_box(&input)).unwrap(),
|
||||
);
|
||||
} else {
|
||||
black_box(serde_json::from_str::<Shape>(black_box(&input)).unwrap());
|
||||
}
|
||||
if round >= 4 {
|
||||
(if baseline { &mut old } else { &mut new })
|
||||
.push(start.elapsed().as_nanos());
|
||||
}
|
||||
}
|
||||
}
|
||||
old.sort_unstable();
|
||||
new.sort_unstable();
|
||||
println!(
|
||||
"validation rows={count} bytes={} samples=40 old_median_ns={} new_median_ns={} old_p95_ns={} new_p95_ns={}",
|
||||
input.len(),
|
||||
old[20],
|
||||
new[20],
|
||||
old[37],
|
||||
new[37]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user