Repository source: docs/plans/php_incremental_read_model_write_slices_v1.md
PHP Incremental Read-Model Write Slices V1
This document outlines a production-safe plan for reducing write latency by removing unnecessary full read-model rebuilds from the hot path while preserving a full-rebuild recovery path.
Problem Summary
Recent Server-Timing data for POST /compose/thread shows that the write path is dominated by read_model_rebuild:
write_file: negligiblegit_add: smallgit_commit: smallgit_rev_parse: smallread_model_rebuild: ~5.4 secondsartifact_invalidate: negligible
That means the user-visible delay is not in canonical file creation or git. It is in the current strategy of fully rebuilding SQLite derived state after each write.
Today, LocalWriteService does:
write canonical record
commit canonical record to git
fully rebuild the SQLite read model
invalidate affected static artifacts
That keeps the read model simple and recoverable, but it makes a one-record write cost scale with total repository size.
Recommended Approach
Use four slices:
instrument the rebuild internals so the slow subphases are explicit
remove the worst full-rebuild hot loops to improve latency even before architecture changes
replace full rebuilds for thread/reply writes with incremental SQLite mutations
keep full rebuild as the repair and fallback path, and verify incremental parity against it
This sequence improves performance early, keeps risk bounded, and avoids switching to incremental updates blind.
Slice 1: Rebuild Subphase Instrumentation
Status:
implemented
rebuild subphases now flow into write-path
Server-Timingasread_model_<phase>direct test coverage added for rebuild timing capture and header formatting
Goal:
identify which full-rebuild subphases dominate the current
read_model_rebuildtiming
Expected changes:
add internal phase timing inside
ReadModelBuilder::rebuild()measure at least:
drop_schemacreate_schemaindex_postsindex_profilesderive_approval_statelink_post_authorsindex_instanceindex_activitywrite_metadata
surface those timings back through the existing write timing path, either:
folded into
Server-Timing, orlogged in a structured form tied to the write request
Verification approach:
a thread/reply POST exposes rebuild subphase timings
timings are stable enough across repeated writes to identify the largest bucket
no behavior change to canonical writes or rendered pages
Risks or open questions:
avoid making timing output too noisy for normal operation
decide whether subphase timing should always be on or be guarded by an environment flag
Components touched:
src/ForumRewrite/ReadModel/ReadModelBuilder.phpsrc/ForumRewrite/Write/LocalWriteService.phpsrc/ForumRewrite/Application.phptests covering timing formatting if needed
Slice 2: Full-Rebuild Hot-Loop Optimization
Status:
implemented
thread aggregates now build in one pass during
indexPosts()profile counts now derive from the already-loaded post list instead of per-profile SQL rescans
rebuild writes now run inside one SQLite transaction
Goal:
materially reduce rebuild cost without changing the overall rebuild contract
Expected changes:
optimize
indexPosts()so thread aggregates are computed in one pass instead of rescanning all posts per threadreplace per-profile
countVisibleRows()scans with accumulated counts derived from already-loaded postsconsider explicit SQLite transaction boundaries around rebuild writes if they are not already effectively batched
keep the final read-model contents identical to the current rebuild output
Verification approach:
Server-Timingshowsread_model_rebuildmaterially lower than the current baselineall existing tests still pass
a rebuild from scratch produces the same query-visible results as before
Risks or open questions:
refactoring
indexPosts()may accidentally change ordering or derived thread metadataprofile counts and hidden-post filtering must preserve current semantics exactly
Components touched:
src/ForumRewrite/ReadModel/ReadModelBuilder.phpread-model and smoke tests as needed
Slice 3: Incremental Thread/Reply Read-Model Updates
Status:
implemented
warm thread/reply writes now use an incremental SQLite updater instead of a full rebuild
cold or stale read-model states still fall back to full rebuild
timing output now distinguishes
read_model_incremental_*from rebuild timings
Goal:
stop performing full rebuilds on the hot path for the most common writes: thread creation and reply creation
Expected changes:
introduce an incremental read-model updater for
createThread()andcreateReply()for thread creation:
insert one
postsrowinsert one
threadsrowinsert one
activityrowupdate author/profile counts if
author_identity_idis presentupdate metadata such as
repository_head,rebuilt_at, and reason
for reply creation:
insert one
postsrowupdate the target
threadsrow forlast_activity_at,reply_count, andlast_post_idinsert one
activityrowupdate author/profile counts if applicable
update metadata
keep
linkIdentity()andapproveUser()on full rebuild initially unless their incremental paths are also straightforward and justified by measured latency
Verification approach:
thread/reply POST
Server-Timingno longer includes multi-second full rebuild workcreated thread/reply pages, board, activity, and profile counts all reflect the write immediately
targeted tests cover both anonymous and authored writes
Risks or open questions:
incremental mutation logic can drift from full rebuild semantics
approval-dependent profile fields must not be accidentally recomputed incorrectly
metadata updates must continue to support stale detection and future rebuild decisions
Components touched:
src/ForumRewrite/Write/LocalWriteService.phplikely a new read-model mutation helper or service
src/ForumRewrite/ReadModel/*support codewrite and smoke tests
Slice 4: Fallback, Repair, and Parity Verification
Status:
implemented
incremental write failure now falls back to a same-request full rebuild when possible
fallback failure still marks derived state stale
parity coverage compares incremental thread/reply results to a fresh rebuild of the same repository state
Goal:
preserve recoverability and prove that incremental updates remain equivalent to a fresh rebuild
Expected changes:
keep full rebuild available as:
startup repair path
stale-marker recovery path
fallback when incremental mutation fails
on incremental update failure after canonical commit:
mark derived state stale
fail safely with a clear error or force repair on the next read
add parity verification tests that compare:
incremental thread/reply result
full rebuild result from the same canonical repository state
Verification approach:
forced incremental failure marks the derived state stale and does not corrupt canonical writes
a subsequent full rebuild repairs the read model
parity tests confirm that incremental and full rebuild outputs match for the supported write flows
Risks or open questions:
fallback behavior should not leave partially updated SQLite state looking valid
parity coverage must be broad enough to catch hidden semantic drift
Components touched:
src/ForumRewrite/Write/LocalWriteService.phpsrc/ForumRewrite/ReadModel/ReadModelStaleMarker.phptest coverage for failure and parity behavior
Acceptance Criteria
This work is complete when:
thread/reply write latency is no longer dominated by full rebuild cost
rebuild subphase timing identifies and verifies the main performance wins
thread/reply writes update SQLite derived state without a full rebuild on the hot path
full rebuild remains available as a safe repair mechanism
tests demonstrate parity between incremental updates and a fresh rebuild for supported flows
Recommended Implementation Order
Slice 1 first, so the rebuild bottlenecks are measured instead of inferred
Slice 2 next, because it should produce immediate latency improvement with low architectural risk
Slice 3 after that, because incremental thread/reply updates are the main hot-path fix
Slice 4 last, because fallback and parity checks make the faster path production-safe
Summary
The measured problem is not git and not file I/O. It is synchronous full read-model rebuild on every write.
The safest path is:
measure rebuild internals
optimize the current rebuild
move thread/reply writes to incremental read-model mutation
keep full rebuild as the correctness backstop