plan: tiering v3 (simpler, after A and B) #968

Closed
kayg wants to merge 0 commits from docs/tiering-v3 into dev
Owner

After two failed qualification attempts, this proposes the simpler reader and index design inside ADR 0062's fixed two-tree Home layout.

  • Explains the filesystem split costs and the extra per-request work in B.
  • Keeps one compact active ID map, a selected pinned root, and the existing app indexes; adds a seekable tag/order index.
  • Sets explicit map and cache limits, a failure table, strict faster-than-dev gates at 1k/10k/100k/1M, and numbered open questions.
  • Lists what to keep or remove from #761, #790–#793 and #953 if accepted.

Docs only. No product code or tests changed. git diff --check passes.

After two failed qualification attempts, this proposes the simpler reader and index design inside ADR 0062's fixed two-tree Home layout. - Explains the filesystem split costs and the extra per-request work in B. - Keeps one compact active ID map, a selected pinned root, and the existing app indexes; adds a seekable tag/order index. - Sets explicit map and cache limits, a failure table, strict faster-than-dev gates at 1k/10k/100k/1M, and numbered open questions. - Lists what to keep or remove from #761, #790–#793 and #953 if accepted. Docs only. No product code or tests changed. `git diff --check` passes.
docs: plan tiering v3 after A and B
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled
e6b16bd71d
Author
Owner

Perf profile and packed-map spike

Existing B qualification

The final qualification is recorded in PR #953 comment 79844. At the exact B head 3f9ce0fd6acbbcdcc5100668ebf2b2a50b6c1cb1 against dev 9377d711217532a44973edcf63c2af0b7f7969d8, it reports:

Path Size B p95 dev p95 Delta
API read 10k 10.121 ms 7.477 ms +35.4%
Append 100k 14.721 ms 12.107 ms +21.6%
Tagged page 100k 79.178 ms 56.293 ms +40.7%

B did save memory: settled 100k RSS was 114.705 vs 126.773 MiB, and idle RSS was 77 vs 91 MiB. Its configured global cache allowance was 704 MiB against the unchanged 64 MiB gate. These savings do not qualify the hot paths.

The full campaign command and lock order are in the linked #953 comment. In brief, both shared perf locks were held together, perf-01 first, with head/dev arms and same-session dev/dev controls.

Focused profile

The focused profile used the same B and dev source heads on perf-01, with the shared locks held together. Each arm recorded 24 reads, 200 appends and 40 tagged pages. The p95s below are diagnostic only:

Size Path dev p95 B p95
10k API read 8.806 ms 6.081 ms
10k Append 11.225 ms 14.283 ms
10k Tagged page 5.763 ms 7.191 ms
100k API read 6.336 ms 12.262 ms
100k Append 19.436 ms 16.508 ms
100k Tagged page 52.931 ms 52.158 ms

The dev/dev controls varied widely at 100k (read p95 6.336–21.544 ms; append 14.997–19.436 ms; tagged page 52.568–54.158 ms). This small profile did not reproduce the full append or tagged-page regressions, so those p95s are not qualification. The focused stage trace put most tagged-page time in the SQL candidate query; validating the returned page's file stamps (at most 50 rows) was small.

Extra work per request

  • Read: Notes::get calls path_of before asking the room, including when the room is already warm. In B, paged path_of locks the SQLite connection, queries pending, queries entries when no newer staged row exists, and allocates an owned path string. Dev's map returns a borrowed path.
  • Append: list-metadata refresh (and the saved-metadata path where it runs) calls the same path_of even though the room already owns its current relative path. That repeats the lock, SQL lookup and path allocation.
  • Tagged page: this route does not call the ID map. For common tags, the candidate query walks an order index and checks tag membership, while the count walks matching tag rows. v3's tag/order index and maintained count address that work; the map change alone cannot.

Packed-map byte spike

The packed row/arena layout measured 51.96 bytes per entry at 100k and 52.90 bytes per entry at a scaled 1M, under the plan's 56-byte ceiling. That is about 4.96 MiB and 50.45 MiB for the map alone. This was a buffer-backed layout measurement, not a production Rust map, RSS measurement or latency qualification. It does not include a claim that every long-path distribution fits; that remains a gate.

The 100k/1M map spike measured size only. The source-level profile and #953 full campaign remain separate evidence. No product code changed in this PR.

Previous full-campaign command, as recorded on #953:

TOB_ROOT="$HOME/tob-b2" TOB_HEAD_SHA=3f9ce0fd6acbbcdcc5100668ebf2b2a50b6c1cb1 TOB_DEV_SHA=9377d711217532a44973edcf63c2af0b7f7969d8 python3 "$HOME/tob/measure.py" --sizes 1000,10000,100000 --cycles 3
## Perf profile and packed-map spike ### Existing B qualification The final qualification is recorded in [PR #953 comment 79844](https://git.kayg.org/kayg/calternal/pulls/953#issuecomment-79844). At the exact B head `3f9ce0fd6acbbcdcc5100668ebf2b2a50b6c1cb1` against dev `9377d711217532a44973edcf63c2af0b7f7969d8`, it reports: | Path | Size | B p95 | dev p95 | Delta | |---|---:|---:|---:|---:| | API read | 10k | 10.121 ms | 7.477 ms | +35.4% | | Append | 100k | 14.721 ms | 12.107 ms | +21.6% | | Tagged page | 100k | 79.178 ms | 56.293 ms | +40.7% | B did save memory: settled 100k RSS was 114.705 vs 126.773 MiB, and idle RSS was 77 vs 91 MiB. Its configured global cache allowance was 704 MiB against the unchanged 64 MiB gate. These savings do not qualify the hot paths. The full campaign command and lock order are in the linked #953 comment. In brief, both shared perf locks were held together, perf-01 first, with head/dev arms and same-session dev/dev controls. ### Focused profile The focused profile used the same B and dev source heads on perf-01, with the shared locks held together. Each arm recorded 24 reads, 200 appends and 40 tagged pages. The p95s below are diagnostic only: | Size | Path | dev p95 | B p95 | |---:|---|---:|---:| | 10k | API read | 8.806 ms | 6.081 ms | | 10k | Append | 11.225 ms | 14.283 ms | | 10k | Tagged page | 5.763 ms | 7.191 ms | | 100k | API read | 6.336 ms | 12.262 ms | | 100k | Append | 19.436 ms | 16.508 ms | | 100k | Tagged page | 52.931 ms | 52.158 ms | The dev/dev controls varied widely at 100k (read p95 6.336–21.544 ms; append 14.997–19.436 ms; tagged page 52.568–54.158 ms). This small profile did not reproduce the full append or tagged-page regressions, so those p95s are not qualification. The focused stage trace put most tagged-page time in the SQL candidate query; validating the returned page's file stamps (at most 50 rows) was small. ### Extra work per request - **Read:** `Notes::get` calls `path_of` before asking the room, including when the room is already warm. In B, paged `path_of` locks the SQLite connection, queries `pending`, queries `entries` when no newer staged row exists, and allocates an owned path string. Dev's map returns a borrowed path. - **Append:** list-metadata refresh (and the saved-metadata path where it runs) calls the same `path_of` even though the room already owns its current relative path. That repeats the lock, SQL lookup and path allocation. - **Tagged page:** this route does not call the ID map. For common tags, the candidate query walks an order index and checks tag membership, while the count walks matching tag rows. v3's tag/order index and maintained count address that work; the map change alone cannot. ### Packed-map byte spike The packed row/arena layout measured 51.96 bytes per entry at 100k and 52.90 bytes per entry at a scaled 1M, under the plan's 56-byte ceiling. That is about 4.96 MiB and 50.45 MiB for the map alone. This was a buffer-backed layout measurement, not a production Rust map, RSS measurement or latency qualification. It does not include a claim that every long-path distribution fits; that remains a gate. The 100k/1M map spike measured size only. The source-level profile and #953 full campaign remain separate evidence. No product code changed in this PR. Previous full-campaign command, as recorded on #953: ```sh TOB_ROOT="$HOME/tob-b2" TOB_HEAD_SHA=3f9ce0fd6acbbcdcc5100668ebf2b2a50b6c1cb1 TOB_DEV_SHA=9377d711217532a44973edcf63c2af0b7f7969d8 python3 "$HOME/tob/measure.py" --sizes 1000,10000,100000 --cycles 3 ```
Author
Owner

Landed in dev via f299efc2f7 — closing.

Landed in dev via f299efc2f77c2d4d2216cabbe66ffd963b92475c — closing.
kayg closed this pull request 2026-11-07 17:53:04 +00:00
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Markdown extraction contract / markdown-native-contract (pull_request) Has been cancelled
Markdown extraction contract / markdown-editor-contract (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!968
No description provided.