feat: Add an opt-in normalized-context cache - #85
Draft
jeswr wants to merge 1 commit into
Draft
Conversation
This was referenced Jul 3, 2026
Closed
jeswr
force-pushed
the
perf/context-cache-refresh
branch
from
July 3, 2026 19:45
a3c8f1f to
3759909
Compare
Parsing a JSON-LD context is dominated by normalization (term expansion, IRI validation, container hashing, keyword-redefinition checks). When the same context is parsed many times this work is fully repeated every time. Profiling a Community Solid Server boot showed over half of all active CPU inside jsonld-context-parser, with the Components.js ^5.0.0 context alone re-normalized ~979 times (once per referencing document, each with a distinct per-document base IRI). This adds an optional `contextCache` on the ContextParser that memoizes the normalized result of `parse`. A single cache instance can be shared across multiple ContextParser instances so a context is normalized once and reused everywhere. Caching is disabled by default; passing no `contextCache` preserves the previous behaviour exactly. - `IContextCache` interface (hash/get/set) and a bounded LRU-backed `ContextCache` implementation, keyed on the full content of the context, its parent context, and the remaining options. - Top-level object contexts are memoized directly (their key legitimately captures the base IRI). Loaded external (URL/array) contexts are instead memoized at a base-independent boundary (`parseExternalCached`): their normalization is computed once with the per-document base replaced by a fixed, unguessable sentinel, and the real base is substituted back on each retrieval by a copy-on-write walk (`rebaseSentinel`/`replaceSentinel`) that shares base-independent subtrees, so a cached entry is never mutated. The per-call base is re-applied on a shallow clone (`applyBaseEntryCloned`). - Correctness is guaranteed: the real (base-carrying) parse is always authoritative, and a sentinel result is cached only after it is verified once, on the miss, to re-base byte-for-byte to the real parse (via `Util.deepEqual`). A context whose normalization genuinely depends on the base (relative @vocab, @type-to-@base expansion) simply fails that check and is not shared -- output stays byte-identical for every context. On the CSS boot workload the external-context hit rate goes from 0% to ~99.8% and the parse is ~1.75x faster end-to-end, with identical output. Supersedes rubensworks#70. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jeswr
force-pushed
the
perf/context-cache-refresh
branch
from
July 3, 2026 20:34
3759909 to
aebf455
Compare
This was referenced Jul 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds an opt-in cache of the normalized context to
ContextParser, so that a context which has already been parsed isn't normalized again. Caching is off by default — passing nocontextCachepreserves current behaviour exactly.New public surface:
IContextCache— interface (hash/get/set).ContextCache— a bounded, LRU-backed implementation.IContextParserOptions.contextCache?: IContextCache.Internally,
parsebecomes a thin caching wrapper and the normalization logic moves to a private_parse. All recursive sub-context parsing is routed through_parse(bypassing the cache) so that only fully post-processed top-level results are ever cached — this keeps cached entries safe from later mutation (e.g. base-IRI application on an outer parse).Why — profiling evidence
Profiling a Community Solid Server boot (
bin/server.js -c config/default.json, componentsjs 5.5.1, jsonld-context-parser 2.3.2):ContextParser.parsealone is 36.7 % of active CPU. Runtime instrumentation of that exact boot showed the same@contexts being fully re-normalized over and over, because a freshContextParser(with an empty cache) is created per.jsonldfile:load()ed during one boot^5.0.0@solid/community-server ^7.0.0@comunica/core ^2.0.0A single
ContextCacheshared across those per-file parsers means each distinct context is normalized once and reused everywhere.Benchmark (
npm run perf)Parsing the Components.js
^5.0.0context (90 terms — one of the contexts re-normalized ~979× above), 1000×, with vs. without the cache. Indicative only — this box is shared with a live server:≈ 8–12× faster on a cache hit; the benchmark asserts the cached result is identical to the uncached one before timing.
Correctness — what changed vs. #70
#70's cache key had a latent bug: two parses that differ only in a (non-empty)
parentContexthashed to the same key, so the second would get the first's (wrong) cached result. Verified against #70's exact hashing code:This PR derives the key from the full content of the context, its parent context, and the remaining options, so:
Because the returned normalized context may be shared across cache hits, the contract (documented on
parse) is that callers must not mutate it when a cache is in use.Tests & checks
yarn build,yarn lint(tslint, 0 warnings),yarn test, andnpx webpackall green.ContextCache.ts100%). New tests cover: cache-hit identity, cross-parser reuse via a shared cache, the parent-context non-collapse (the feat: context cache #70 fix), empty-parent equivalence, LRU eviction, cached-rejection consistency, and theContextCache.hash/get/setunit surface.Note on the diff
.github/workflows/ci.ymlandcodeql-analysis.ymlappear here only because @jeswr's fork is behind upstream on those files, and the agent's token lacks theworkflowscope needed to push upstream's versions. They are pinned to the fork base in an isolated commit (chore: Pin workflow files…) and are not part of this change — they'll disappear once @jeswr syncs the fork and drops that commit.Proposed changelog: Add an optional cache of normalized contexts (opt-in via the
contextCacheoption /ContextCache).