mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 13:46:16 -08:00
test(perf): count change-detection ticks in the electron-performance build
J1 renderer.cdTicksToFirstCard, J2 renderer.cdTicksToFirstPage and the J1 idle baseline renderer.cdTicksIdle30s. Angular's ɵsetProfiler is only reachable through the dev-mode window.ng global, so the electron-performance configuration alone swaps environment.ts for environment.performance.ts, which re-exports the production AppConfig and wraps ApplicationRef._tick. Production and PWA sources and output are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
525ca7bc44
commit
454c1fe10a
16 files changed
+711
-65
No files matched your search
@@ -253,6 +253,10 @@ this in the same thread as the first worst offender.
|
||||
network, layout and DOM-mutation counts carry over; the Angular
|
||||
change-detection and template-update counts do not (production skips
|
||||
`checkNoChanges`), and per-firing milliseconds are upper bounds.
|
||||
The tick count in the optimized build is now measured by J1's idle window:
|
||||
`renderer.cdTicksIdle30s` read 3 ticks per 30 s on the journey's dashboard
|
||||
(one M3U source, one Xtream portal), the baseline for plan item C6. See
|
||||
[performance journeys](performance-journeys.md#idle-window).
|
||||
- Windows and Linux. Throttling and occlusion behavior differ per platform.
|
||||
- Idle during playback, and on routes other than the dashboard. The
|
||||
conditional table above is from code reading only.
|
||||
|
||||
@@ -112,6 +112,8 @@ main-process counters below, which exist only with `IPTVNATOR_PERF_CAPTURE=1`:
|
||||
| `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.layoutShiftScoreSettled` | The same filter from navigation start until the settle point after the first card (see [Settle window](#settle-window)), rounded to three decimals. It catches shifts that land after the cutoff, such as skeletons that collapse once their data resolves. |
|
||||
| `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. |
|
||||
| `renderer.cdTicksToFirstCard` | `ApplicationRef` ticks from document start until the terminal batch, read from the `electron-performance` build's tick counter (see [Change-detection ticks](#change-detection-ticks)). The tick that rendered the card runs before the observer's microtask, so it is included. |
|
||||
| `renderer.cdTicksIdle30s` | Ticks during the 30 s [idle window](#idle-window) that opens at the settle point, with nothing touching the page. The baseline for plan item C6 (zoneless change detection). |
|
||||
|
||||
#### Settle window
|
||||
|
||||
@@ -185,6 +187,85 @@ On the Linux CI runner (`Performance journeys` job of #1756, run
|
||||
after the first card), and the one hit shows the same two 316 px moves of
|
||||
the recent-sources rail.
|
||||
|
||||
#### Idle window
|
||||
|
||||
After the settle point J1 leaves the dashboard alone for
|
||||
`JOURNEY_IDLE_WINDOW_MS` (30 s) and counts what it does anyway:
|
||||
`renderer.cdTicksIdle30s` is the number of change-detection ticks in that
|
||||
window, and `evidence.idle.domMutations` the mutation records in the whole
|
||||
document. The [idle work audit](idle-work-audit-2026-09.md) found Eager
|
||||
components re-rendering on every such tick in a dev build; this counter
|
||||
measures the ticks in the optimized build, so plan item C6 can show what
|
||||
zoneless change detection removes.
|
||||
|
||||
The window opens when the settle window closes, so startup data still
|
||||
landing is not idle work, and it is timed by a renderer `setTimeout`. The
|
||||
record refuses an iteration whose window opened before the settle point or
|
||||
lasted less than 30 s, or more than 1 s longer
|
||||
(`launch-journey-record-idle-window-late`): a timer that late means the
|
||||
page was busy, not idle. `evidence.idle` keeps the measured `durationMs` and
|
||||
`settledToIdleStartMs`. The main-process counters and the IPC capture are
|
||||
read after the window, which does not move them: they are frozen earlier.
|
||||
J2's launches skip the window (`runLaunchJourney` with `idleWindowMs: null`),
|
||||
so its click does not wait 30 s; a record without a finished window is
|
||||
refused as a J1 measurement.
|
||||
|
||||
#### Change-detection ticks
|
||||
|
||||
`window.ng` and Angular's profiler hook (`ɵsetProfiler`) exist only in dev
|
||||
mode, and the `electron-performance` build is optimized like production. So
|
||||
that build alone installs its own counter: its `fileReplacements` entry
|
||||
swaps `apps/web/src/environments/environment.ts` for
|
||||
`environment.performance.ts`, which re-exports the production `AppConfig`
|
||||
and calls `installChangeDetectionTickCounter()` from
|
||||
`change-detection-tick-counter.ts` while `main.js` is evaluated, before
|
||||
Angular bootstraps. The counter wraps the internal `ApplicationRef._tick`,
|
||||
the method every tick runs through: the zone scheduler's `onMicrotaskEmpty`,
|
||||
the zoneless scheduler, `afterNextRender` idle buckets and the public
|
||||
`ApplicationRef.tick()` all call it, and it is where Angular emits the
|
||||
profiler's `ChangeDetectionStart`. The count therefore equals the profiler's
|
||||
tick count and stays comparable across the zoneless migration. The running
|
||||
total is `window.__iptvnatorCdTicks.count`; the probe subtracts it at the
|
||||
journey's boundaries (zero at document start for J1, the value in the
|
||||
capture-phase click listener for J2). If a future Angular renames `_tick`,
|
||||
the performance build throws at startup instead of reporting zero.
|
||||
|
||||
This is a fileReplacements swap rather than an environment flag checked in
|
||||
`app.config.ts` on purpose: a flag, even one the optimizer folds, would put
|
||||
an import and a branch into the production sources, while the swap leaves
|
||||
every file the production and PWA builds compile unchanged. Their output is
|
||||
byte-identical with and without the counter (every emitted file hashes the
|
||||
same apart from the `ngsw.json` build timestamp), so
|
||||
`renderer.initialBytes` cannot move. A build-config test fails if another
|
||||
configuration references `environment.performance.ts`. The other benchmarks
|
||||
built from `electron-performance` (M3U import, Xtream, cancellation) carry
|
||||
the counter too; it adds one increment per tick.
|
||||
|
||||
A build without the counter reports
|
||||
`capabilities.changeDetectionTicks: "unavailable-counter-missing"` and the
|
||||
record refuses the iteration, so a zero is never a missing hook.
|
||||
|
||||
First local measurement (macOS, 2026-09-30, three `perf:journeys` runs,
|
||||
18 iterations per journey including warm-ups):
|
||||
|
||||
| Counter | Run 1 | Run 2 | Run 3 |
|
||||
| ----------------------------- | ----- | ----- | ----- |
|
||||
| `renderer.cdTicksToFirstCard` | 20 | 20 | 21 |
|
||||
| `renderer.cdTicksIdle30s` | 3 | 3 | 3 |
|
||||
| `renderer.cdTicksToFirstPage` | 22 | 22 | 22 |
|
||||
|
||||
Every run marked all three `stable: true`. `renderer.cdTicksIdle30s` and
|
||||
`renderer.cdTicksToFirstPage` were identical in all 18 iterations, and every
|
||||
idle window saw 90 mutation records. `renderer.cdTicksToFirstCard` read 20
|
||||
in 13 iterations and 21 in the five measured iterations of the third run
|
||||
(its warm-up read 20), with every other J1 counter unchanged
|
||||
(`renderer.ipcCallsToFirstCard` 14, `renderer.domMutationsToFirstCard` 554).
|
||||
With zone.js a tick follows every macrotask that ran in the Angular zone, so
|
||||
two startup callbacks that land in one task on one launch and in two tasks
|
||||
on another differ by one tick without any different work. Treat a one-tick
|
||||
difference in J1 as that race, and confirm on the CI runner that the counter
|
||||
is deterministic before it becomes a baseline.
|
||||
|
||||
#### Main-process counters
|
||||
|
||||
With `IPTVNATOR_PERF_CAPTURE=1`, which the journey sets,
|
||||
@@ -239,14 +320,8 @@ 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.
|
||||
|
||||
One counter from the plan is listed under `unavailable` with the reason
|
||||
instead of being faked:
|
||||
|
||||
- `renderer.cdTicksToFirstCard`: the `electron-performance` build optimizes
|
||||
scripts, which sets `ngDevMode` to false, so Angular does not publish
|
||||
`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.
|
||||
No J1 counter from the plan is listed under `unavailable` any more; the
|
||||
list stays in the record so a future gap is reported instead of faked.
|
||||
|
||||
### Wall-clock
|
||||
|
||||
@@ -306,6 +381,8 @@ serial-depth counter is a better guardrail candidate than a raw call count.
|
||||
"journeys": {
|
||||
"launch": {
|
||||
"counters": {
|
||||
"renderer.cdTicksIdle30s": 3,
|
||||
"renderer.cdTicksToFirstCard": 20,
|
||||
"renderer.ipcCallsToFirstCard": 12,
|
||||
"renderer.layoutShiftScoreSettled": 0.236
|
||||
},
|
||||
@@ -323,7 +400,7 @@ serial-depth counter is a better guardrail candidate than a raw call count.
|
||||
"spawnToFirstCardMs.p50": 1234.5,
|
||||
"spawnToFirstCardMs.p90": 1300.1
|
||||
},
|
||||
"unavailable": { "renderer.cdTicksToFirstCard": "reason" },
|
||||
"unavailable": {},
|
||||
"iterations": [
|
||||
{
|
||||
"index": 0,
|
||||
@@ -446,12 +523,12 @@ strings and stream paths carry credentials and are never stored.
|
||||
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `renderer.ipcCallsToFirstPage` | Bridge `start` trace events between the start and end sentinels, counted by a second `journey-main-ipc-capture.ts` instance installed with `startSentinelId`. Calls before the start marker are tallied separately (`callsBeforeStart`); a start marker that is missing, repeated or received after the end sentinel fails the iteration. |
|
||||
| `renderer.domMutationsToFirstPage` | `MutationRecord`s from the click until the terminal batch. Records produced before the click (hover, settling) are taken from the observer at the start and counted under `evidence.settle` instead. |
|
||||
| `renderer.cdTicksToFirstPage` | `ApplicationRef` ticks from the click until the terminal batch: the counter's running total read in the capture-phase click listener, before the app handles the click, subtracted from its value at the terminal batch (see [Change-detection ticks](#change-detection-ticks)). |
|
||||
| `renderer.layoutShiftScore` | Sum of all `layout-shift` entries from the click until the post-paint cutoff, rounded to three decimals. Unlike J1 it includes entries with `hadRecentInput === true`: the journey is a response to the click and runs inside the 500 ms input window, so the CLS filter would always read 0. The split is under `evidence.layoutShift`. |
|
||||
| `renderer.longTasks` | `longtask` entries over 50 ms whose time range overlaps the window from the click to the cutoff. The task that dispatches the click began before the event's timestamp and still counts; buffered J1 tasks that ended before the click are dropped. Evidence until it is shown to be stable on the CI runner, as for J1. |
|
||||
| `main.mockHttpRequestsToSettled` | Requests the proxy received from the click until, after the terminal batch, no new request had arrived for 1 s and none was in flight (a response slower than that, and what it triggers, stays inside the window). The window ends at the ledger position read by that accepted quiet sample; a request arriving after it was never seen in flight, so it goes to `evidence.httpRequestsAfterSettledByRoute` instead of the counter. The ledger is read 1 s after that sample, so that late traffic is actually observed. The window starts at the renderer's click stamp, the same boundary as every other J2 counter, not when Playwright began its actionability checks; the proxy stamps requests with the test process's wall clock, and both processes read the same host clock. Bounding by the terminal would compare the test process's clock with the renderer's, so the count up to the terminal epoch is evidence only (`evidence.httpRequestsToFirstPage`); `evidence.httpRequestsByRoute` names the requests. |
|
||||
|
||||
Two counters are listed under `unavailable`. `renderer.cdTicksToFirstPage`
|
||||
is missing for the same reason as its J1 counterpart.
|
||||
One counter is listed under `unavailable`.
|
||||
`main.sqlStatementsToFirstPage` is missing because the running
|
||||
`main.sqlStatements` total that J1 freezes at `ready-to-show` can only be
|
||||
read from the test process through the journey gate. It therefore cannot be
|
||||
|
||||
Reference in new issue
Block a user