mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 12:36:15 -08:00
test(performance): count startup phases and SQL statements for the J1 launch journey
Implements plan item A2. With IPTVNATOR_PERF_CAPTURE=1 the main process
keeps named counters and registers a main-only performance:read-counters
IPC handler; without the flag nothing is counted and the handler does not
exist.
- debug-trace.ts owns the registry; traceStartupPhase replaces the
trace('startup', ...) sites and counts main.startupPhases.
- The database worker counts executed statements through better-sqlite3's
Statement prototype (the verbose callback expands every statement and
made bulk inserts 2-4x slower) and posts the count over its message
port, flushed before every other worker message. The main-thread shared
connection is counted through a new connection observer in the shared
database library.
- The first main window freezes main.modulesRegisteredBeforeWindow at
creation and main.sqlStatementsBeforeReadyToShow at ready-to-show.
- The journey gate drops the ready-to-show that Electron emits for the
about:blank detour, so the app sees the real document's first paint,
and taps the counters handler; the J1 record reads both counters after
the renderer probe completes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d02f90dfe
commit
37ac72b951
38 files changed
+1817
-73
No files matched your search
@@ -63,7 +63,8 @@ longer in the DOM, and a source card has a non-empty client rect. Counters are
|
||||
frozen at that microtask checkpoint, so bridge calls and mutations issued
|
||||
later in the same task are included and everything after it is not.
|
||||
|
||||
Three test-side pieces are injected; production code is not changed:
|
||||
Three test-side pieces are injected. The app itself only contributes the
|
||||
main-process counters below, which exist only with `IPTVNATOR_PERF_CAPTURE=1`:
|
||||
|
||||
- `journey-renderer-gate.cjs` is loaded into the main process with `-r`, the
|
||||
mechanism Playwright uses for its own loader. Playwright resolves
|
||||
@@ -72,7 +73,15 @@ Three test-side pieces are injected; production code is not changed:
|
||||
registered afterwards would race the first document. The gate makes the
|
||||
first `loadFile` navigate to `about:blank` and holds the real load until
|
||||
the test releases it. A 15 s safety timeout releases it on its own and the
|
||||
iteration is then invalid.
|
||||
iteration is then invalid. Electron emits `ready-to-show` for the first
|
||||
paint of a hidden window, and `about:blank` paints too, so the gate drops
|
||||
that event while the window shows `about:blank`; otherwise the app would
|
||||
show a blank window and freeze its `ready-to-show` counter before its own
|
||||
document exists. Electron emits the event again for the real document's
|
||||
first paint because the window is still hidden, which is the moment
|
||||
production sees. The gate also keeps the listener the app registers with
|
||||
`ipcMain.handle('performance:read-counters')`, so the test can call it from
|
||||
the main process.
|
||||
- `journey-renderer-probe.ts` is registered with `addInitScript` on that
|
||||
`about:blank` page, so it runs at the start of the real document. It
|
||||
records that it ran while the document was still `loading` with zero
|
||||
@@ -92,12 +101,54 @@ Three test-side pieces are injected; production code is not changed:
|
||||
| `renderer.layoutShiftScore` | Sum of `layout-shift` entries with `hadRecentInput === false`, rounded to three decimals (a shift of 0.0001 flips in and out of the cutoff between runs; the CLS "good" threshold is 0.1, so three decimals keep the counter exact without hiding anything a user could see). The cutoff is sampled in a timer queued from the first `requestAnimationFrame` after the terminal batch, that is after the frame that paints the card has been committed; entries delivered live after the terminal batch are buffered and filtered by the same cutoff. |
|
||||
| `renderer.longTasks` | `longtask` entries over 50 ms up to that same cutoff, which includes the task that rendered the card. The count depends on machine speed, so it is evidence until a run shows it is stable on the CI runner. |
|
||||
|
||||
#### Main-process counters
|
||||
|
||||
With `IPTVNATOR_PERF_CAPTURE=1`, which the journey sets,
|
||||
`apps/electron-backend/src/app/services/debug-trace.ts` keeps named counters
|
||||
in the main process (`services/performance-counters.ts`) and `main.ts`
|
||||
registers the `performance:read-counters` IPC handler. Without the flag
|
||||
nothing is counted, no listener is attached and the handler does not exist;
|
||||
the preload never exposes the channel. After the renderer probe completes,
|
||||
`journey-main-counters.ts` calls the handler through `electronApp.evaluate`
|
||||
and the gate's tap.
|
||||
|
||||
| Counter | Source |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `main.modulesRegisteredBeforeWindow` | `main.startupPhases`, one per `traceStartupPhase` call (the phases printed as `[startup]` trace lines), frozen right after the first main window is constructed. |
|
||||
| `main.sqlStatementsBeforeReadyToShow` | `main.sqlStatements`, frozen at the first main window's `ready-to-show`. It counts the statements of the main-thread connection (`sql-main`, schema creation and migrations) and of the database worker, which posts its count over its message port ([DB worker](sqlite-db-worker.md)). |
|
||||
|
||||
Main-thread statements are counted synchronously. The worker flushes its
|
||||
count before every other message it posts, so every worker statement whose
|
||||
response the main process has handled is included. The worker count is
|
||||
ordered against the worker's responses, not against wall-clock: statements
|
||||
whose count is still in flight when `ready-to-show` is dispatched are not.
|
||||
One call of `run`, `get`, `all`, `iterate` or `exec` that returns normally is
|
||||
one statement; on the launch workloads this matches the number of SQL trace
|
||||
lines exactly.
|
||||
|
||||
`main.sqlStatementsBeforeReadyToShow` is not yet deterministic. The main
|
||||
thread runs the shared connection's schema creation and migrations (about 90
|
||||
statements on the J1 profile) in one synchronous block after the load event,
|
||||
and `ready-to-show` is dispatched after it. The stale-download and
|
||||
stale-recording recovery that follows (one statement each) races the event,
|
||||
so iterations differ by two and the summary marks the counter
|
||||
`stable: false`. The database worker runs no statement before the first
|
||||
paint.
|
||||
Each frozen counter carries its epoch. The record refuses an iteration whose
|
||||
window counter was frozen after the gate saw the first load, or whose
|
||||
`ready-to-show` counter was frozen before the gate released the real
|
||||
document. Running totals at read time are kept under
|
||||
`evidence.mainCountersAtRead`, the freeze epochs under
|
||||
`evidence.epochs.mainWindowCreated` and `evidence.epochs.mainReadyToShow`, and
|
||||
the number of dropped blank `ready-to-show` events under
|
||||
`evidence.rendererGateReadyToShowHeldOnBlank`.
|
||||
|
||||
Counters are exact: the summary carries the value shared by every measured
|
||||
iteration. When iterations disagree, the summary reports the maximum and marks
|
||||
the counter `stable: false` under `counterStability`; such a counter is not
|
||||
promoted to a guardrail until it is deterministic.
|
||||
|
||||
Two counters from the plan are listed under `unavailable` with the reason
|
||||
One counter from the plan is listed under `unavailable` with the reason
|
||||
instead of being faked:
|
||||
|
||||
- `renderer.cdTicksToFirstCard`: the `electron-performance` build optimizes
|
||||
@@ -105,10 +156,6 @@ instead of being faked:
|
||||
`window.ng` and `ɵsetProfiler` is unavailable. The probe checks this at the
|
||||
terminal moment and the record refuses a build where the hook exists but was
|
||||
not counted.
|
||||
- `main.sqlStatementsBeforeReadyToShow`: SQL statements are only visible as
|
||||
worker-thread trace lines on stdout, which Node forwards asynchronously, so
|
||||
they cannot be ordered against `ready-to-show`. Plan item A2 adds a channel
|
||||
that can be counted.
|
||||
|
||||
### Wall-clock
|
||||
|
||||
|
||||
@@ -444,6 +444,24 @@ first, this receipt remains distinct from the later authoritative
|
||||
exposing the pending request. Disabled profiling performs no receipt clock or
|
||||
transport work, and `DatabaseWorkerClient.cancel()` remains fire-and-return.
|
||||
|
||||
With `IPTVNATOR_PERF_CAPTURE=1`, the worker connection also counts the SQL
|
||||
statements it executes and posts `performance-sql-statements` messages that
|
||||
carry only a positive count, never SQL text or bound values. Counts are
|
||||
coalesced per microtask and flushed before every other worker message, so a
|
||||
response never overtakes the statements that produced it.
|
||||
`DatabaseWorkerClient` adds them to the main-process `main.sqlStatements`
|
||||
counter and settles nothing. Statements are counted by wrapping the
|
||||
execution methods of better-sqlite3's `Statement` prototype and the
|
||||
connection's `exec`, not through the `verbose` callback: a callback makes
|
||||
better-sqlite3 expand every statement's SQL, which made bulk inserts two to
|
||||
four times slower. A call that throws is not counted, which matches the SQL
|
||||
trace for statements that fail before execution. The main process counts its
|
||||
own shared connection the same way (`services/main-sql-statement-count.ts`,
|
||||
through the shared library's connection observer). Without the flag both
|
||||
connections are opened unchanged. See
|
||||
`workers/database-worker-sql-statement-count.ts` and
|
||||
[performance journeys](performance-journeys.md).
|
||||
|
||||
## Renderer Contract
|
||||
|
||||
The preload bridge keeps the existing database methods but adds scoped worker
|
||||
|
||||
@@ -30,7 +30,7 @@ IPTVNATOR_TRACE_STARTUP=1 pnpm nx serve electron-backend
|
||||
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events
|
||||
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
|
||||
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
|
||||
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset
|
||||
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset. It also keeps count-only main-process counters (startup phases, database worker SQL statements, and their values at main-window creation and `ready-to-show`) and registers the main-only `performance:read-counters` IPC handler, which the preload does not expose; see [performance journeys](../architecture/performance-journeys.md)
|
||||
- `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`
|
||||
|
||||
|
||||
Reference in new issue
Block a user