Skip to content

scratch: a fixed write-through read cache for the adapter's id tables - #577

Closed
luisleo526 wants to merge 7 commits into
mainfrom
scratch/id-table-read-cache
Closed

luisleo526 wants to merge 7 commits into
mainfrom
scratch/id-table-read-cache

Conversation

@luisleo526

Copy link
Copy Markdown
Collaborator

What

The adapter's cohort and pair id tables now read through a small, fixed-size, write-through page cache. Before this, every lookup in those tables was a real pread, so a run that re-reads the same ids paid one system call per lookup however hot the ids were.

Bounds

  • Pages are 4 KiB. The four adapter tables get 64 / 64 / 32 / 32 KiB (cohort AVL, exit-rank AVL, erased minima, consumed cycles; the pair stores share one allocation for data and index): 192 KiB per adapter, not per engine, independent of bars, ids and trades.
  • The default capacity of every table is zero and allocates nothing; only the adapter opts its four tables in. Allocation is lazy, uses nothrow, and falls back to direct reads if it fails.
  • Slab and arena backends (including unjailed regular-file arenas and inherited or sealed memfds) bypass the cache entirely: no allocation, tag probe or copied page.
  • docs/pages/memory-management.md gains one row for this cache.

Correctness

The cache never delays or reorders a write. Owners invalidate every overlapping line before the real write, including a write that may fail after a prefix has landed. Appends invalidate from the old tail page; index growth, clear, moves and a changed backend invalidate every line. Reads past the logical end go to the original reader, so short-read, EIO and errno text and the copied prefix are unchanged.

Tests

ctest -R test_p2cx_sc412 (13 targets): write, invalidate, eof, bypass, budget, default, append, fold-append, fsize, thrash, alloc-failure, plus a source-level run through the adapter. Each compares every returned value and the final file image with the uncached path, using digests recorded from the unchanged engine (frozen base 6b33c47f), and asserts that the cached run makes strictly fewer real read calls and never reads more bytes. The budget case forces evictions (working set larger than the slots) and also revisits a hot subset, so hits are guaranteed; it fails if the budget is ignored. On the unchanged engine the cache rows fail as intended (cache and direct counts equal, 20,761 preads each).

Not covered

No behaviour change is intended: the returned bytes are the same and only the number of reads changes. The paired parity sweep for this head is posted separately.

🤖 Generated with Claude Code

luisleo526 and others added 7 commits October 28, 2026 05:40
The adapter's cohort and pair id tables answered every lookup with a real pread, so a run that
re-reads the same ids paid one system call per lookup however hot the ids were. The tables now
read through a small cache of 4 KiB logical pages that is fixed in size and never grows with
bars, ids or trades: 64/64/32/32 KiB for the four tables, 192 KiB per adapter (not per engine).

The cache is write-through. It never delays or reorders a write: owners invalidate every
overlapping line before the real write, including a write that may fail after a prefix; growth,
clear, move and a changed backend invalidate everything. Reads past the logical end go straight
to the original reader so short-read, EIO and errno text and the copied prefix are unchanged.
Slab and arena backends (including unjailed regular-file arenas and inherited or sealed memfds)
bypass the cache without allocating. The default capacity of a table is zero; only the adapter
opts its tables in.

Tests compare every returned value and the resulting file image against the uncached path, with
digests recorded from the unchanged engine, and assert that the cached run makes strictly fewer
real read calls and never more bytes. They cover write, invalidation, end-of-file, bypass, the
byte budget, the default, append and fold-append, file-size changes, eviction pressure and
allocation failure. memory-management.md states the new fixed 192 KiB per adapter.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
# Conflicts:
#	docs/pages/memory-management.md
…he is invalidated

The base-golden comments and failure messages still told a person to fill in the constants; they
are recorded from the unchanged engine, so say that. The memory-management row said growth
invalidates from the old tail page, which is true for appends only: index growth, clear, moves and
backend changes invalidate every line.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The cmake.d checker requires every registered test name to have a source of the same name, so the
twelve mode rows registered as separate tests (test_p2cx_sc412_write, ..._budget, ...) were refused.
test_p2cx_sc412 now runs every row when it is started without an argument and still accepts a row
name as the first argument to rerun one row; the source-level program keeps its own test.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The read cache allocates its page buffer lazily with new (std::nothrow), once per cache; the source gate found
no entry for that site. U1524 records it as unreviewed (the fixed size is the author's statement: 4 KiB pages, at
most 16 lines per table, and is not proven by the gate). The unreviewed ceiling becomes 1278 and the pinned OPEN
count 1466 (1465 entries plus this one). ci/cr150-source-sites.json was re-derived with
cr150_source_gate.py --emit-site-inventory; the gate over the result reports entries 1466, matched 1466,
findings 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…the memory-management sentences it replaces

The read cache adds three lines before the cited lines of pine_adapter.hpp and moves lines in
id_pair_store.hpp, so 18 file:line citations in five pages are re-pointed at the lines that hold
the same symbols (numbers only; no page changes its line count).

The "Cohort id table" row of memory-management.md replaces two statements introduced by 30e9f9a
(docs: memory-management page re-read against main after the 1.8.0 changes): "The ordered index
lives on disk and is read through one 64 KiB window per scratch file" and the citation
"`grow_index`, `include/pineforge/detail/id_pair_store.hpp:N`)". They are replaced because
DiskResidentIdTable::find walks the disk tree on every lookup, even for resident rows, and those
reads now go through the id-table page cache described in the new row above it; the row now says
so, names exit_rank_keys_ as a separate plain DiskIdTable, and cites the index growth by symbol
instead of by a line number that moves.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@luisleo526

Copy link
Copy Markdown
Collaborator Author

Superseded by #593 (same tree; the first commit's message now names the two replaced sentences).

@luisleo526 luisleo526 closed this Oct 28, 2026
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