Skip to content

perf(serialise): stream JSON directly and compile format strings#7

Merged
benmandrew merged 1 commit into
mainfrom
perf/serialise-json
Jun 13, 2026
Merged

perf(serialise): stream JSON directly and compile format strings#7
benmandrew merged 1 commit into
mainfrom
perf/serialise-json

Conversation

@benmandrew

Copy link
Copy Markdown
Owner

Summary

Graph JSON serialisation was the dominant wall-clock cost — ~19.7s vs ~2.5s of search generation on a 1.83M-node graph. This isolates three serialisation optimisations, with byte-shape-identical output (the web frontend is untouched).

Changes

  • Stream JSON directly instead of building an nlohmann::json DOM then .dump()-ing it. Records are written into a 1 MiB reused buffer flushed to the file, so the multi-GB string is never fully held in memory. graph_to_json now parses graph_to_string's output, keeping a single serialisation implementation (the DOM path only ran in tests).
  • FMT_COMPILE for the per-cell board formatting (Table::to_string) and per-node/per-edge records. fmt was re-parsing {:^5} etc. at runtime for every cell of every node; compiling the format strings removes the parse_format_string / on_format_specs overhead (the single largest remaining cost).
  • Bulk-append runs of non-escaped characters in the JSON string escaper instead of one char at a time.

Results

Write time on the 1.83M-node graph (--max-depth 30): 19.7s → 7.9s (~2.5×), memory now bounded.

Testing

All 230 assertions pass — test_table.cpp asserts exact to_string() output and test_serialise.cpp validates the parsed structure. Output field shapes verified identical to the previous format (the "size":["..."] array quirk, raw UTF-8 suit glyphs, escaped newlines, label/forceLabel only on start/winning nodes).

Graph serialisation was the dominant wall-clock cost (~19.7s vs ~2.5s
generation on a 1.8M-node graph). Three changes, output byte-shape identical:

- Replace the nlohmann::json DOM construction + .dump() with direct streaming
  into a 1 MiB reused buffer flushed to the file, so the whole multi-GB string
  is never held in memory. graph_to_json now parses graph_to_string's output,
  keeping one serialisation implementation (the DOM path only ran in tests).

- Use FMT_COMPILE for the per-cell board formatting (Table::to_string) and the
  per-node/per-edge JSON records. fmt was re-parsing '{:^5}' etc. at runtime
  for every cell of every node; compiling the format strings removes the
  parse_format_string / on_format_specs overhead (the single largest cost).

- Bulk-append runs of non-escaped characters in the JSON string escaper
  instead of one char at a time.

Write time on the 1.8M-node graph: 19.7s -> 7.9s (~2.5x). All 230 test
assertions pass (test_table asserts exact to_string output; test_serialise
validates the parsed structure).
@benmandrew benmandrew merged commit 8b33b2e into main Jun 13, 2026
6 checks passed
@benmandrew benmandrew deleted the perf/serialise-json branch June 13, 2026 19:33
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.

1 participant