Skip to content

JSOM 3.0.0 — path cache removed (measured), const reads race-free, C++20/23 and the CLI gated

Choose a tag to compare

@HarryPehkonen HarryPehkonen released this 19 Sep 13:18
· 26 commits to main since this release

Breaking changes

The path cache is gone, and with it its public API. Every JsonDocument used to own a
three-level path cache (exact paths, prefixes, recent prefixes with 10-minute aging) that
at(), find(), exists() and at_multiple() fed on every lookup. It was measured for
the first time and it lost, so it was deleted rather than repaired.

Removed: PathCache, JsonDocument::precompute_paths(), warm_path_cache(),
clear_path_cache(), get_path_cache_stats(), NavigationEngine::navigate_with_cache(),
navigate_simple(), NavigationResult, and the --cache-warm / --cache-stats flags.

was now
NavigationEngine::navigate_simple(root, path) NavigationEngine::find(root, path)
NavigationEngine::navigate_with_cache(root, path, cache) NavigationEngine::find(root, path)
doc.precompute_paths(depth) / doc.warm_path_cache(paths) delete the call — navigation reads the document directly
doc.get_path_cache_stats() delete the call
--cache-warm, --cache-stats removed; unknown --options on pointer now exit 1 instead of being ignored

at(), find(), exists(), at_multiple(), set_at(), remove_at() and list_paths()
are unchanged in signature and semantics.

CLI: jsom pointer … now rejects unknown --options with a message and exit 1. They
used to be collected into a vector nothing read, so a typo (or a deleted flag) exited 0.

Security

Reading one document from several threads is race-free now. The cache was reached from
const methods through const_cast<JsonDocument*>(this) and lived in mutable members, so
doc.at(path) on a const document wrote to it. Four threads reading one shared document
produced 71 ThreadSanitizer data races and then a SEGV inside memmove. The test was
written first and watched fail; it is clean now, and a tsan CI stage (~6 s) keeps it that
way. The contract is documented: any number of concurrent readers, one writer — a
returned reference dies at the next mutation.

2.0.0 is superseded. That race is present in the 2.0.0 release (it predates this work),
so anyone sharing a document across threads should move to 3.0.0 rather than stay on 2.0.0.

Also gone with the cache: a raw owning pointer (new PathCache() / delete), a
process-global mutation counter bumped by every mutation, cached raw pointers into document
storage that could dangle when a child vector grew, and wall-clock eviction inside a core
data structure.

The nesting-depth guard is unchanged and verified with the input that used to kill the
process: a 60 KB document with 30,000 nested arrays now answers
Maximum nesting depth exceeded (limit 256) and exits 1.

Performance

The cache made the common access pattern slower, not faster. -O3 -march=native, 100 KB
document of 1000 records, median of 5 runs:

access pattern with cache without
every path once (7003 paths) 33.810 ms 1.881 ms 17.97× faster without
repeat one shallow path ×10000 2.640 ms 2.649 ms unchanged
shared-prefix sweep (1000 leaves) 0.303 ms 0.282 ms 1.07× faster without
write + read ×2000 9.547 ms 8.334 ms 1.15× faster without
repeat one 200-deep path ×10000 42.909 ms 69.835 ms 1.6× faster with

The single case the cache helped — repeating the same deep path — is served by holding the
pointer from the first find(), and the README shows how. It also retained ~72 KB per
1000-record document and had no hit/miss counters, so its benefit had never been
measurable at all.

JSOM remains faster than nlohmann/json in the 17 paired benchmarks the repo ships
(1.26×–1.98×), measured with tools/perf_probe.cpp; object parse is the slowest shape
(~19 MB/s) and object storage is the next known target.

Testing and tooling

New gates: tsan (framework-free probe, ~6 s), std (compiles and runs
tools/std_probe.cpp as C++17, C++20 and C++23 — that claim was previously untested),
cli (22 smoke checks over the jsom binary: exit codes, validation modes, pointer
operations, rejected flags), coverage (opt-in, gcov; reports 66.4% library line
coverage, weakest being the formatting engine at 18.9%). Local CI is git-hooks-only, 12
stages, ~7 minutes.

Fixed: the conformance-corpus test resolved its data path relative to the working
directory, so ctest — which runs from the build directory — failed while the same binary
passed from the project root. The path is baked in at configure time now. Removed 18
constants that nothing referenced.

Docs

Documentation describes only what JSOM offers now. CODING_STANDARDS.md gains rule 11
(const means "changes nothing", including hidden state) and the thread/standard gates in
its checklist. The version lives in exactly one place (project(JSOM VERSION …) in
CMakeLists.txt).