perf(backend): bound undo replay via baseline squash + bench tool - #59
Merged
Conversation
Undo latency grew O(N) with paint history because every undo replayed all active paint_events through PostGIS clipping. Cap active events at BEEBEEBIKE_MAX_UNDO_HISTORY (default 15); oldest events squash into a new rated_areas_baseline snapshot so rebuild starts from the baseline and replays at most `cap` events. Startup backfill trims pre-existing users. Adds `just bench-undo` (undo_bench binary) to measure p50/p95/p99 replay latency at various stack depths and recommend a cap for the deployment.
- Bench creates its own `beebeebike_bench_<uuid>` database on the target Postgres, runs embedded migrations, seeds synthetic data, and drops the database on exit. The application DB is never touched — safe against prod, no --confirm-prod guard needed. - Ship `undo_bench` inside the backend Docker image so it runs on any host that has the image, with no cargo/sqlx-cli dependency. - `just bench-undo` auto-starts the dev db container and runs the bench locally via cargo. `just bench-undo-prod` execs the shipped binary inside the prod backend container.
…t-c772e5 # Conflicts: # backend/src/ratings.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
rated_areasby replaying every activepaint_eventsrow through PostGIS clipping — O(N) in total history.BEEBEEBIKE_MAX_UNDO_HISTORY(default 15). When paint would exceed the cap, the oldest active event is squashed into a newrated_areas_baselinesnapshot and deleted. Rebuild seedsrated_areasfrom the baseline then replays at mostcapevents — O(cap).backfill_baseline_oncetrims pre-existing heavy users to the cap (idempotent).just bench-undo/just bench-undo-prodrun a self-contained benchmark: creates a throwaway database on the target Postgres, runs embedded migrations, seeds synthetic data, benchmarks replay across a range of stack depths, drops the database, and recommends a cap based on--target-msminus assumed--rtt-ms. Safe against prod — the application DB is never touched.Why
Heavy users hit 100s–1000s of paint events; each undo became perceptibly sluggish. Baseline squash keeps the existing replay model (and its PostGIS clipping semantics) intact while bounding work to a small constant. Storage per user also trends down — old events collapse into a merged non-overlapping polygon set in the baseline.
How to tune on prod
The
undo_benchbinary ships inside the backend Docker image, so no cargo/sqlx-cli is needed on the server:For a fresh server with no stack yet:
docker compose -f compose.prod.yml up -d db backend && just bench-undo-prod …— same recipe.Notes for reviewer
Targetenum gates table names to a static&'static str— no SQL injection surface despiteformat!.apply_paintbecamepubso theundo_benchbin crate can call it; same forsquash_excess_history/backfill_baseline_once.008only creates the table; the data backfill runs in Rust at startup so clipping semantics have one source of truth.redopath now explicitly passesTarget::RatedAreas(behavior unchanged).target_idedit path inpaintdoesn't writepaint_eventsso the cap doesn't apply there — unchanged.sqlx::migrate!("./migrations")(embedded at compile time), so the binary is self-sufficient inside the slim Debian runtime image.Test plan
just test-backend— 49 integration tests pass, including 4 new ones (cap squash, undo past cap, redo after squash, legacy backfill)just lint-backend— fmt + clippy-D warningscleanjust bench-undo --depths 1,5,10 --iterations 3 --geometry-size small— prints latency table + recommendation; throwaway DB created and dropped, zero artifacts in dev DBundo_benchbinary alongsidebeebeebike-backend)just preview+ manual: paint >15 strokes, undo rapidly, confirm snappy and stops at capjust bench-undo-prod, setBEEBEEBIKE_MAX_UNDO_HISTORYin~/beebeebike/.env