93 lines
5.1 KiB
Markdown
93 lines
5.1 KiB
Markdown
# 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:
|
||
|
||
```sh
|
||
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.
|