perf(cache): hot-path wins in CacheAndBufferLayer (2/3 of #991)#993
perf(cache): hot-path wins in CacheAndBufferLayer (2/3 of #991)#993SamTV12345 wants to merge 7 commits into
Conversation
Split clone() into cloneIn (write path, preserves toJSON semantics) and cloneOut (read path, delegates to V8-native structuredClone). The cached value is always JSON-safe by construction, so the read-path fast cloner does not need toJSON support.
flush() previously iterated the entire LRU (default capacity 10 000) to find entries with dirty != null. Maintain a Set<string> of currently dirty keys, populated in set() (synchronously, so callers observe the key in _dirtyKeys immediately) and in _setLocked (after the entry is in the buffer), drained in markDone with a reference-equality guard so entry replacement during an in-flight write is handled correctly.
The constructor no longer arms a periodic timer. Instead, _setLocked calls _scheduleFlush() when it makes an entry dirty; the timer fires once, runs flush(), and clears itself. flush() re-arms the timer if new dirty entries remain (entry replacement during write). Idle databases consume zero timer ticks.
A cache-hit get() with no concurrent writer no longer constructs a SelfContainedPromise or mutates the per-key lock Map. The check (_locks.has(key)) and buffer read execute synchronously before the first await, so no writer can interleave; structuredClone produces a consistent snapshot. Adjusts test_metrics.spec.ts to reflect that cache-hit get() (but not getSub()) no longer increments lockAcquires or lockReleases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cloneIn preserves functions inside objects (the recursive call hits
the `typeof !== 'object'` branch and returns them as-is), so the
cache can hold values that structuredClone refuses. The previous
cloneOut comment claimed this was impossible by construction; it
isn't. Wrap structuredClone in a try/catch and fall back to cloneIn
on the failure path. V8 try/catch is essentially free on the happy
path; only function-containing reads pay the slower walker.
Restores parity with the pre-refactor clone() semantics for the
`set({fn: ...}) -> get()` round-trip via the cache. Adds a
regression test in test/memory/test_tojson.spec.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four internal-only wins in lib/CacheAndBufferLayer.ts: split clone() into cloneIn/cloneOut (structuredClone for the read path), maintain a dirty-key set so flush() does not scan the whole LRU, swap setInterval for an on-demand setTimeout, and add a lock-free fast path for get() cache hits. No API or default change; toJSON semantics preserved on the write path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TDD-structured plan for the four wins in the spec: cloneIn/cloneOut split, _dirtyKeys set, lazy setTimeout-based flush, and lock-free get() fast path. Five tasks (baseline + four wins + verification) broken into 2-5 minute steps with exact code, exact commands, and per-task commits.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Review Summary by QodoPerformance optimization of CacheAndBufferLayer with four compounding wins
WalkthroughsDescriptionPerformance optimization of CacheAndBufferLayer.ts with four compounding wins and comprehensive test coverage: • **Split clone() into cloneIn and cloneOut**: cloneIn preserves toJSON semantics for writes; cloneOut uses V8-native structuredClone for reads with fallback to cloneIn for non-serializable values • **Dirty-key Set tracking**: Replaced full LRU buffer scan in flush() with a Set of dirty keys, reducing iteration overhead from O(buffer size) to O(dirty entries) • **Lazy flush scheduling**: Replaced always-on setInterval with on-demand setTimeout armed only when entries go dirty; idle databases perform zero timer/scan work • **Lock-free fast path in get()**: Cache hits with no concurrent writer skip per-key lock acquisition/release, eliminating promise allocation and Map mutations • **Safety fallback**: cloneOut gracefully handles non-serializable values to prevent DataCloneError on read path • **Comprehensive test coverage**: New test files for dirty-key invariant, lazy flush scheduling, lock-free fast path, and toJSON fallback behavior • **Design documentation**: Includes implementation plan and design specification with correctness arguments and edge case analysis Diagramflowchart LR
A["Read Path"] -->|"uses structuredClone"| B["cloneOut"]
B -->|"fallback for non-serializable"| C["cloneIn"]
D["Write Path"] -->|"preserves toJSON"| C
E["set() call"] -->|"adds to"| F["_dirtyKeys Set"]
F -->|"replaces full scan"| G["flush()"]
H["First dirty entry"] -->|"arms"| I["_flushTimer"]
I -->|"cleared after"| G
J["get() cache hit"] -->|"no concurrent writer"| K["Lock-free fast path"]
K -->|"skips lock acquire/release"| L["Synchronous return"]
File Changes1. lib/CacheAndBufferLayer.ts
|
Code Review by Qodo
1. Timer armed during flush
|
perf(cache): hot-path wins in CacheAndBufferLayer
Part 2 of 3 from splitting #991. Scope:
lib/CacheAndBufferLayer.tsonly — no public API change, no default change, no behavioral regression (includingtoJSONsemantics). This layer sits on the hot path of every operation regardless of backend.Four compounding wins plus one safety fix:
1.
structuredCloneon the read pathclone()was a recursive deep-copy run on every read and write. Split into:cloneIn(the existingtoJSON-aware walker) for the write direction — keeps documentedtoJSONbehavior.cloneOutusing the V8-nativestructuredClonefor the read direction, where the cached value is already JSON-safe.2. Dirty-key Set
flush()scanned the entire LRU buffer (default cap 10 000) every interval just to find dirty entries. Now aSet<string>of dirty keys is maintained, soflush()iterates only what's actually dirty. Invariant +markDoneordering documented in code.3. Lazy flush scheduling
Replaces the always-on
setIntervalwith an on-demand,unref'dsetTimeoutarmed when the first entry goes dirty and cleared after flushing. Idle databases do zero timer/scan work.4. Lock-free fast path in
get()On a guaranteed cache hit with no in-flight writer,
get()now returns the cloned value without acquiring/releasing the per-key lock (saving a promise allocation + twoMapmutations per call). The fast path is fully synchronous; correctness argument is in the design doc and a code comment marks the no-awaitregion.Safety fix
cloneOutfalls back tocloneInfor valuesstructuredClonecan't handle (e.g. a stored{toJSON: fn}), so the read path never throwsDataCloneError.Tests
New, under
test/mock/:test_dirty_set,test_lazy_flush,test_lock_fast_path; plustest_metricsupdates and atest_tojsoncase for the fallback. All existing cache/metrics/LRU/bulk tests stay green.Verification
tsc --noEmitpasses.vitest run test/mock test/memory→ 185 passing.Independence
Touches only
CacheAndBufferLayer.ts+ tests + design docs — no overlap with the drivers PR, and the only overlap with the formatting PR is one test file (test_tojson.spec.ts). Can be reviewed in parallel.🤖 Generated with Claude Code