Skip to content

feat: context cache - #70

Closed
jeswr wants to merge 8 commits into
rubensworks:masterfrom
jeswr:wip/context-cache
Closed

feat: context cache#70
jeswr wants to merge 8 commits into
rubensworks:masterfrom
jeswr:wip/context-cache

Conversation

@jeswr

@jeswr jeswr commented Oct 27, 2023

Copy link
Copy Markdown
Contributor

Creates a context caching mechanism. The use case of this is parsing numerous json-ld objects which all have the same context object; where context parsing is often the main bottleneck; see performance results below:

Parse a context that has not been cached; and without caching in place x 108 ops/sec ±0.32% (86 runs sampled)
Parse a list of iri contexts that have been cached x 79,985 ops/sec ±0.44% (90 runs sampled)
Parse a context object that has not been cached x 1,950 ops/sec ±1.36% (90 runs sampled)
Parse a context object that has been cached x 7,637 ops/sec ±0.20% (91 runs sampled)

@coveralls

coveralls commented Oct 27, 2023

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 6666516387

  • 53 of 53 (100.0%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.1%) to 99.904%

Totals Coverage Status
Change from base Build 6492290900: -0.1%
Covered Lines: 570
Relevant Lines: 570

💛 - Coveralls

@rubensworks

Copy link
Copy Markdown
Owner

This looks very neat, thanks @jeswr!

Before I do an in-depth review, I have some general questions.

  • What is the overall performance impact of this when caching is not very valuable? Might be useful to run the parser perf checks again with an without this cache. I suspect hashing to have some overhead.
  • Do you have insights on memory consumption? My fear is that for parsers that are reused, many (potentially large) contexts can be cached. I know there's the LRU cache, but I wonder if we should consider disabling the cache by default (not sure tbh).
  • Do all JSON-LD spec tests still pass with this change?

Pinging @sdevalk, as this will interest him regarding rubensworks/rdf-dereference.js#48

@jeswr

jeswr commented Oct 27, 2023

Copy link
Copy Markdown
Contributor Author

What is the overall performance impact of this when caching is not very valuable? Might be useful to run the parser perf checks again with an without this cache. I suspect hashing to have some overhead.

In the worst case it seems to be an extra 50% overhead. It can be worked around by trying to work by-reference rather than by hash most of the time (see https://github.com/inrupt/solid-client-vc-js/blob/0f8ce276b6ea8a977b9d2ea189bc92385ef44b48/src/parser/jsonld.ts#L70-L111) however; the danger here is that you accidentally consume a large amount of memory by using contexts as keys so I'm leaving this for a subsequent piece of work once better benchmarking is in place and we have a good way of pruning them quickly.

FYI the custom caching mechanism in https://github.com/inrupt/solid-client-vc-js/blob/0f8ce276b6ea8a977b9d2ea189bc92385ef44b48/src/parser/jsonld.ts reduced the time on e2e tests from 20min to 20seconds.

Do you have insights on memory consumption? My fear is that for parsers that are reused, many (potentially large) contexts can be cached. I know there's the LRU cache, but I wonder if we should consider disabling the cache by default (not sure tbh).

I've disabled caching by default in b48cc07. So we will need to make an update to jsonld-streaming-parser to allow custom contexts / caches to be passed in.

Do all JSON-LD spec tests still pass with this change?

I'm not seeing rdf-test-suite as a dev dependency of this library or any commands to run spec testing. Do you have a script somewhere to run spec tests on this lib?

@rubensworks

Copy link
Copy Markdown
Owner

danger here is that you accidentally consume a large amount of memory by using contexts as keys so I'm leaving this for a subsequent piece of work once better benchmarking is in place and we have a good way of pruning them quickly.

I suspect that caching by reference would consume less memory than hashing, as caching is only done on pointers towards shared memory.

reduced the time on e2e tests from 20min to 20seconds.

Ooh, nice!

I'm not seeing rdf-test-suite as a dev dependency of this library or any commands to run spec testing. Do you have a script somewhere to run spec tests on this lib?

This will have to be tested by manually plugging this into jsonld-streaming-parser.

@jeswr

jeswr commented Oct 27, 2023

Copy link
Copy Markdown
Contributor Author

as caching is only done on pointers towards shared memory.

My main concern here is that if we have a poor configuration of the lru-cache then we are preventing GC on context objects that have already been parsed and may no longer be relevant.

@rubensworks

Copy link
Copy Markdown
Owner

My main concern here is that if we have a poor configuration of the lru-cache then we are preventing GC on context objects that have already been parsed and may no longer be relevant.

I would suspect lru-cache to be very well battle-tested by now, but I don't know enough of its internals to make any hard claims :-)

@jeswr

jeswr commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #85, which refreshes this context-cache onto current master with two improvements: (1) a correctness fix — the original key collapsed contexts differing only in parentContext; (2) a baseIRI-independent (sentinel-based, copy-on-write) design that actually hits on the real workload (0% → 99.78% cache hits, ~1.76x on the CSS boot parse phase, output verified byte-identical). Closing in favour of #85. Thanks for the original design — it was the right instinct.

@jeswr jeswr closed this Jul 3, 2026
jeswr pushed a commit to jeswr/jsonld-context-parser.js that referenced this pull request Jul 3, 2026
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 pushed a commit to jeswr/jsonld-context-parser.js that referenced this pull request Jul 3, 2026
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>
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.

3 participants