Skip to content

Commit 32bbbba

Browse files
committed
perf(json): index large replacer key sets
1 parent 96a3bf2 commit 32bbbba

4 files changed

Lines changed: 120 additions & 11 deletions

File tree

json/json.mbt

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,20 @@ priv enum WriteFrame {
122122
///
123123
/// Only applies to object properties, not array elements.
124124
pub struct Replacer {
125-
priv f : (String, Json) -> Json?
126-
} derive(@debug.Debug)
125+
priv kind : ReplacerKind
126+
}
127+
128+
///|
129+
priv enum ReplacerKind {
130+
Custom((String, Json) -> Json?)
131+
Keep(ArrayView[StringView])
132+
Exclude(ArrayView[StringView])
133+
}
134+
135+
///|
136+
pub impl @debug.Debug for Replacer with fn to_repr(_) {
137+
@debug.Repr::record(Map([("f", @debug.Repr::literal("<function: ...>"))]))
138+
}
127139

128140
///|
129141
/// Create a new Replacer with a custom function.
@@ -153,7 +165,7 @@ pub struct Replacer {
153165
/// ```
154166
#alias(new, deprecated="Use `Replacer()` instead")
155167
pub fn Replacer::Replacer(f : (String, Json) -> Json?) -> Replacer {
156-
{ f, }
168+
{ kind: Custom(f) }
157169
}
158170

159171
///|
@@ -175,7 +187,7 @@ pub fn Replacer::Replacer(f : (String, Json) -> Json?) -> Replacer {
175187
/// }
176188
/// ```
177189
pub fn Replacer::keep(array : ArrayView[StringView]) -> Replacer {
178-
{ f: (idx, value) => if array.contains(idx) { Some(value) } else { None } }
190+
{ kind: Keep(array) }
179191
}
180192

181193
///|
@@ -197,7 +209,34 @@ pub fn Replacer::keep(array : ArrayView[StringView]) -> Replacer {
197209
/// }
198210
/// ```
199211
pub fn Replacer::exclude(array : ArrayView[StringView]) -> Replacer {
200-
{ f: (idx, value) => if array.contains(idx) { None } else { Some(value) } }
212+
{ kind: Exclude(array) }
213+
}
214+
215+
///|
216+
fn Replacer::prepare(self : Replacer) -> (String, Json) -> Json? {
217+
match self.kind {
218+
Custom(f) => f
219+
Keep(array) =>
220+
if array.length() <= 8 {
221+
(idx, value) => if array.contains(idx) { Some(value) } else { None }
222+
} else {
223+
let keys : Map[String, Unit] = Map([])
224+
for key in array {
225+
keys[key.to_owned()] = ()
226+
}
227+
(idx, value) => if keys.contains(idx) { Some(value) } else { None }
228+
}
229+
Exclude(array) =>
230+
if array.length() <= 8 {
231+
(idx, value) => if array.contains(idx) { None } else { Some(value) }
232+
} else {
233+
let keys : Map[String, Unit] = Map([])
234+
for key in array {
235+
keys[key.to_owned()] = ()
236+
}
237+
(idx, value) => if keys.contains(idx) { None } else { Some(value) }
238+
}
239+
}
201240
}
202241

203242
///|
@@ -280,6 +319,7 @@ pub fn Json::stringify(
280319
replacer? : Replacer,
281320
) -> String {
282321
let buf = StringBuilder(size_hint=0)
322+
let prepared_replacer = replacer.map(replacer => replacer.prepare())
283323

284324
// Explicit stack to replace recursive calls
285325
let stack : Array[WriteFrame] = []
@@ -347,8 +387,8 @@ pub fn Json::stringify(
347387
match iterator.next() {
348388
Some((k, v)) => {
349389
let mut v2 = v
350-
if replacer is Some(replacer) {
351-
if (replacer.f)(k, v) is Some(v) {
390+
if prepared_replacer is Some(replacer) {
391+
if replacer(k, v) is Some(v) {
352392
v2 = v
353393
} else {
354394
continue None
@@ -527,21 +567,29 @@ fn escape(str : String, escape_slash~ : Bool) -> String {
527567
/// - Non-object values (arrays, strings, numbers, etc.) are returned unchanged
528568
/// - The original JSON value is not modified; a new value is returned
529569
pub fn Json::transform(self : Self, replacer : Replacer) -> Json {
570+
self.transform_with(replacer.prepare())
571+
}
572+
573+
///|
574+
fn Json::transform_with(
575+
self : Self,
576+
replacer : (String, Json) -> Json?,
577+
) -> Json {
530578
match self {
531579
Object(members) =>
532580
members
533581
.iter()
534582
.filter_map(pair => {
535583
let (k, v) = pair
536-
if (replacer.f)(k, v) is Some(v2) {
537-
Some((k, v2.transform(replacer)))
584+
if replacer(k, v) is Some(v2) {
585+
Some((k, v2.transform_with(replacer)))
538586
} else {
539587
None
540588
}
541589
})
542590
|> Map::from_iter()
543591
|> Object
544-
Array(members) => Array(members.map(m => m.transform(replacer)))
592+
Array(members) => Array(members.map(m => m.transform_with(replacer)))
545593
value => value
546594
}
547595
}

json/json_test.mbt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,22 @@ test "stringify with replacer" {
193193
)
194194
}
195195

196+
///|
197+
test "Replacer::keep observes key array changes between uses" {
198+
let keys : Array[StringView] = Array::makei(9, i => {
199+
if i == 0 {
200+
"a"
201+
} else {
202+
"unused-" + i.to_string()
203+
}
204+
})
205+
let replacer = @json.Replacer::keep(keys)
206+
let json : Json = { "a": 1, "b": 2 }
207+
inspect(json.stringify(replacer~), content="{\"a\":1}")
208+
keys[0] = "b"
209+
inspect(json.stringify(replacer~), content="{\"b\":2}")
210+
}
211+
196212
///|
197213
test "stringify with replace recursively" {
198214
let json : Json = {

json/pkg.generated.mbti

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,12 @@ pub fn Position::equal(Self, Self) -> Bool
5454

5555
pub struct Replacer {
5656
// private fields
57-
} derive(@debug.Debug)
57+
}
5858
#alias(new, deprecated)
5959
pub fn Replacer::Replacer((String, Json) -> Json?) -> Self
6060
pub fn Replacer::exclude(ArrayView[StringView]) -> Self
6161
pub fn Replacer::keep(ArrayView[StringView]) -> Self
62+
pub impl @debug.Debug for Replacer
6263

6364
#deprecated
6465
pub fn Json::as_array(Self) -> Array[Self]?

json/replacer_bench_test.mbt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 International Digital Economy Academy
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
///|
16+
fn make_replacer_bench_data() -> (Json, Array[StringView]) {
17+
let object = Map([])
18+
let keys : Array[StringView] = Array::makei(2500, i => {
19+
"key-" + (i * 2).to_string()
20+
})
21+
for i in 0..<5000 {
22+
object["key-" + i.to_string()] = Json::number(i.to_double())
23+
}
24+
(Json::object(object), keys)
25+
}
26+
27+
///|
28+
test "bench Json::stringify Replacer::keep keys=2500 fields=5000" (
29+
it : @bench.T,
30+
) {
31+
let (json, keys) = make_replacer_bench_data()
32+
it.bench(fn() {
33+
let replacer = @json.Replacer::keep(keys)
34+
it.keep(json.stringify(replacer~).length())
35+
})
36+
}
37+
38+
///|
39+
test "bench Json::stringify Replacer::keep keys=3 fields=5000" (it : @bench.T) {
40+
let (json, _) = make_replacer_bench_data()
41+
let keys : Array[StringView] = ["key-0", "key-2500", "key-4999"]
42+
let replacer = @json.Replacer::keep(keys)
43+
it.bench(fn() { it.keep(json.stringify(replacer~).length()) })
44+
}

0 commit comments

Comments
 (0)