[PR #1322] [MERGED] perf(json): encode streamed JSON iteratively #2799

Closed
opened 2026-10-17 21:10:23 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1322
Author: @kaoru0822-kitauji
Created: 10/9/2026
Status: ✅ Merged
Merged: 10/9/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: perf/json-serialization-iterative


📝 Commits (2)

  • 3b03d42 perf(json): encode streamed JSON iteratively
  • 61a8580 Merge remote-tracking branch 'origin/main' into perf/json-serialization-iterative

📊 Changes

2 files changed (+189 additions, -64 deletions)

View changed files

📝 src/domain/jsonSerialization.test.ts (+63 -8)
📝 src/domain/jsonSerialization.ts (+126 -56)

📄 Description

Summary

The CLI's --format json output, MCP result encoding and Evidence bundle export stream JSON through jsonParts / canonicalJsonParts (src/domain/jsonSerialization.ts). The encoder recursed with one generator per nesting level and yielded every token (,, indentation, keys, scalars) separately. Each token therefore resumed every enclosing generator, so cost grew with output size × nesting depth.

A CPU profile of rea analyze-javascript-application /Applications/Typora.app --format json (873 MB of output) on main: 66.2 s total, of which writeJsonOutput took 21.8 s inclusive and encodeValue alone 15.7 s of self time, the largest single hotspot in the run.

Change

encodeValue is now iterative: an explicit container stack and a local buffer, yielding about 16 KiB parts. Unchanged:

  • Bytes: compact, pretty (JSON.stringify(v, null, 2)) and canonical key order (.sort()) are identical;
  • Omission: object members that are undefined, functions or symbols are skipped; undefined in arrays, non-finite numbers and non-plain objects still throw the same TypeErrors; repeated references are allowed and cycles rejected;
  • Long strings and keys (over 8,192 characters) still go through the existing surrogate-pair-safe encodeString chunking;
  • Laziness: a part is yielded once the buffer reaches 16 KiB, before later members are read, so consumers that stop early (encodeToolResult's budget check) still don't read the rest of an oversized result. A new test pins this with a throwing getter.

The public functions (jsonParts, canonicalJsonParts, bufferedJsonParts) keep their signatures.

Results

Synthetic nested results (node, macOS arm64), old → new, outputs byte-identical to each other and to native JSON.stringify / canonicalize:

shape compact pretty canonical
20k nodes, depth 2 (18–42 M chars) 860 → 234 ms 1263 → 258 ms 876 → 261 ms
5k nodes, depth 8 (16–45 M chars) 971 → 169 ms 1442 → 216 ms 950 → 206 ms

Real CLI: analyze-javascript-application /Applications/Typora.app --format json takes 67 s → 47 s, and the 873,068,745-byte output is byte-identical (cmp).

Verification

  • src/domain/jsonSerialization.test.ts: the existing cases plus native equality for 9,000-character keys, omitted members and 500 levels of mixed nesting; the error cases; laziness and bounded parts.
  • npm run test:focused -- on every test that exercises the serializer's consumers (CLI JSON output, MCP large and oversized results, tool result encoding, Evidence bundles, source maps): 12 files, 148 tests. All of tests/boundary/cli: 36 files, 190 tests.
  • npm run check:changed, tsc --noEmit, npm run format:check

npm run jscpd currently fails on main itself (2.5013% > 2.5%, from #1301). This PR adds no duplicated lines (8,041, the same as main), and #1317 restores the gate.

🤖 Generated with Claude Code


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1322 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/9/2026 **Status:** ✅ Merged **Merged:** 10/9/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `perf/json-serialization-iterative` --- ### 📝 Commits (2) - [`3b03d42`](https://github.com/morluto/rea/commit/3b03d425a544902f8f4696713b68e7484321c4ae) perf(json): encode streamed JSON iteratively - [`61a8580`](https://github.com/morluto/rea/commit/61a8580135da870d66b98f6e3e7f1f037f23b696) Merge remote-tracking branch 'origin/main' into perf/json-serialization-iterative ### 📊 Changes **2 files changed** (+189 additions, -64 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/jsonSerialization.test.ts` (+63 -8) 📝 `src/domain/jsonSerialization.ts` (+126 -56) </details> ### 📄 Description ## Summary The CLI's `--format json` output, MCP result encoding and Evidence bundle export stream JSON through `jsonParts` / `canonicalJsonParts` (`src/domain/jsonSerialization.ts`). The encoder recursed with one generator per nesting level and yielded every token (`,`, indentation, keys, scalars) separately. Each token therefore resumed every enclosing generator, so cost grew with output size × nesting depth. A CPU profile of `rea analyze-javascript-application /Applications/Typora.app --format json` (873 MB of output) on `main`: 66.2 s total, of which `writeJsonOutput` took 21.8 s inclusive and `encodeValue` alone 15.7 s of self time, the largest single hotspot in the run. ## Change `encodeValue` is now iterative: an explicit container stack and a local buffer, yielding about 16 KiB parts. Unchanged: - **Bytes:** compact, pretty (`JSON.stringify(v, null, 2)`) and canonical key order (`.sort()`) are identical; - **Omission:** object members that are `undefined`, functions or symbols are skipped; `undefined` in arrays, non-finite numbers and non-plain objects still throw the same `TypeError`s; repeated references are allowed and cycles rejected; - **Long strings and keys** (over 8,192 characters) still go through the existing surrogate-pair-safe `encodeString` chunking; - **Laziness:** a part is yielded once the buffer reaches 16 KiB, before later members are read, so consumers that stop early (`encodeToolResult`'s budget check) still don't read the rest of an oversized result. A new test pins this with a throwing getter. The public functions (`jsonParts`, `canonicalJsonParts`, `bufferedJsonParts`) keep their signatures. ## Results Synthetic nested results (node, macOS arm64), old → new, outputs byte-identical to each other and to native `JSON.stringify` / `canonicalize`: | shape | compact | pretty | canonical | |---|---|---|---| | 20k nodes, depth 2 (18–42 M chars) | 860 → 234 ms | 1263 → 258 ms | 876 → 261 ms | | 5k nodes, depth 8 (16–45 M chars) | 971 → 169 ms | 1442 → 216 ms | 950 → 206 ms | Real CLI: `analyze-javascript-application /Applications/Typora.app --format json` takes 67 s → 47 s, and the 873,068,745-byte output is byte-identical (`cmp`). ## Verification - `src/domain/jsonSerialization.test.ts`: the existing cases plus native equality for 9,000-character keys, omitted members and 500 levels of mixed nesting; the error cases; laziness and bounded parts. - `npm run test:focused --` on every test that exercises the serializer's consumers (CLI JSON output, MCP large and oversized results, tool result encoding, Evidence bundles, source maps): 12 files, 148 tests. All of `tests/boundary/cli`: 36 files, 190 tests. - `npm run check:changed`, `tsc --noEmit`, `npm run format:check` `npm run jscpd` currently fails on `main` itself (2.5013% > 2.5%, from #1301). This PR adds no duplicated lines (8,041, the same as `main`), and #1317 restores the gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:10:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2799
No description provided.