5.1 KiB
Persisted run analysis and compact routes
Imports and recording refreshes now calculate summaries, GAP, terrain breakdowns,
lap GAP and charts from the full-resolution samples, in the same transaction as
the activity. analysis_version identifies the calculation/display version.
Current-version empty results distinguish incomplete recordings from pending
backfills. Raw recordings remain intact.
The page reads analysis_summary; the detail JSON endpoint reads display_data.
Both are deferred JSON columns so activity lists and record comparisons do not
load them. No page request calculates full-resolution analysis or reads raw laps.
Pending activities show unavailable derived metrics until backfilled.
Dashboard maps use the authorized /activities/<id>/route endpoint, which reads
only the persisted route_geometry and returns coordinates. The full detail data
remains protected by the same owner/friend/public-link access checks as before.
No shared caching is introduced for private responses.
Route display combines Douglas-Peucker simplification (initial tolerance 3 m) with regular measurement samples for colours. Tolerance increases for complex routes to fit a target of 1,400 points across the recording. Each GPS segment's endpoints are retained, so heavily fragmented routes may exceed that target; gaps are never joined to meet a budget. Coordinates are rounded to six decimals. Map measurements use only distance, speed, HR and grade; other raw fields are omitted. Chart data retains up to 750 evenly spaced samples, including endpoints. Calculations and personal records always use full-resolution source data.
The dashboard replay uses canvas rendering and binary search through route edges instead of a linear search on each animation frame.
After the schema migration, backfill existing activities:
flask --app app backfill-analysis --batch-size 25
# Dokku avoids interpreting Flask's --app option as a Dokku option:
dokku run stride env FLASK_APP=app flask backfill-analysis --batch-size 25
Each batch commits independently and uses PostgreSQL row locks. Reruns skip
current-version activities. --owner-id ID scopes the job; --force rebuilds
current results. The job fixes an upper activity ID at start; concurrent imports
calculate their own results. No downloads or Garmin authentication are needed.
Baseline measured on production on 2026-10-02, before this change: longest run 6,845 samples; median of three internal authenticated requests: dashboard HTML 8 ms, run HTML 156 ms, run JSON 213 ms / 2,654 KiB uncompressed. These timings exclude network transfer, browser rendering and map tile downloads.
After deployment and backfill on 2026-10-02: all 19 activities at analysis version 1; rerun processed zero rows. Same longest run retained 6,845 raw samples and used 1,001 display route points. Persisted summary exactly matched a fresh full-resolution calculation. Median of three internal authenticated requests:
| Response | Server time | Uncompressed size |
|---|---|---|
| Dashboard HTML | 9 ms | 27 KiB |
| Longest run HTML | 8 ms | 16 KiB |
| Longest run JSON | 29 ms | 313 KiB |
| Longest run route-only JSON | 7 ms | 23 KiB |
Page and JSON requests issued zero raw sample/lap queries. HTTPS health returned
HTTP 200 and status: ok; Dokku web and sync worker startup checks succeeded.
The synthetic browser fixture verified dashboard replay, route colour switching
and charts. Its in-memory SQLite server uses one request thread to avoid concurrent
access to the shared SQLite connection.
Paginated history
Dashboard, shared profiles and the friends feed show 25 activities per page, ordered by start time descending with activity ID as the tie-breaker. Navigation preserves profile URLs and returns to the history section. Invalid or empty out-of-range pages return 404; an empty first page remains valid.
Dashboard and profile totals are SQL aggregates over the entire authorized history. Eight weekly buckets use independent local-time Monday boundaries converted to UTC, including daylight-saving changes. The latest GPS run is an independent indexed query, so paging does not change the map. Friend/profile filters apply equally to pagination, counts, totals and route selection.
Personal record comparisons stream only ID, title, timestamp, sport, calculation version and stored efforts in batches of 1,000. They retain at most six current record holders and effort details for the requested page (or one run's detail). No full-history Activity ORM objects, raw samples, charts or laps are loaded. Comparisons remain linear in the number of recorded runs; page rows and ORM object counts are bounded. Owners are loaded with the page to avoid per-row athlete queries. Composite indexes support owner/date and shared owner/date lists.
A disposable local SQLite benchmark with 10,000 synthetic activities returned 25 rows and 26 ORM objects on pages 1, 200 and 400. HTML remained about 30 KiB; measured request times were 148, 122 and 97 ms respectively. These are synthetic local measurements, not production latency guarantees. Browser checks with 55 synthetic activities verified pages 1–3, including the five-row final page.