8000 feat(query): queries, cursors and BrowserDb read from pinned snapshots (#543, PR 3 of 3) by adityamukho · Pull Request #580 · project-minigraf/minigraf · GitHub
[go: up one dir, main page]

Skip to content

feat(query): queries, cursors and BrowserDb read from pinned snapshots (#543, PR 3 of 3) - #580

Merged
adityamukho merged 3 commits into
v3from
feat/543-snapshots-read
Oct 13, 2026
Merged

adityamukho merged 3 commits into
v3from
feat/543-snapshots-read

Conversation

@adityamukho
Copy link
Copy Markdown
Collaborator

Part of #543 (streaming engine 1/9, #473): plan steps 5, 8, 9, 10 of docs/superpowers/plans/2026-10-11-543-snapshots-and-quarantine.md. PR 1 was #578, PR 2 was #579.

Changes

  • Snapshots for every read (step 5). Snapshot is Clone (pin in an Arc), carries now and the read methods (live_facts_of_scans, all_facts, facts_as_of); its pending reads stop at its length on every scan path. DatalogExecutor::set_snapshot: Minigraf::execute/query, PreparedQuery::execute/query and BrowserDb.query read one snapshot per query. Valid-time "now" is the snapshot's time, taken together with the pending cutoff.
  • Cursors pin. Cursor holds its snapshot until dropped (decision 7), so checkpoints and rebuilds run but do not reuse its generation's pages.
  • BrowserDb (step 8). Free pages in force are withheld from release (bytes kept, no IndexedDB delete) and released by a later commit once out of force. pins::split_released decides; a withheld id the commit wrote again is live and dropped from both lists. This fixes the PR 2 Codex note (released = listed included quarantined ids).
  • Crash test (step 9). A crash at every write point of a save (plain and torn writes) while a snapshot pins an older generation: the pinned reader is unchanged, the reopened database has every fact, verify() is clean, space is accounted for, and later checkpoints reuse the quarantined ids.
  • Docs (step 10). CHANGELOG (cursor pinning, the half-published checkpoint fix), DURABILITY, CLAUDE.md, TEST_COVERAGE (1466 tests: 1456 passing, 10 ignored). The docs-site PR will follow separately.

Deviations from the plan

  • The plan kept Cursor::from_result without a snapshot for BrowserDb. Instead, browser cursors pin too, so step 8's withholding is actually used and tested (otherwise it would be dead code).
  • split_released takes a written predicate (the buffer's dirty set) besides listed/withheld/in_force. An id withheld earlier can be handed out by the commit that sees it out of force, and its bytes must not be dropped.
  • Docs site: the edited text is all under @since v3.0.0 (never released), so it is edited in place rather than wrapped in @until/@since.

Tests

  • tests/cursor_test.rs: a cursor open across writes, checkpoints and a rebuild returns its answer at open; the file grows over 4× the no-cursor growth while it is open (checked to fail with the pin removed) and stops growing after close; a prepared cursor does the same.
  • graph::storage: a snapshot leaves out later appends on all four scan kinds.
  • storage::pins: four split_released unit tests.
  • browser: a #[wasm_bindgen_test] keeps pages pinned by an open cursor readable across 20 commits and releases them afterwards (checked to fail with withholding removed). wasm-pack test --headless --firefox --features browser --lib: 33 passed.
  • cargo test: 1456 passed, 10 ignored. snapshot_atomicity_test passed 15 runs in a row. Clippy is clean on native (--all-features), wasm32-unknown-unknown with browser, and wasip1.

Review

Codex (codex exec -s read-only) found one issue: now was read after the pending lock was released. It is fixed in 1aa2bea. A transaction stamped before a snapshot but appended after it is still left out. That state really existed, and the behaviour is the same as before.

Pending

🤖 Generated with Claude Code

https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR

adityamukho and others added 2 commits October 13, 2026 21:32
#543)

PR 3 of 3 for #543 (plan steps 5, 8, 9, 10).

- Snapshot gains Clone (pin in an Arc), now, and the read methods
  (live_facts_of_scans, all_facts, facts_as_of); a snapshot's pending reads
  stop at its length on every scan. FactStorage's reads go through one.
- DatalogExecutor reads from one snapshot (set_snapshot); Minigraf::execute
  and query, PreparedQuery::execute and query, and BrowserDb.query take one.
  "now" for valid-time is the snapshot's time.
- Cursor holds its snapshot, so it pins the committed generation until it is
  dropped, like a fact log.
- BrowserDb: free pages in force are withheld from release (no dropped bytes,
  no IndexedDB delete) and released by a later commit once out of force;
  pins::split_released decides.
- Crash test: a crash at every write point of a save with a pinned
  generation loses no fact or page, and the reopened database reuses the
  quarantined ids.
- Docs: CHANGELOG, DURABILITY, CLAUDE.md, TEST_COVERAGE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR
Codex review: now was read after the pending lock was released, so an
append in between was left out of a snapshot whose now was past it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyLscCUuJ1GQpdAeX79EdR
@codecov
codecov Bot commented Oct 13, 2026
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.42105% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/graph/storage.rs 90.00% 4 Missing ⚠️
src/storage/pins.rs 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

# Conflicts:
#	CLAUDE.md
#	docs/TEST_COVERAGE.md
@adityamukho
adityamukho merged commit d4dabb0 into v3 Oct 13, 2026
21 checks passed
@adityamukho
adityamukho deleted the feat/543-snapshots-read branch October 13, 2026 17:25
@adityamukho
Copy link
Copy Markdown
Collaborator Author

#394 perf envelope (--profile envelope, --scales 100k,1m): v3 base 27951e3 vs this PR, run alternately base/PR/base/PR on an idle machine (other sessions idle; load 1.2–1.75 from the runs themselves). p50 for each of the two runs; Δ compares the best of each.

scale shape base p50 #580 p50 Δ best
100k point_entity 13 µs / 15 µs 12 µs / 13 µs -6.1%
100k attribute_value 24.0 ms / 25.0 ms 24.1 ms / 23.7 ms -1.5%
100k attribute_scan 41.9 ms / 45.2 ms 42.0 ms / 42.6 ms +0.3%
100k join_2 49.4 ms / 51.1 ms 50.6 ms / 52.3 ms +2.3%
100k join_3_selective 109.9 ms / 109.7 ms 106.1 ms / 108.6 ms -3.3%
100k join_3_scan 142.2 ms / 148.8 ms 142.8 ms / 139.6 ms -1.8%
100k recursive_ancestors 2605.1 ms / 2705.9 ms 2709.3 ms / 2781.9 ms +4.0%
100k as_of 12 µs / 14 µs 13 µs / 14 µs +6.0%
100k valid_at 13 µs / 15 µs 14 µs / 13 µs +0.9%
100k count_grouped 42.7 ms / 45.4 ms 45.6 ms / 44.2 ms +3.6%
100k hot_current 233 µs / 249 µs 248 µs / 235 µs +1.0%
100k hot_as_of 233 µs / 242 µs 249 µs / 250 µs +6.7%
100k checkpoint_10k_ms 40.0 ms / 41.4 ms 40.3 ms / 38.1 ms -4.7%
1m point_entity 12 µs / 13 µs 12 µs / 12 µs +1.4%
1m attribute_value 279.7 ms / 298.4 ms 294.1 ms / 274.7 ms -1.8%
1m attribute_scan 433.6 ms / 459.1 ms 445.3 ms / 471.3 ms +2.7%
1m join_2 580.9 ms / 595.3 ms 596.9 ms / 634.8 ms +2.7%
1m join_3_selective 1211.1 ms / 1194.6 ms 1196.2 ms / 1282.0 ms +0.1%
1m join_3_scan 1867.6 ms / 1864.9 ms 1865.5 ms / 1882.4 ms +0.0%
1m recursive_ancestors 35124.7 ms / 36206.7 ms 37702.0 ms / 39759.1 ms +7.3%
1m as_of 13 µs / 13 µs 14 µs / 15 µs +3.0%
1m valid_at 14 µs / 14 µs 14 µs / 15 µs +3.8%
1m count_grouped 501.3 ms / 505.1 ms 497.0 ms / 520.6 ms -0.8%
1m hot_current 237 µs / 237 µs 265 µs / 269 µs +11.8%
1m hot_as_of 238 µs / 245 µs 281 µs / 241 µs +1.3%
1m checkpoint_10k_ms 77.4 ms / 95.8 ms 77.6 ms / 79.9 ms +0.2%

Point queries (point_entity, as_of, valid_at) are unchanged at both scales: within ±6% at 12–15 µs, which is run-to-run noise. Two results are outside noise and need a closer look: hot_current at 1M is +12% (265/269 µs vs 237/237 µs, consistent across both runs), and recursive_ancestors at 1M is +7% (5 runs per sample). Everything else is within ±4%.

@adityamukho
Copy link
Copy Markdown
Collaborator Author

Follow-up to the envelope comparison above, re-measured on an idle machine with alternating and swapped runs:

  • hot_current and hot_as_of at 1M: no regression. The PR equals base within 1–2%, so the +12% was noise.
  • recursive_ancestors at 1M: a real +24% (45.9 s → 56.4 s). It's caused by an extra full copy of the pending facts in Snapshot::all_facts. Fixed in perf(storage): clone pending facts straight into all_facts (#543) #587, which brings it back to 45.0 s.

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

0