Skip to content

perf(backend): bound undo replay via baseline squash + bench tool - #59

Merged
cafca merged 3 commits into
mainfrom
claude/dreamy-chatelet-c772e5
Apr 24, 2026
Merged

perf(backend): bound undo replay via baseline squash + bench tool#59
cafca merged 3 commits into
mainfrom
claude/dreamy-chatelet-c772e5

Conversation

@cafca

@cafca cafca commented Apr 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Undo/redo got slower as paint history grew: every undo rebuilt rated_areas by replaying every active paint_events row through PostGIS clipping — O(N) in total history.
  • Cap active events at BEEBEEBIKE_MAX_UNDO_HISTORY (default 15). When paint would exceed the cap, the oldest active event is squashed into a new rated_areas_baseline snapshot and deleted. Rebuild seeds rated_areas from the baseline then replays at most cap events — O(cap).
  • Startup backfill_baseline_once trims pre-existing heavy users to the cap (idempotent).
  • New just bench-undo / just bench-undo-prod run 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-ms minus 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_bench binary ships inside the backend Docker image, so no cargo/sqlx-cli is needed on the server:

# On helena (or any prod host with the compose stack):
just bench-undo-prod --depths 1,5,10,15,20,25,35,50 --iterations 10 --geometry-size medium

# Review the recommendation, then set in ~/beebeebike/.env:
# BEEBEEBIKE_MAX_UNDO_HISTORY=<recommendation>

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

  • Target enum gates table names to a static &'static str — no SQL injection surface despite format!.
  • apply_paint became pub so the undo_bench bin crate can call it; same for squash_excess_history / backfill_baseline_once.
  • Migration 008 only creates the table; the data backfill runs in Rust at startup so clipping semantics have one source of truth.
  • redo path now explicitly passes Target::RatedAreas (behavior unchanged).
  • target_id edit path in paint doesn't write paint_events so the cap doesn't apply there — unchanged.
  • The bench tool uses 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 warnings clean
  • just bench-undo --depths 1,5,10 --iterations 3 --geometry-size small — prints latency table + recommendation; throwaway DB created and dropped, zero artifacts in dev DB
  • CI builds the updated Docker image (adds undo_bench binary alongside beebeebike-backend)
  • just preview + manual: paint >15 strokes, undo rapidly, confirm snappy and stops at cap
  • On helena: just bench-undo-prod, set BEEBEEBIKE_MAX_UNDO_HISTORY in ~/beebeebike/.env

cafca added 3 commits April 24, 2026 14:43
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
@cafca
cafca merged commit 424129a into main Apr 24, 2026
5 checks passed
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