Startup: batch the seq repair (per-point loop made cold start ~15s, every boot)

The startup seq repair (runMigrations) assigned seq to NULL-seq sessions
one UPDATE per point. Merge/split leave seq=NULL by design, so recent
merged tracks (10865 + 3614 points) meant ~15000 sequential IPC writes on
every launch - and for the big sessions the writes did not persist before
the next launch re-ran the whole thing. That was the slow start.

Replace it with the batched CASE re-sequence (chunks of 300, like
saveGapFill/saveSmoothedTrack): ~50 UPDATEs instead of ~15000. Verified on
device: repair of all 3 sessions dropped from ~15s to ~1.1s, seq now
persists so it is one-time (boot 2 does no repair, migrations 357ms), 0
NULL seq left, seq matches ts order (0 inversions).
This commit is contained in:
4aiman 2026-09-29 12:37:15 +03:00
commit 74f3ec4d6e
3 changed files with 68 additions and 6 deletions

View file

@ -22,7 +22,7 @@
| Файл | Строк | Назначение |
|------|-------|------------|
| `js/main.js` | ~5900 | Экраны, навигация, модалки, графики, нутриенты, HR UI, компас (EMA + gimbal lock), ландшафт (3 режима), дороги. `appConfirm`/`appPrompt` (in-app диалоги вместо нативных, в стеке окон). Страница «Сохранённые маршруты», оверлей маршрута на записи и просмотре (`whenMapReady` — await готовности карты, не таймаут). Панель де-джиттера на детали трека (ползунки окно/итерации, живое превью, сохранить/откат, `reopenDetail` — переоткрытие детали без наложения оверлеев). `showRouteDetail` — предпросмотр маршрута (карта+профиль высоты+тап-высоты+фуллскрин), `showRouteEditor` — создание/правка маршрута (тап карты=точка в конец, тап линии=вставка в сегмент, тащить=двигать, тап точки=удалить; `makeMarker` возвращает маркер для push/splice). `appConfirm`/`appPrompt`/`appAlert` — in-app диалоги (нативных `alert/prompt/confirm` не осталось; `<select>` оставлены). `orient-dbg` (сенсор-оверлей на карте) — только при `window.__BALDAV_DEBUG`. `drawRoutePolyline`/`routeSlopeColor` (полосы уклона по 1%)/`routeElevColor` — топо/уклон-обводка в панели `routePane` (маршрут ПОД треком; на rotate-карте панель вешается внутрь `_rotatePane`, иначе маршрут не виден при записи). `fetchElevations` — цепочка GPXZ→open-elevation→open-meteo→OpenTopoData. Диагностические `console.log("[тег]...")` глушатся в release по `get_app_info().debug` (cfg!(debug_assertions)) |
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints`/`addRoute(...,waypoints)` — плотная геометрия + `waypoints_json` {waypoints,legMode} для правки планировщиком, `getRoutes.hasEle/.wpts`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера. `saveGapFill`/`revertGapFill` — обратимый гэп-филл: вставка достроенных точек с `estimated=1` (seq = seq предыдущей реальной точки) / удаление их; колонки `gps_points.estimated`, `gps_sessions.gap_filled`. Nogo: таблица `nogo_areas` (lat/lon/radius_m) + `getNogoAreas`/`addNogoArea`/`deleteNogoArea` |
| `js/db.js` | ~1550 | SQLite через Tauri IPC. Миграции, CRUD, 41 поле добавок, water_pct, reserves, sync. Хелперы маршрутов (routes CRUD, `updateRoutePoints`/`addRoute(...,waypoints)` — плотная геометрия + `waypoints_json` {waypoints,legMode} для правки планировщиком, `getRoutes.hasEle/.wpts`). `saveSmoothedTrack` — чанковый `CASE`-апдейт lat/lon_smoothed (18 UPDATE на 4366 точек вместо 4366), `setSessionSmoothed` — откат де-джиттера. `saveGapFill`/`revertGapFill` — обратимый гэп-филл: вставка достроенных точек с `estimated=1` (seq = seq предыдущей реальной точки) / удаление их; колонки `gps_points.estimated`, `gps_sessions.gap_filled`. Nogo: таблица `nogo_areas` (lat/lon/radius_m) + `getNogoAreas`/`addNogoArea`/`deleteNogoArea`. `runMigrations` -> стартовый seq-репейр для NULL-seq сессий (merge/split создают их намеренно): батчевый `CASE` по 300 точек (как `saveGapFill`), а не UPDATE на точку - иначе ~15000 IPC на старте = ~15с и не досохранялось, гонялось каждый запуск |
| `js/gps.js` | ~1650 | GPS запись, ккал (нетто-модель + MET-нетто floor, накат=базовый), altitude filter + гистерезис высоты, BLE HR polling, dead reckoning. Источник истины БД при живой записи — нативный файл (`_processIncomingPoint(p, fromNative)`: watchPosition только рисует, поллинг пишет). `recoverFromFiles` — восстановление точек с устройства (матч по id/дате + `_s<id>` в имени, дедуп ±2.5с, `recalculateSession(noDelete)`, чистка импортированных файлов). `recomputeAllStats` — пересчёт статы всех треков без изменения точек. `filterByAccuracy` — отбор по точности (minAccuracyM) внутри `computeStats`+`filterNoise`: неточные фиксы исключаются из расчёта/линии (обратимо, non-destructive; null/≥999 keep), bearing_deg. `smoothTrackSG(pts,W,it)` — де-джиттер Савицкого-Голея (квадратичный МНК по окну, итеративно, концы закреплены; lat/lon→метры→обратно, все поля точки сохраняются). `parseRouteGPX(xml)` — парсер маршрутов (`rtept`/`trkpt`/`wpt`, читает `<ele>`, DOMParser). `routeElevStats(pts)` — профиль маршрута (накопл. дистанция, грейд к след. точке, набор/сброс с гистерезисом, макс-уклон; высоты сглаживаются). `fillGaps(points)` → `{points, gaps}` — дорожная достройка GPS-дропаутов (BRouter shortest) с интерполяцией ts и флагом `estimated`; `gaps[].status` = filled/skipped-long/skipped-noroute для UI-предпросмотра. `config.gpxzKey` — ключ GPXZ (localStorage) |
| `js/sync.js` | ~300 | Синхронизация между устройствами (LAN, PHP сервер на роутере) |
| `js/chzn.js` | ~330 | Честный знак (CRPT). Скан DataMatrix → карточка Нацкаталога → префилл КБЖУ. `ChZ`: сессия в localStorage (`chzn_session`), `importFromText`, `refresh`/`ensureSid` (sid 48ч, refreshToken ~год), `authedPost/Get` (userId+sid в query, авто-refresh на 401), `check`, `nutritionInfo` (витамины, часто 204), `normalizeCode` (реконструкция GS 0x1D перед AI 93), `parseCard` (группа «Энергетическая ценность…», имена атрибутов разнятся у производителей), `resolve`. `/authorize/anonymous` закрыт Play Integrity — свою сессию не завести, только импорт из приложения (`tools/chzn_export.sh`) |

View file

@ -0,0 +1,49 @@
# Сессия 2026-09-29 - долгий старт БалДава + .gitignore
По утверждённому плану `~/.claude/plans/merry-discovering-dream.md` (замерить -> починить).
## Задача
1. build.sh + BUILD_NUMBER в .gitignore (в build.sh пароль роутера, не место в git).
2. Разобраться, почему БалДав стал долго стартовать, и починить.
## .gitignore
Добавлены `build.sh` и `BUILD_NUMBER` (оба были untracked, не в игноре). Коммит 2d91c06.
## Диагностика старта (по требованию - сначала замер)
Собирается только debug (release не рассматриваем), значит причина не в профиле сборки, а в
работе на каждом старте. Замер на устройстве (adb am start -W + логи Tauri/Console, тайминг-логи
временно добавлял в init/lib.rs, потом убрал):
- `am start -W` TotalTime ~400ms (первый кадр - экран загрузки, быстро). Медленно - JS-init
ПОСЛЕ первого кадра, он не виден в TotalTime.
- В логах: `[db.js] Repairing seq for 3 sessions...` -> сессии 687 + 3614 + 10865 точек с
`seq IS NULL`. Репейр (db.js `runMigrations`, ~2019) делал **по одному UPDATE на точку** -
~15000 последовательных IPC-round-trip'ов в debug-Rust. По таймингам одна сессия 3614 =
~3.8с, сессия 10865 не досчитывалась.
- Ключевое (проверено через `run-as cat baldav.db` + sqlite3, journal_mode=delete, WAL ни при
чём): репейр гонялся на КАЖДОМ старте (2 подряд без действий пользователя) и для крупных
сессий seq НЕ сохранялся -> заново каждый запуск. Это и есть "долго": ~15с блокирующего
init'а на старте.
Причина "стал": недавние merge/split (свежие фичи) создали NULL-seq сессии на 10865 и 3614
точек; поточечный репейр давился на них.
## Фикс (frontend/js/db.js, репейр seq в runMigrations)
Поточечный цикл заменён на **батчевую пере-нумерацию** (CASE чанками по 300, паттерн
`saveGapFill`/`saveSmoothedTrack`): один UPDATE на ~300 точек вместо одного на точку. Порядок -
по ts (как и было). ~15000 invoke'ов -> ~50.
## Проверка на устройстве (build 477, debug)
- BOOT 1 (seq ещё NULL): репейр всех 3 сессий (15166 точек) за **~1.1с** (было ~15с+);
`[BOOT] migrations` = 1356ms.
- BOOT 2 (без действий между): "checking seq repair" есть, "Repairing seq" НЕТ - `nullSeqSessions`
пусто, репейр не запускался. `[BOOT] migrations` = **357ms**. seq сохранился, репейр стал
одноразовым.
- Дамп БД после чистого force-stop: 0 NULL seq во всей БД; сессия 1789547934760 - seq 1..3614,
инверсий по ts = 0 (порядок корректный, регрессии в отрисовке трека нет).
## Осталось / отдельно
- `import_seed_products` (lib.rs) на up-to-date-старте всё равно копирует сид 8.5МБ + гоняет
дедуп = ~259ms каждый старт. Это НЕ было причиной "долго" (репейр был), поэтому seed-опт не
делал: область products/dedup имела баги (FK), трогать без нужды рискованно. Вынес пользователю
на решение.
- Тайминг-обвязку (JS [BOOT], Rust [SEED-TIMING]) убрал; lib.rs/main.js вернулись к HEAD.

View file

@ -2024,13 +2024,26 @@ async function runMigrations() {
);
if (nullSeqSessions.length > 0) {
console.log(`[db.js] Repairing seq for ${nullSeqSessions.length} sessions...`);
for (const row of nullSeqSessions) {
const pts = await select(
"SELECT rowid, ts FROM gps_points WHERE session_id = ?1 ORDER BY ts ASC, rowid ASC",
for (let row of nullSeqSessions) {
// Batched CASE update (like saveGapFill/saveSmoothedTrack): one UPDATE per ~300 points
// instead of one per point. The per-point loop was ~15000 awaited IPC round-trips on
// merged/split tracks (seq=NULL by design) — many seconds of startup, EVERY boot, and
// for the big sessions it did not even persist before the next launch re-ran it.
let pts = await select(
"SELECT id FROM gps_points WHERE session_id = ?1 ORDER BY ts ASC, id ASC",
[row.session_id]
);
for (let i = 0; i < pts.length; i++) {
await execute("UPDATE gps_points SET seq = ?1 WHERE rowid = ?2", [i + 1, pts[i].rowid]);
let SEQCHUNK = 300; // 2 params/row (id, seq) → under SQLite's 999 bind-var limit
for (let i = 0; i < pts.length; i += SEQCHUNK) {
let chunk = pts.slice(i, i + SEQCHUNK);
let when = [], ids = [], params = [];
for (let k = 0; k < chunk.length; k++) {
params.push(chunk[k].id); let idi = params.length;
params.push(i + k + 1); let sqi = params.length;
when.push(`WHEN ?${idi} THEN ?${sqi}`);
ids.push(`?${idi}`);
}
await execute(`UPDATE gps_points SET seq=CASE id ${when.join(" ")} END WHERE id IN (${ids.join(",")})`, params);
}
console.log(`[db.js] Repaired seq for session ${row.session_id}: ${pts.length} points`);
}