Skip to content

ADR 007: Stored, simplified map lines and a browser cache for the trip page

Context

A one-week track of 600+ NM (up to ~600 000 points at 1 Hz) made the trip page and the trips list slow. The cost was server time, not data sent: Trip#map_points loaded every point of every track on every render and only then thinned in Ruby (0.78 s for the trip map of a 166k-point trip, 0.41 s per card). It did so for every card in the list, for every Days ↔ Tracks switch, and for every Turbo morph refresh.

Options considered: downsampling per request (in SQL or Ruby), a lazy Solid Cache, PostGIS simplification (PostGIS is not installed), a separate map JSON endpoint with versioned URLs, extra detail when a day is focused, and caching on the Cloudflare CDN.

Decision

  1. The tab switch renders only the tab frame. A Turbo frame request for trip-view gets the frame variant (show.html+frame.erb) without the map and the numbers.
  2. Lines are simplified once and stored, never per request.
  3. Douglas–Peucker, not even thinning. Even thinning spends the budget on nights at anchor and cuts corners off tacks. The first and last point and both points of every engine switch are always kept, so sail/engine colours stay exact.
  4. Trip page line per track, 5 m tolerance, in its own table track_map_lines rather than a tracks column, because many pages load whole tracks without needing the line. It is computed where the points are already in memory (FIT import, segment recompute, trims). The trip map joins the tracks' lines, at most 10 000 points per trip.
  5. Card line per trip (trips.map_line_card), 50 m, at most 600 points. It is rebuilt in recompute_aggregates! after every change to any of the trip's tracks, from the tracks' stored lines and never from the points, so the rebuild takes milliseconds.
  6. Compact storage: BER varints with zigzag deltas and engine switches as indexes (MapLines::Codec), about 3 bytes per point.
  7. A track without a stored line falls back to every n-th kept point (ADR 017). The backfill (map_lines:backfill) runs after every deploy (deploy:after).
  8. The whole trip page is cached in the browser with an ETag (private, must-revalidate, Vary: Turbo-Frame), revalidated on every load. An unchanged page gets a 304 without rendering. The ETag covers everything the page shows and the release (config.x.boot_id, new on every boot). No public or CDN caching: the data is private per user, and the app is not behind Cloudflare's proxy (DNS only).
  9. Not now: a separate map JSON endpoint, more detail for a focused day, Cloudflare for public trips, PostGIS simplification.

Consequences

  • Trip map 0.78 s → 0.002 s, card 0.41 s → < 0.001 s (166k-point trip). For a 600k-point week: 0.06 s and 0.001 s, against 1.7 s before just to load the points.
  • The maps are only as current as the last recompute. Every path that changes points or trims must store the line (Ingestion::Derivation#store_map_line), and every change to a trip's tracks must run recompute_aggregates!.
  • The trip map never shows more than 10 000 points; detail below 5 m (about GPS accuracy) is not drawn.
  • Every deploy invalidates all trip page ETags once (boot_id).
  • Storing a line costs about 1 s per 100 000 points in the jobs.