diff --git a/benches/bench.rs b/benches/bench.rs index 9f0f8ac..255bea9 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -29,6 +29,11 @@ fn citm_catalog_json_large_serde_value(b: &mut Bencher) { bench_serde_value(b, &get_citm_catalog_json_large()); } +#[bench] +fn key_heavy_json_value(b: &mut Bencher) { + bench_value(b, &get_key_heavy_json()); +} + #[bench] fn tsconfig_json_ast(b: &mut Bencher) { bench_ast(b, &get_tsconfig_json()); @@ -92,6 +97,21 @@ fn get_citm_catalog_json() -> String { read_to_string("benches/data/citm_catalog.json").unwrap() } +fn get_key_heavy_json() -> String { + // array of many small objects with short distinct string keys, to stress + // per-property object-key allocation on the value-building path + let mut result = String::new(); + result.push('['); + for i in 0..20_000 { + if i > 0 { + result.push(','); + } + result.push_str(r#"{"id":1,"name":"abc","kind":"widget","enabled":true,"count":42,"tag":"x"}"#); + } + result.push(']'); + result +} + fn get_tsconfig_json() -> String { read_to_string("benches/data/tsconfig.json").unwrap() } diff --git a/dprint.json b/dprint.json index d789501..645a1d4 100644 --- a/dprint.json +++ b/dprint.json @@ -13,8 +13,8 @@ "./benches/json" ], "plugins": [ - "https://plugins.dprint.dev/markdown-0.19.0.wasm", - "https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364", - "https://plugins.dprint.dev/json-0.20.0.wasm" + "https://plugins.dprint.dev/markdown-0.22.1.wasm", + "https://plugins.dprint.dev/exec-0.7.2.json@6c30bd00dfb176774ece412d0fbf149478269ecc92c55ab2d6f4116938ed993e", + "https://plugins.dprint.dev/json-0.22.0.wasm" ] } diff --git a/src/parse_to_value.rs b/src/parse_to_value.rs index cd73db3..3015714 100644 --- a/src/parse_to_value.rs +++ b/src/parse_to_value.rs @@ -58,7 +58,7 @@ fn parse_object<'a>(parser: &mut JsoncParser<'a>) -> Result, Parse None => break, Some(key) => { first = false; - let key_string = key.into_string(); + let key_string = key.into_cow(); parser.scan_object_colon()?; match parser.scan()? { Some(value_token) => { @@ -132,13 +132,13 @@ mod tests { .unwrap(); let mut object_map = Map::new(); - object_map.insert(String::from("a"), JsonValue::Null); + object_map.insert(Cow::Borrowed("a"), JsonValue::Null); object_map.insert( - String::from("b"), + Cow::Borrowed("b"), JsonValue::Array(vec![JsonValue::Null, JsonValue::String(Cow::Borrowed("text"))].into()), ); - object_map.insert(String::from("c"), JsonValue::Boolean(true)); - object_map.insert(String::from("d"), JsonValue::Number("25.55")); + object_map.insert(Cow::Borrowed("c"), JsonValue::Boolean(true)); + object_map.insert(Cow::Borrowed("d"), JsonValue::Number("25.55")); assert_eq!(value, JsonValue::Object(object_map.into())); } diff --git a/src/parser.rs b/src/parser.rs index 097bef3..f7e9b39 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -19,6 +19,15 @@ impl<'a> ObjectKey<'a> { ObjectKey::Word(s) => s.to_string(), } } + + /// Converts the key into a `Cow`, borrowing from the source when possible + /// to avoid an allocation for clean (unescaped) keys. + pub fn into_cow(self) -> Cow<'a, str> { + match self { + ObjectKey::String(s) => s, + ObjectKey::Word(s) => Cow::Borrowed(s), + } + } } /// Shared JSONC parser infrastructure used by both `parse_to_value` and diff --git a/src/value.rs b/src/value.rs index 64e279d..5499ff3 100644 --- a/src/value.rs +++ b/src/value.rs @@ -19,35 +19,35 @@ pub type Map = indexmap::IndexMap; /// A JSON object. #[derive(Clone, PartialEq, Debug)] -pub struct JsonObject<'a>(Map>); +pub struct JsonObject<'a>(Map, JsonValue<'a>>); impl<'a> IntoIterator for JsonObject<'a> { - type Item = (String, JsonValue<'a>); + type Item = (Cow<'a, str>, JsonValue<'a>); #[cfg(not(feature = "preserve_order"))] - type IntoIter = std::collections::hash_map::IntoIter>; + type IntoIter = std::collections::hash_map::IntoIter, JsonValue<'a>>; #[cfg(feature = "preserve_order")] - type IntoIter = indexmap::map::IntoIter>; + type IntoIter = indexmap::map::IntoIter, JsonValue<'a>>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } -impl<'a> From>> for JsonObject<'a> { - fn from(properties: Map) -> JsonObject { +impl<'a> From, JsonValue<'a>>> for JsonObject<'a> { + fn from(properties: Map, JsonValue<'a>>) -> JsonObject<'a> { JsonObject::new(properties) } } #[cfg(not(feature = "preserve_order"))] #[inline(always)] -fn remove_entry<'a>(map: &mut Map>, key: &str) -> Option<(String, JsonValue<'a>)> { +fn remove_entry<'a>(map: &mut Map, JsonValue<'a>>, key: &str) -> Option<(Cow<'a, str>, JsonValue<'a>)> { map.remove_entry(key) } #[cfg(feature = "preserve_order")] #[inline(always)] -fn remove_entry<'a>(map: &mut Map>, key: &str) -> Option<(String, JsonValue<'a>)> { +fn remove_entry<'a>(map: &mut Map, JsonValue<'a>>, key: &str) -> Option<(Cow<'a, str>, JsonValue<'a>)> { map.shift_remove_entry(key) } @@ -76,7 +76,7 @@ macro_rules! generate_get { impl<'a> JsonObject<'a> { /// Creates a new JsonObject. - pub fn new(inner: Map>) -> JsonObject<'a> { + pub fn new(inner: Map, JsonValue<'a>>) -> JsonObject<'a> { JsonObject(inner) } @@ -86,7 +86,7 @@ impl<'a> JsonObject<'a> { } /// Drops the object returning the inner map. - pub fn take_inner(self) -> Map> { + pub fn take_inner(self) -> Map, JsonValue<'a>> { self.0 } @@ -231,8 +231,8 @@ mod test { #[test] fn it_should_take() { let mut inner = Map::new(); - inner.insert(String::from("prop"), JsonValue::String(Cow::Borrowed("asdf"))); - inner.insert(String::from("other"), JsonValue::String(Cow::Borrowed("text"))); + inner.insert(Cow::Borrowed("prop"), JsonValue::String(Cow::Borrowed("asdf"))); + inner.insert(Cow::Borrowed("other"), JsonValue::String(Cow::Borrowed("text"))); let mut obj = JsonObject::new(inner); assert_eq!(obj.len(), 2); @@ -251,7 +251,7 @@ mod test { #[test] fn it_should_get() { let mut inner = Map::new(); - inner.insert(String::from("prop"), JsonValue::String(Cow::Borrowed("asdf"))); + inner.insert(Cow::Borrowed("prop"), JsonValue::String(Cow::Borrowed("asdf"))); let obj = JsonObject::new(inner); assert_eq!(obj.len(), 1);