Skip to content

perf: Add a guarded synchronous JSON-LD fast path to RdfParser (~20-25x on generated-document parsing)#1

Draft
jeswr wants to merge 1 commit into
masterfrom
perf/jsonld-fast-path
Draft

perf: Add a guarded synchronous JSON-LD fast path to RdfParser (~20-25x on generated-document parsing)#1
jeswr wants to merge 1 commit into
masterfrom
perf/jsonld-fast-path

Conversation

@jeswr

@jeswr jeswr commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a guarded, synchronous JSON-LD fast path to RdfParser for the fixed-shape, known-context documents that Components.js itself generates and consumes. Any document or context feature outside the implemented subset transparently falls back to the unchanged generic pipeline (rdf-parsejsonld-streaming-parser), so behaviour — including error behaviour — is preserved by construction.

Loading a large Components.js application (e.g. Community Solid Server) parses ~1000 small JSON-LD documents that share a handful of prefetched contexts and a rigidly regular, generator-produced shape. Profiling a CSS cold start showed the bulk of ComponentsManager loading time inside the generic JSON-LD machinery (per-document parser construction, per-value async handler dispatch, context re-processing). This PR removes that cost for the generated documents while leaving every other document on the spec-compliant path.

Design

  • RdfParser.parse buffers candidate JSON-LD documents (by path extension / content type, capped at 8 MiB — larger documents stream through the generic parser) and attempts tryParseJsonLdFastPath; on undefined it replays the buffered text through the generic pipeline.
  • Each distinct root @context (URL set) is normalized once with the real jsonld-context-parser ContextParser + PrefetchedDocumentLoader, then analyzed and cached (in a WeakMap keyed on the prefetched-contexts record, so caches are per-ComponentsManager and leak-free). The analysis marks unsafe terms — scoped contexts, @reverse, non-@list/@set containers, keyword aliases, @vocab-typed coercions, nulled terms, … — and any document using one of them (as a key or @type value) falls back.
  • The converter is a plain synchronous recursive walk emitting RDF/JS quads. Term-to-IRI expansion delegates to (memoized) jsonld-context-parser expandTerm calls with the same expand options as the streaming parser, so IRI semantics are identical by construction. Canonical numeric lexical forms and rdf:JSON canonicalization mirror the streaming parser exactly (same canonicalize package).
  • Quads still flow through RdfStreamIncluder: imports and IRI-validation warnings behave exactly as before, and imported documents re-enter the fast path individually.
  • Document-level guard: a keyword scan rejects anything that may use @graph, @reverse, @language, @index, @nest, @base, @vocab, nested @contexts, …; the converter additionally bails (→ fallback) on relative IRIs, unknown terms, blank-node predicates, list-of-lists, non-canonicalizable numbers, and every other observed divergence. Two subtle streaming-parser behaviours were mirrored deliberately rather than "fixed": key-order-sensitive @json value objects, and non-canonicalized explicitly-typed numeric @values (both bail to the generic parser except in the exact shapes it handles as JSON).
  • Opt-out: disableJsonLdFastPath: true in RdfParserOptions forces the generic pipeline.

Correctness evidence

  • Boot-corpus oracle (not in CI; harness: oracle.js, available on request): all 1048 files of a real Community Solid Server 7.1.9 boot — 828 generated component documents + 220 hand-written config files — parse rdf-isomorphic to the generic pipeline (identical error behaviour for error cases). All 828 component files take the fast path; all 220 config files (they use @graph) fall back as designed.
  • 119 new unit tests compare fast-path output against the generic parser (isomorphism or identical errors) across the emit matrix (types, coercions, lists, value objects, @json, numbers/booleans/null, blank nodes, imports, buffering/cap/replay) and the fallback matrix (every unsafe-term class, every guard rejection).
  • Existing suite: 987 tests, 34 suites, 100 % statements/branches/functions/lines (the repository threshold) — integration tests exercise the fast path end-to-end since all .jsonld fixtures now go through it.

Performance

Micro-benchmark (microbench.js): parse the 828-document CSS boot corpus (in-memory text, warm) through RdfParser, medians of 7 alternating fast/stock reps, identical 34,926-quad output. Box: pss-solid-test (2-core shared, nice -n 18; absolute numbers are noisy on this box — the ratio is the claim, arms interleaved in the same run):

run stock (generic pipeline) fast path speedup
1 5,255 ms (4,013–17,311) 206 ms (156–245) 25.6×
2 5,427 ms 244 ms 22.2×
3 3,854 ms (3,759–4,696) 169 ms 23.6×

End-to-end CSS cold start (the real target): the same fast path back-ported onto the componentsjs 5.5.1 that Community Solid Server 7.1.9 actually boots (validated first: all 1048 boot files rdf-isomorphic patched-vs-stock on the 5.5.1 stack), measured as node bin/server.js -c config/file.json to Listening to server, alternating arms, fresh data root per run:

arm boots (ms) median
stock 5.5.1 19,652 / 17,046 / 16,848 17,046 ms
+ JSON-LD fast path 10,346 / 10,719 / 10,898 10,719 ms

−6.3 s, a 1.59× faster cold start from this change alone (833 documents fast-pathed, 145 hand-written configs fell back as designed). The stock median reproduces the ~17.5 s baseline from the earlier profiling. The remaining config-parse cost is per-file context re-normalization on the fallback path — that is what LinkedSoftwareDependencies#170 (shared context parser + normalized-context cache) addresses; the two changes compose.

Compatibility notes

  • New runtime dependency: canonicalize (~1 kB, already a transitive dependency via jsonld-streaming-parser) — used to byte-match the generic parser's rdf:JSON literals.
  • Blank-node labels from the fast path use a distinct cjsfp-<doc>-<n> namespace, so they can never collide with streaming-parser labels in mixed fast/fallback loads (graphs are isomorphic, labels differ — same situation as any two parser instances).
  • No public API changes; one new optional RdfParserOptions flag (disableJsonLdFastPath).

Generated by an agent (Claude Fable 5) on @jeswr's behalf — staged on the fork for his review before upstreaming.

Review timing: This draft was prepared with Claude; I (@jeswr) will personally review it before it progresses. I'm currently batching a lot of work in flight, so expect active review Wednesday–Friday (8–10 July).

Loading a Components.js application parses hundreds of small JSON-LD
documents (component definitions and config files) that share a handful
of well-known, prefetched contexts and a rigidly regular,
generator-produced shape. The generic streaming pipeline
(rdf-parse -> jsonld-streaming-parser) pays per-document parser
construction and per-value async handler dispatch for JSON-LD
flexibility these documents never use; profiling a Community Solid
Server cold start attributed the bulk of the boot to this parsing path.

This adds lib/rdf/JsonLdFastPath.ts: a specialized, synchronous
JSON-LD-to-quads converter behind a conservative shape guard:

- RdfParser.parse buffers candidate JSON-LD documents (path/content-type
  driven, capped at 8 MiB) and attempts the fast path; any document or
  context feature outside the implemented subset falls back to the
  unchanged generic pipeline, so behaviour (including error behaviour)
  is preserved by construction.
- Each distinct root @context (URL set) is normalized ONCE with the real
  jsonld-context-parser ContextParser + PrefetchedDocumentLoader and
  cached (weakly, per prefetched-contexts record); the analysis marks
  terms whose definitions the converter cannot honour (scoped contexts,
  @reverse, non-@list containers, keyword aliases, ...) so any document
  using them falls back.
- Term-to-IRI expansion inside the converter delegates to memoized
  jsonld-context-parser expandTerm calls with the same expand options as
  the streaming parser, so IRI semantics are identical by construction.
- Quads still flow through RdfStreamIncluder, so imports and IRI
  validation behave exactly as before, and imported documents re-enter
  the fast path individually.

Correctness: on the full Community Solid Server boot corpus (828
generated component documents + 220 hand-written config files), the
fast path output is rdf-isomorphic to the generic pipeline for all 1048
files; all 828 component files take the fast path, and all 220 config
files (which use @graph) fall back as designed. 119 new unit tests
compare fast-path output against the generic parser (isomorphism or
identical errors) across the emit and fallback matrix; the suite
retains 100% coverage.

Performance (pss-solid-test, 2-core shared box, warm in-memory corpus,
medians of 7 alternating reps): parsing the 828-document boot corpus
through RdfParser drops from ~3.9-5.4 s to ~0.17-0.24 s (~20-25x) with
identical 34,926-quad output. Hand-written configs are unaffected (they
keep the generic path).

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.

1 participant