mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
test(performance): add J4 search journey
Measures typing a six-character query into the header search box on /workspace/search until the global search results settle, on a profile with the M3U fixture and the mock's existing 12,000-item `large` Xtream catalog. Counters: bridge calls and SQL statements per search (with a per-keystroke breakdown), serial IPC depth, DOM mutations, change-detection ticks, layout shift and long tasks; wall-clock last keystroke to settled and first keystroke to first result. Runs in the existing journeys target and the warn-only CI job, whose summary now prints the per-keystroke table. Moves J3's picsum artwork blocker into a shared helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e8b181fcea
commit
4ecc2d1096
13 files changed
+2087
-67
No files matched your search
@@ -19,9 +19,9 @@ live in `tools/performance/`.
|
||||
| J4 `search` | six-character query typed into global search | results list settled |
|
||||
|
||||
J1 is instrumented: `renderer.initialBytes` from the built output, and the
|
||||
runtime counters of the launch benchmark below. J2 and J3 are instrumented by
|
||||
their own specs (below). J4 follows the plan in `.plans/` and is added in its
|
||||
own thread; each thread names its journey and counter in the PR description.
|
||||
runtime counters of the launch benchmark below. J2, J3 and J4 are
|
||||
instrumented by their own specs (below). Each thread names its journey and
|
||||
counter in the PR description.
|
||||
|
||||
## Running the journeys
|
||||
|
||||
@@ -291,10 +291,12 @@ 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. SQL statements are counted only with
|
||||
`IPTVNATOR_PERF_COUNT_SQL=1` as well, because the hook wraps every statement
|
||||
execution: the launch journey sets both (the flags are built in
|
||||
`journey-launch-environment.ts`), while J2's launches and the M3U, refresh
|
||||
and Xtream benchmarks do not set the SQL flag and keep measuring unwrapped
|
||||
statements. A harness test fails if any other source sets the SQL flag. After the renderer probe completes,
|
||||
execution: the launch journey and J4, which reports
|
||||
`renderer.sqlStatementsPerSearch`, set both (the flags are built in
|
||||
`journey-launch-environment.ts`), while J2's and J3's launches and the M3U,
|
||||
refresh and Xtream benchmarks do not set the SQL flag and keep measuring
|
||||
unwrapped statements. A harness test fails if any other source sets the SQL
|
||||
flag. After the renderer probe completes,
|
||||
`journey-main-counters.ts` calls the handler through `electronApp.evaluate`
|
||||
and the gate's tap.
|
||||
|
||||
@@ -554,6 +556,13 @@ iterations carry `evidence.media` (the video element at `playing`) and
|
||||
a `media` field (`null` for J1 and J2), which the probe's
|
||||
`schemaVersion` 1 readers ignore.
|
||||
|
||||
J4 adds the `journeys.search` entry, again with the same shape. Its
|
||||
iterations carry `evidence.perKeystroke` (one entry per typed key, see
|
||||
[J4](#j4-search-type-a-query-until-the-results-settle)), which the CI job
|
||||
summary prints as a table for the first measured iteration. J4 has its own
|
||||
renderer probe (`search-journey-probe.ts`, `schemaVersion` 1); the shared
|
||||
probe is unchanged.
|
||||
|
||||
## J2 `open-source`: open a source to a browsable list
|
||||
|
||||
`open-source.journey.ts` reuses the J1 profile and process pattern: the
|
||||
@@ -782,6 +791,151 @@ machine, so check the runner's `counterStability` before trusting the
|
||||
mutation and request counts. No J3 baseline exists yet; J3 counters join the
|
||||
ratchet once three runner runs agree.
|
||||
|
||||
## J4 `search`: type a query until the results settle
|
||||
|
||||
`search.journey.ts` follows J3: the profile is seeded once through the "Add
|
||||
playlist" dialogs (`seedLaunchJourneyProfile` with `SEARCH_JOURNEY_SEED`),
|
||||
every iteration copies it, spawns a fresh process through `runLaunchJourney`
|
||||
and hands the running app to `measureSearchJourney` in
|
||||
`src/journeys/search-journey-app.ts`. One warm-up and five measured
|
||||
iterations. Unlike J2 and J3 the launch runs with the main-process counters
|
||||
(`mainCounters: true`, so `IPTVNATOR_PERF_CAPTURE` and
|
||||
`IPTVNATOR_PERF_COUNT_SQL`), because SQL statements are a J4 counter; every
|
||||
statement then runs through the counting hook.
|
||||
|
||||
**Profile.** J1's M3U source plus an Xtream portal ("Journey search portal")
|
||||
on the mock's existing `large:large` scenario: 60 categories of 200 items,
|
||||
so 4,000 live channels, 4,000 movies and 4,000 series. No fixture was added
|
||||
for the journey. The query is `system`, which matches 170 series titles of
|
||||
that deterministic catalog: more than global search's first page of 100, so
|
||||
the page is full and more results are available. Six characters take the
|
||||
title FTS path of `globalSearch`
|
||||
(`apps/electron-backend/src/app/database/operations/content.operations.ts`);
|
||||
the M3U arm runs as well, because live content is included, but none of the
|
||||
four fixture channels matches. Poster artwork points at `picsum.photos` and
|
||||
is cancelled in the main process as in J3
|
||||
(`src/performance/journey-external-artwork.ts`, shared by both journeys);
|
||||
the count is kept as `evidence.externalArtworkCancelled`. The mock is not
|
||||
put behind the request ledger: global search reads only the local database.
|
||||
|
||||
**What typing does.** The header search box
|
||||
(`app-workspace-shell-header .search-field input[type="search"]`) applies
|
||||
its term after `SEARCH_INPUT_DEBOUNCE_MS` (350 ms,
|
||||
`WorkspaceShellSearchSyncService`) and writes it to the URL as `q`. On
|
||||
`/workspace/search` (`app.routes.ts`, Electron only) `SearchResultsComponent`
|
||||
adopts `q` and runs `executeSearch` after its own 300 ms debounce, which
|
||||
calls the `dbGlobalSearch` bridge method once with a limit of 101.
|
||||
|
||||
**Start.** After J1 has ended, the test clicks the rail's **Global search**
|
||||
link and focuses the header search box (not measured), installs the IPC
|
||||
capture with a start sentinel, arms the probe
|
||||
(`src/performance/search-journey-probe.ts`) and waits until the app has
|
||||
been quiet for 1 s: no DOM mutation, no new or pending bridge call, and an
|
||||
unchanged `main.sqlStatements` total (30 s timeout, which fails the
|
||||
iteration). J1's capture is detached. The test then types the query with one
|
||||
`keyboard.type` call per character on a fixed schedule, 100 ms apart from
|
||||
the first key (well below the 350 ms debounce, as steady typing would be),
|
||||
and samples the main process just before each key. The probe's
|
||||
capture-phase `keydown` listener on `window` stamps every key in the search
|
||||
box before the app sees it; the first one starts the journey and sends
|
||||
`cancelSourceProbe('__iptvnator-journey-search-start__')`. The record
|
||||
rejects an iteration whose DOM mutations, bridge calls or SQL statements
|
||||
moved between the quiet snapshot and the first key.
|
||||
|
||||
**End.** "No DOM mutation for 200 ms after the last keystroke" alone would
|
||||
end inside the 650 ms of debounce, before any query ran: nothing in the DOM
|
||||
changes while the term waits. So after the sixth key the probe waits for
|
||||
the results of the final term to be shown (the path ends with
|
||||
`/workspace/search`, the URL `q` is the query, no `.loading-state` is
|
||||
rendered and an `app-content-card` in the results container is visible). The
|
||||
mutation batch that first meets that condition opens a 200 ms quiet window,
|
||||
and every later batch restarts it. When the window elapses the journey has
|
||||
settled: the settled moment is the last mutation batch, the counters stop at
|
||||
the confirmation, and the probe sends
|
||||
`cancelSourceProbe('__iptvnator-journey-search-end__')`. Without a settle
|
||||
within 15 s of the last key the probe marks the iteration invalid
|
||||
(`settle-timeout`).
|
||||
|
||||
A settle that times out fails the iteration with what the probe and the
|
||||
main process saw: the URL `q`, the input's value, the results view, the
|
||||
bridge calls, renderer console errors, the SQL totals before each key, and
|
||||
the traced `dbGlobalSearch` calls with their summarized arguments and
|
||||
results.
|
||||
|
||||
### Counters
|
||||
|
||||
| Counter | Source |
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `renderer.ipcCallsPerSearch` | Bridge `start` trace events between the start and end sentinels, as `renderer.ipcCallsToFirstPage` in J2. `evidence.ipcCallsByMethod` names them. |
|
||||
| `renderer.sqlStatementsPerSearch` | `main.sqlStatements` (main thread and database worker, see [Main-process counters](#main-process-counters)) read right before the first key and again after the probe settled. The worker reports its count before the response it belongs to, so the statements of a query are counted before its results reach the renderer. Statements in the 500 ms after that read are kept as `evidence.sqlStatementsAfterSettled`, so background work that would have leaked into the count is visible. |
|
||||
| `renderer.ipcSerialDepthToResults` | `computeJourneyIpcSerialDepth` over the capture's timeline between the sentinels (see [Serial IPC depth](#serial-ipc-depth)); `evidence.ipcSerialDepth.chain` and `evidence.ipcTimeline` show the calls. |
|
||||
| `renderer.domMutationsToResults` | `MutationRecord`s from the first keydown until the quiet window was confirmed (by definition none arrive inside it). |
|
||||
| `renderer.cdTicksToResults` | `ApplicationRef` ticks from the first keydown (read in the capture-phase listener, before the app handles the key) until the confirmation (see [Change-detection ticks](#change-detection-ticks)). |
|
||||
| `renderer.layoutShiftScore` | All `layout-shift` entries from the first keydown until the confirmation, including `hadRecentInput` ones (typing is input, as in J2 and J3), rounded to three decimals; the split is under `evidence.layoutShift`. |
|
||||
| `renderer.longTasks` | `longtask` entries over 50 ms whose time range overlaps the window from the first keydown to the confirmation. Evidence until shown to be stable on the runner. |
|
||||
|
||||
`evidence.perKeystroke` breaks the journey down by key: for each typed
|
||||
character, what happened from that key until the next one (the last entry:
|
||||
until the settle). `domMutations` and `cdTicks` are split at the renderer's
|
||||
keydown stamps. `ipcCalls`, `queryCalls` (`dbGlobalSearch` calls) and
|
||||
`sqlStatements` are differences of the main-process samples taken just
|
||||
before each key, so their boundaries sit a few milliseconds before the
|
||||
renderer's. A search with working debounce shows zeros for the first five
|
||||
keys and one query after the last; a search that queried on every key from
|
||||
the second character on would show a `dbGlobalSearch` call and its
|
||||
statements in each of those entries, even when a later key superseded the
|
||||
result.
|
||||
|
||||
No counter is listed under `unavailable`. HTTP requests are not a J4
|
||||
counter: global search does not touch the network.
|
||||
|
||||
### Wall-clock
|
||||
|
||||
| Entry | Derivation |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `firstKeystrokeToFirstResultMs.p50/.p90` | First mutation batch with a visible result card (for any term) minus the first keydown. |
|
||||
| `lastKeystrokeToSettledMs.p50/.p90` | Settled moment (the last mutation batch before the 200 ms quiet window) minus the last keydown. The quiet window itself is not included. |
|
||||
|
||||
Both are taken in the renderer. With the current debounces both include
|
||||
the 650 ms the term waits (350 ms in the shell, then 300 ms in the results
|
||||
component); the first also includes the 500 ms of typing.
|
||||
|
||||
### First measurement
|
||||
|
||||
Local, macOS, 2026-10-04, three full `perf:journeys` runs plus repeated J4
|
||||
runs. In the runs where J4 completed (the first and third full runs), every
|
||||
counter was identical in all ten measured iterations except one:
|
||||
`renderer.ipcCallsPerSearch` 1 (`dbGlobalSearch`),
|
||||
`renderer.sqlStatementsPerSearch` 2, `renderer.ipcSerialDepthToResults` 1,
|
||||
`renderer.domMutationsToResults` 402 (100 cards on the first page),
|
||||
`renderer.layoutShiftScore` 0 and `renderer.longTasks` 0.
|
||||
`renderer.cdTicksToResults` read 14 in all five iterations of the third run
|
||||
and 13, 18, 13, 15, 13 in the first, so check the runner's
|
||||
`counterStability` before trusting it. P50/P90
|
||||
`lastKeystrokeToSettledMs` 686/692 ms and `firstKeystrokeToFirstResultMs`
|
||||
1,178/1,184 ms in the third run (699/720 and 1,193/1,210 in the first).
|
||||
|
||||
Per keystroke, the first five keys caused one change-detection tick each
|
||||
and nothing else: no bridge call, no SQL statement and no DOM mutation.
|
||||
All of the work followed the sixth key. Search therefore does not run a
|
||||
query per keystroke. The debounce does dominate the wall clock: about
|
||||
650 ms of the 686 ms from the last key to settled is the two stacked
|
||||
debounces (350 ms in the shell, then 300 ms in the results component). The
|
||||
query itself, from the bridge call to the rendered first page, takes the
|
||||
remaining 30-40 ms.
|
||||
|
||||
About one launch in sixty showed the empty-results view: the single
|
||||
`dbGlobalSearch` call went out with the same arguments (`system`, all three
|
||||
types, hidden categories included, limit 101) and the main process answered
|
||||
with an empty array in about 20 ms, with no renderer error. The database
|
||||
was unchanged from passing launches (the same 119 statements before the
|
||||
first key, the usual 2 for the query, none afterwards), and every failure
|
||||
traced was the first launch after seeding. The journey fails such an
|
||||
iteration instead of measuring it, so a full run occasionally fails J4.
|
||||
The cause is in the app, not the harness, and is not fixed here. No J4
|
||||
baseline exists yet; J4 counters join the ratchet once three runner runs
|
||||
agree.
|
||||
|
||||
## `renderer.initialBytes`
|
||||
|
||||
The bytes a browser fetches before Angular can bootstrap, read from the built
|
||||
@@ -1079,8 +1233,10 @@ reports slow imports of non-Latin playlists.
|
||||
2. Give the journey its own probe options (`cardSelector`,
|
||||
`companionSelectors`, `routeFragment`, `startClick` for a click start,
|
||||
`media` for a media-event end such as J3's `playing`) or extend
|
||||
`journey-renderer-probe.ts` when the end condition is neither. Use a state
|
||||
key and sentinel ids of its own. Keep the probe self-contained: Playwright
|
||||
`journey-renderer-probe.ts` when the end condition is neither. A journey
|
||||
whose start or end does not fit that probe gets a probe of its own, as
|
||||
J4's typed start and quiet-window end do (`search-journey-probe.ts`). Use
|
||||
a state key and sentinel ids of its own. Keep the probe self-contained: Playwright
|
||||
serializes it with `toString()`. A click-started journey settles with
|
||||
`waitForJourneyClickQuiet` from `journey-click-settle.ts`.
|
||||
3. Map the measurement to a `JourneyIterationRecord` in a
|
||||
|
||||
Reference in new issue
Block a user