perf(workspace): share the startup inventory read and defer non-first-card IPC (J1)

Journey J1 / renderer.ipcCallsToFirstCard: 12 -> 7.

- PlaylistsService.getAllPlaylists() shares one in-flight SQLite read
  between concurrent callers (the playlist effect and the XMLTV source
  reconciliation at startup). Settled reads are never reused and every
  SQLite write detaches the pending read.
- getM3uFavoriteChannels() stops re-reading the write-once IndexedDB ->
  SQLite migration receipt once it has been seen.
- StartupDeferralService holds the download list, app update status and
  the dashboard's recent items and favorites until one task after the
  render that reveals the routed content (5 s safety timeout).
  IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1 is the kill switch.
- reconcileEpgSources and the two distinct migration-flag reads stay on
  the critical path: the first is the #1548 revision fence, the second
  are different keys, not duplicates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-29 08:59:35 +02:00
1 parent 76dd8c099e
commit e5ee064407
24 files changed
+648 -23

No files matched your search

+29
View File
@@ -179,6 +179,35 @@ harness, which is what the ratchet needs. The main process start
(`Date.now() - process.uptime()`) is recorded per iteration under
`evidence.epochs` for cross-checks.
### Startup work before the first card
`renderer.ipcCallsToFirstCard` counts what the renderer asks of the main
process before the first card. Only work that the first card needs may run
before it:
- `PlaylistsService.getAllPlaylists()` shares one in-flight SQLite read
between concurrent callers (`SharedInFlightRead`): at startup the playlist
effect and the XMLTV source reconciliation both read the inventory, and the
second caller joins the first read and receives a copy. A settled read is
never reused, and every SQLite write detaches the pending read, so a caller
that follows a write reads again.
- `StartupDeferralService` (`@iptvnator/services`) holds work the first
screen does not need: the initial download list (the header badge), the
app update status (the update card), and the dashboard's recent items and
favorites. `app.config.ts` arms it with `STARTUP_WORK_DEFERRAL`, and
`AppStartupStatusComponent` opens it one task after the render that
reveals the routed content, so released work never lands in the task of
the first card. It opens on its own after 5 s if that render never
happens. Without the token (tests, other hosts) the gate is open.
- `IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1` (or `true`) is the short-lived kill
switch: the preload exposes it as `startupDeferralDisabled` and the gate
starts open. Remove it within two releases of the change that added it.
- `reconcileEpgSources` stays before the first card on purpose: its
completion bumps `EpgSourceSettingsService.revision()`, the fence that
keeps XMLTV lookups from returning data of a removed source. Deferring it
would let dashboard EPG lookups start first and then be discarded and
requeued.
### Summary schema
```json
+5 -1
View File
@@ -282,7 +282,11 @@ hooks: `dashboard-hero`, `dashboard-hero-slide` (`data-hero-kind`),
`updateDate` / `importDate` for sources that have never been used.
3. `DashboardDataService` is passive on construction. The dashboard feature
owns the initial reloads for recent items, favorites, and Xtream recently
added rows on page entry.
added rows on page entry. Recent items and favorites reload through
`reloadForPageEntry()`, which on launch waits for `StartupDeferralService`
(the first rendered screen, journey J1); their rails keep their scoped
skeletons meanwhile, and the Xtream recently-added and TMDB rails follow
`globalFavoritesLoaded()` as before. Later page entries find the gate open.
4. No dashboard-local `Layout` state, no localStorage keys, no migrations.
Per-rail visibility is the one persisted preference, and it lives in the
global settings store (`Settings.dashboardRails`), not in a
+1
View File
@@ -34,6 +34,7 @@ IPTVNATOR_TRACE_STARTUP=1 pnpm nx serve electron-backend
- `IPTVNATOR_PERF_COUNT_SQL=1`, together with `IPTVNATOR_PERF_CAPTURE=1`, also counts every SQL statement of the main-process and database-worker connections for `main.sqlStatementsBeforeReadyToShow`; it wraps each statement execution, including every row of a bulk insert, so only the launch journey sets it and the import benchmarks leave it unset
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unset
- `IPTVNATOR_DISABLE_COMPILE_CACHE=1` disables the main-process V8 compile cache; `IPTVNATOR_COMPILE_CACHE_DIR=<dir>` relocates it. The startup trace reports the outcome as `compile-cache`
- `IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1` starts the renderer's startup gate open, so the download list, update status and dashboard recent/favorites reads run before the first screen again (kill switch; see "Startup work before the first card" in `docs/architecture/performance-journeys.md`)
- Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching