Skip to content

perf(json): write escapes and indentation directly into the builder - #3820

Draft
bobzhang wants to merge 1 commit into
mainfrom
hongbo/perf-json-stringify
Draft

perf(json): write escapes and indentation directly into the builder#3820
bobzhang wants to merge 1 commit into
mainfrom
hongbo/perf-json-stringify

Conversation

@bobzhang

Copy link
Copy Markdown
Contributor

Json::stringify allocated a throwaway String for every string value and object key: escape() built a fresh StringBuilder, returned a String, and the caller copied it into the outer buffer — with no fast path for the overwhelmingly common case of strings that need no escaping.

Change

  • escape_into(buf, str, escape_slash~) replaces escape(): scans code units for the first escapable character (the escape set is all-ASCII, so code-unit scanning is exact even around surrogate pairs). Clean strings — most keys and values — become a single write_string. Strings containing escapes take the original char-by-char loop, now writing directly into the outer builder. The fallback path is byte-identical to the old loop, including for malformed UTF-16 (unpaired surrogates copy through verbatim; a regression test built with unsafe_substring pins both paths).
  • write_indent replaces indent_str, writing the newline and spaces directly instead of allocating "\n" + " ".repeat(n) per separator beyond the old 8-space cache. Non-positive indent now explicitly means compact output (previously a negative indent hit an accidental abort inside String::repeat).

Benchmarks (native, release; committed as json/stringify_bench_test.mbt)

bench before after
string-heavy n=1000 3.89 ms 1.16 ms ~3.4×
pretty indent=4 n=200 1.11 ms 0.37 ms ~3.0×
escape-heavy n=500 320 µs 220 µs ~1.5×

Review

Reviewed by Codex CLI (codex-cli 0.144.1) in two rounds. Round 1 rejected the initial version, catching an abort on unpaired surrogates adjacent to escape characters (view-slice boundary validation) and a negative-indent behavior change — both real. The revised version avoids view slicing entirely; round 2: "Approved — no blocking findings. Both concerns are resolved, and well-formed output remains equivalent... Public interface is unchanged."

Signed-off-by: Codex CLI codex@openai.com

Validation

  • moon check clean, moon fmt applied, no .mbti changes
  • moon test: 6716 passed, 0 failed (json 185/185, incl. new surrogate regression test)

🤖 Generated with Claude Code

Json::stringify allocated a throwaway String for every string value
and object key: escape() built a fresh StringBuilder and returned a
String that was immediately copied into the outer buffer, with no fast
path for strings needing no escapes. Replace it with escape_into(),
which scans for the first escapable code unit (all-ASCII set, so
code-unit scanning is exact even around surrogates) and bulk-copies
the whole string when clean — the common case for keys and most
values; strings containing escapes keep the original char-by-char
loop, now writing into the outer builder, which also preserves the old
behavior for malformed UTF-16 (unpaired surrogates copy through
verbatim; a regression test pins this).

indent_str similarly allocated "\n" + " ".repeat(n) per separator for
indents past its 8-space cache; write_indent() now writes the newline
and spaces directly, allocation-free at any depth. Non-positive indent
now means compact output (previously a negative indent aborted inside
String::repeat).

Adds stringify benchmarks (matching parse_bench_test.mbt style).
Native release, before -> after:

- string-heavy n=1000:  3.89 ms -> 1.16 ms  (~3.4x)
- pretty indent=4 n=200: 1.11 ms -> 0.37 ms (~3.0x)
- escape-heavy n=500:   320 us -> 220 us    (~1.5x)
Reviewed by Codex CLI (codex-cli 0.144.1) in two rounds: the first
flagged an abort on unpaired surrogates after escape chars and a
negative-indent behavior change; both were fixed and re-review
approved: "Both concerns are resolved, and well-formed output remains
equivalent... Public interface is unchanged."

Signed-off-by: Codex CLI <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coveralls

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 5182

Coverage decreased (-0.009%) to 91.259%

Details

  • Coverage decreased (-0.009%) from the base build.
  • Patch coverage: 17 of 17 lines across 1 file are fully covered (100%).
  • 1 coverage regression across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
json/json.mbt 1 98.99%

Coverage Stats

Coverage Status
Relevant Lines: 17206
Covered Lines: 15702
Line Coverage: 91.26%
Coverage Strength: 198928.07 hits per line

💛 - Coveralls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants