mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
* perf(web): run the renderer zoneless and drop the zone.js polyfill Plan item C6 step 5. app.config.ts provides provideZonelessChangeDetection() directly and apps/web builds without the zone.js polyfill, so a tick runs only when a signal, markForCheck, an Angular event or a render hook asks for one, not after every macrotask. The flag from #1824 (change-detection.providers*.ts and the *-zoneless configurations) goes away; a build-config test fails if the polyfill or a zone provider comes back. NgZone.run/runOutsideAngular calls that only mattered with zone.js are removed from the dashboard data services, the settings unload guard and the embedded MPV position poll (with the spec case asserting it), and the unused polyfills.ts, polyfills-test.ts and setup-jest.ts are deleted. Unit tests keep zone.js/testing for fakeAsync and waitForAsync; apps/remote-control-web still bootstraps with zone.js. renderer.initialBytes: 1,596,201 -> 1,545,931 bytes (-50,270) on a local production build of this base; the baseline is lowered to the measured value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): cite #1929 as the initial-bytes baseline evidence Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): ratchet the change-detection ticks that zoneless made deterministic On the canonical runner (Performance ratchet run 38359088033, three runs of this branch, five iterations each) renderer.cdTicksToFirstCard read 5, renderer.cdTicksIdle30s 3 and renderer.cdTicksToFirstPage 9 in all 15 iterations, against 31, 1 to 4 and 22 with zone.js. They become baselines with zero slack and the Performance journeys job checks them. J3 and J4 tick counts still vary by one and stay unenforced; J3 wall clock is in master's range, so the local rise measured on #1824 was machine load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(web): allow J1's one-tick lazy-chunk race in the first-card tick baseline After merging #1931 (Angular Material off the startup path) the Performance journeys job read renderer.cdTicksToFirstCard 5, 5, 5, 5, 6: the lazily loaded chunk resolves on either side of a render. All 15 iterations before it read 5. The baseline stays 5 with one tick of slack and a note; a return towards zone.js levels (31) still fails, and the idle and first-page tick baselines stayed exact in the same run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): find a finished programme for the phone dialog after UTC midnight The Xtream mock lays its 30-minute EPG slots out relative to now, and the list view shows the programmes that overlap the selected day. In the first half hour after midnight (the spec pins TZ=UTC) today holds only the programme on air, so 'stacks the programme dialog actions on a phone' found no data-when=past row and failed every retry on #1929's Windows shard (00:18-00:23 UTC). The spec now waits for the list, and steps back a day when today has no finished programme yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(architecture): record the zoneless renderer in the workspace contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
328 lines
21 KiB
Markdown
328 lines
21 KiB
Markdown
# Zoneless change-detection migration
|
||
|
||
Working checklist for plan item C6 of the performance journeys plan: move the
|
||
renderer (`apps/web`) from zone.js to `provideZonelessChangeDetection()`.
|
||
The win is measured with the change-detection tick counters described in
|
||
[performance journeys](performance-journeys.md#change-detection-ticks); the
|
||
[idle work audit](idle-work-audit-2026-09.md) found the Eager roots that
|
||
re-render on every tick. Update this file in the same PR that converts an item.
|
||
|
||
The inventory was taken on `e8b181fce` (2026-10-04, Angular 22.1.6). Run
|
||
`pnpm nx run electron-backend-e2e:test-performance-harness` after editing the
|
||
Eager list: `zoneless-migration.spec.ts` fails when the list and the code
|
||
disagree, so a new Eager component cannot land unnoticed and a converted one
|
||
must be ticked here.
|
||
|
||
## Starting point
|
||
|
||
- **OnPush is already the default.** Since Angular 22 an unset
|
||
`changeDetection` means OnPush, and the old `Default` strategy is spelled
|
||
`ChangeDetectionStrategy.Eager`. Only components that set `Eager` are
|
||
checked on every tick. `ChangeDetectionStrategy.Default` is not used.
|
||
- **Renderer bootstrap.** `apps/web/src/app/app.config.ts` provides
|
||
`provideZoneChangeDetection({ eventCoalescing: true })` and
|
||
`apps/web/project.json` builds with `"polyfills": ["zone.js"]`.
|
||
`apps/remote-control-web` does the same; it is a separate app and outside
|
||
this migration unless a step says otherwise.
|
||
- **Unit tests already run zoneless.** Every `src/test-setup.ts` (apps/web,
|
||
apps/remote-control-web and 24 libs) calls `setupZonelessTestEnv` and loads
|
||
`zone.js`/`zone.js/testing` only for `fakeAsync` and `waitForAsync`. A
|
||
component that passes its specs is therefore not proof of zone-free
|
||
production behavior when the spec calls `fixture.detectChanges()` itself.
|
||
- **IPC callbacks never ran in the Angular zone.** `window.electron.on*`
|
||
listeners arrive through `contextBridge` and are not zone-patched, so every
|
||
one that works today already writes signals or calls `NgZone.run`.
|
||
- **Counters before the migration** (macOS, from
|
||
[performance journeys](performance-journeys.md#change-detection-ticks)):
|
||
`renderer.cdTicksToFirstCard` 20–21 (the one-tick zone.js race),
|
||
`renderer.cdTicksIdle30s` 3, `renderer.cdTicksToFirstPage` 22;
|
||
`renderer.cdTicksToPlaying` has no recorded run yet.
|
||
|
||
## PR sequence
|
||
|
||
1. [x] Keep `@ngrx/store-devtools` out of production bundles (#1810). Not a
|
||
zone change; it lowered `renderer.initialBytes` before the migration
|
||
starts moving it.
|
||
2. [x] This inventory and its guard spec.
|
||
3. [x] Per-project PRs (#1818 to #1823), in this order, each converting the project's Eager
|
||
components and fixing its zone-dependent sites while zone.js stays on:
|
||
`libs/ui/*`, `libs/workspace/*`, `libs/playlist/*`, `libs/portal/*`,
|
||
playback (`libs/ui/playback`, `libs/playlist/m3u/feature-player`),
|
||
`apps/web`. Each reports the tick counters before and after and runs the
|
||
affected unit and E2E tests.
|
||
4. [x] `provideZonelessChangeDetection()` behind a build-time
|
||
`fileReplacements` flag, off by default; the three implemented journeys
|
||
(J1-J3; J4 is still planned) and the Electron E2E suite run with it on
|
||
(see [Zoneless flag](#zoneless-flag)).
|
||
5. [x] Flag on by default, `zone.js` out of `polyfills`, new tick baselines
|
||
(`renderer.cdTicksIdle30s` and any counter that becomes deterministic once
|
||
the zone.js race is gone).
|
||
|
||
## Eager components
|
||
|
||
65 production files, 66 components (`epg-progress-panel.component.ts` holds
|
||
two); two have since been deleted (the old Sources row and its health dot). Tick an entry by deleting `changeDetection: ChangeDetectionStrategy.Eager`
|
||
(or setting OnPush) once its template state is signals, signal inputs or
|
||
explicitly marked. The guard spec compares the unticked entries with the
|
||
files whose component metadata still sets
|
||
`changeDetection: ChangeDetectionStrategy.Eager` (comments do not count).
|
||
The settings sections read form values in their templates and the parent
|
||
patches the form outside their events (Discard, backup import, the EPG file
|
||
picker), so each marks itself on the form's `events` through
|
||
`markSectionForCheckOnFormEvents` (`apps/web/src/app/settings`).
|
||
|
||
### apps/web (15)
|
||
|
||
- [x] `apps/web/src/app/app.component.ts` (idle audit root)
|
||
- [x] `apps/web/src/app/app-update-notification-panel.component.ts` (idle audit root)
|
||
- [x] `apps/web/src/app/settings/app-update-release-notes-dialog.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-about-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-backup-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-dashboard-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-delete-all-playlists-dialog.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-epg-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-general-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-playback-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-remote-control-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-tmdb-section.component.ts`
|
||
- [x] `apps/web/src/app/settings/settings-unsaved-changes-dialog.component.ts`
|
||
|
||
### libs/ui (20 files, 21 components)
|
||
|
||
- [x] `libs/ui/components/src/lib/confirm-dialog/confirm-dialog.component.ts`
|
||
- [x] `libs/ui/components/src/lib/content-hero/content-hero.component.ts`
|
||
- [x] `libs/ui/components/src/lib/expandable-text/expandable-text.component.ts`
|
||
- [x] `libs/ui/components/src/lib/portal-detail-shell/content-about.component.ts`
|
||
- [x] `libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.ts`
|
||
- [x] `libs/ui/components/src/lib/progress-capsule/progress-capsule.component.ts`
|
||
- [x] `libs/ui/components/src/lib/season-container/episode-info-dialog.component.ts`
|
||
- [x] `libs/ui/components/src/lib/watched-badge/watched-badge.component.ts`
|
||
- [x] `libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts`
|
||
- [x] `libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.ts` (idle audit root; also `EpgTrustConfirmDialogComponent`)
|
||
- [x] `libs/ui/epg/src/lib/epg-source-status/epg-source-status.component.ts`
|
||
- [ ] `libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts` (`apps/remote-control-web` only)
|
||
- [x] `libs/ui/playback/src/lib/art-player/art-player.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/external-player-info-dialog/external-player-info-dialog.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/vod-details/vod-details.component.ts`
|
||
- [x] `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`
|
||
|
||
`libs/ui/playback` (8) goes with the playback PR, not the `libs/ui` one.
|
||
|
||
### apps/remote-control-web (1)
|
||
|
||
- [ ] `apps/remote-control-web/src/app/app.ts` (separate app; converts with `remote-control.component.ts`)
|
||
|
||
### libs/workspace (7)
|
||
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-command-palette/workspace-command-palette.component.ts`
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-collection-context-panel.component.ts`
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-context-panel.component.ts`
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-context-panel/workspace-settings-context-panel.component.ts`
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-keyboard-shortcuts/workspace-keyboard-shortcuts-dialog.component.ts`
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.ts` (idle audit root)
|
||
- [x] `libs/workspace/shell/feature/src/lib/workspace-sources/workspace-sources.component.ts`
|
||
|
||
### libs/playlist (12)
|
||
|
||
- [x] `libs/playlist/import/feature/src/lib/add-playlist-dialog/add-playlist-dialog.component.ts`
|
||
- [x] `libs/playlist/import/feature/src/lib/auto-import/auto-import.component.ts`
|
||
- [x] `libs/playlist/import/feature/src/lib/file-upload/file-upload.component.ts`
|
||
- [x] `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts`
|
||
- [x] `libs/playlist/import/feature/src/lib/text-import/text-import.component.ts`
|
||
- [x] `libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts`
|
||
- [x] `libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts`
|
||
- [x] `libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts`
|
||
- [x] `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts`
|
||
- [x] `libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts`
|
||
- [x] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts`
|
||
- [x] `libs/playlist/shared/ui/src/lib/source-health/source-cleanup-dialog.component.ts`
|
||
|
||
`libs/playlist/m3u/feature-player` (2) goes with the playback PR.
|
||
|
||
### libs/portal (8)
|
||
|
||
- [x] `libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts`
|
||
- [x] `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts`
|
||
- [x] `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
|
||
- [x] `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
|
||
- [x] `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
|
||
- [x] `libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts`
|
||
- [x] `libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts`
|
||
|
||
Test-only files that set Eager are not listed; they do not ship. The guard
|
||
skips every `*.spec.ts` / `*.test.ts` file with or without a suffix of one
|
||
or more segments (`*.spec-stubs.ts`, `*.test-helpers.ts`,
|
||
`*.test-data-stubs.ts`, …),
|
||
`test-setup.ts` and `test-stubs/` directories.
|
||
|
||
## Zone-dependent sites
|
||
|
||
Plain (non-signal) fields read by a template and written from a callback
|
||
that is not an Angular template event. Under zone.js the next tick happens to
|
||
refresh an Eager view; under zoneless nothing schedules one. Each fix makes
|
||
the field a signal (or a `computed`), or writes it through one.
|
||
|
||
| Done | Site | What depends on the zone | Owning PR |
|
||
| --- | --- | --- | --- |
|
||
| [x] | `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts` `onChannelNumberInput`/`clearChannelNumberInput` | 2 s `window.setTimeout` hides the channel-number overlay through plain `showChannelNumberOverlay`/`channelNumberInput` | playback |
|
||
| [x] | same file, `applySettings` and the settings `effect()` | IndexedDB `storage.get(...).subscribe` and an effect assign plain `playerSettings`, which picks the player in the template | playback |
|
||
| [x] | `libs/portal/shared/data-access/src/lib/source-status.service.ts` `checkPortal` (replaced the old Sources row's `checkPortalStatus`) | the PWA Xtream portal status lands after `await`; it is written to a signal | playlist |
|
||
| [x] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` (EPG clear and EPG file pick handlers) | plain `playlist` reassigned after `await` | playlist |
|
||
| [x] | `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` (device-id derivation) | `form.patchValue` after `await`; no fix needed: while derivation is on `hasManualDeviceIds` is false whatever the IDs are, the controls write their own DOM values, and typing goes through value-accessor listeners that mark the view | playlist |
|
||
| [x] | `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` (favorites load) | `favorites` Map filled in a `subscribe` without `markForCheck`; the component is OnPush already, so this is a latent bug today | portal |
|
||
| [x] | `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts` (favorites load) | same pattern; the neighbouring `favoriteMarks.changes$` handler does call `markForCheck` | portal |
|
||
| [x] | same file, programme dialog `afterClosed` | deletes from `epgPrograms`/`currentProgramsProgress` after `await` without marking | portal |
|
||
| [x] | `apps/web/src/app/settings/settings-backup.facade.ts` (backup import) | `change` listener on a detached file input → `hydrateFromStore()`; section templates read `form().value.theme`/`coverSize`; each section now marks itself on its form's `events` (`markSectionForCheckOnFormEvents`), which `settings.component.form.spec.ts` guards without a forced render | apps/web |
|
||
| [x] | `libs/ui/epg/src/lib/epg-guide/epg-guide.component.ts` (jump to now, keyboard focus) | `afterNextRender` registered from CDK/RxJS callbacks; zone.js followed them with a tick, zoneless schedules no render, so the guide opened at midnight. It now marks itself when it registers the hook. Found by `epg-guide.e2e.ts` on the zoneless build | flag |
|
||
| [ ] | `libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts` | plain `isLoading`/`error`/`status` written after `await` and from a 2 s `setInterval` | only if `apps/remote-control-web` goes zoneless |
|
||
|
||
## Explicit zone and change-detector calls
|
||
|
||
They kept working under zoneless (`NgZone` becomes `NoopNgZone`, so `run`
|
||
and `runOutsideAngular` just call through) and were removed in the flip PR,
|
||
because with zone.js on they still mattered. The embedded MPV position poll's
|
||
spec case that asserted `runOutsideAngular` went with it.
|
||
|
||
- [x] `apps/web/src/app/settings/settings-unload-guard.service.ts`: two
|
||
`zone.run` calls around the window-close dialog (IPC
|
||
`onWindowCloseRequested` and `beforeunload`).
|
||
- [x] `libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts`:
|
||
`runOutsideAngular(() => setInterval(...))` for the position poll;
|
||
`embedded-mpv-session-controller.position.spec.ts` asserts the call and
|
||
changes with it.
|
||
- [x] `libs/workspace/dashboard/data-access/src/lib/dashboard-data.service.ts`:
|
||
18 `ngZone.run(() => signal.set(...))` calls, all redundant around signal
|
||
writes.
|
||
- [x] `libs/workspace/dashboard/data-access/src/lib/dashboard-source-expiry.service.ts`:
|
||
one `ngZone.run` around a signal update.
|
||
- `ChangeDetectorRef` in `stalker-live-stream-layout.component.ts`
|
||
(4 × `markForCheck`, 1 × `detectChanges` before measuring a row) and
|
||
`portal-channels-list.component.ts` (3 × `markForCheck`, 2 ×
|
||
`detectChanges`): correct under zoneless; replace the Maps with signals in
|
||
the portal PR if it stays small.
|
||
|
||
## Checked and signal-safe
|
||
|
||
No change needed; recorded so the flag PR knows where to look if a journey
|
||
regresses. Embedded MPV and external players are the riskiest paths because
|
||
their events arrive over IPC.
|
||
|
||
- **IPC listeners** (17 registrations): app update status, external player
|
||
sessions, window close and window state, player errors, playlist open
|
||
requests, embedded MPV sessions, playback history gate, downloads,
|
||
recordings, playlist refresh, DB operation and save progress, EPG progress,
|
||
playback position updates, channel change and remote-control commands. All
|
||
write signals, signal stores or NgRx, or have no UI state.
|
||
- **Player libraries** (video.js, mpegts.js, hls.js, artplayer, shaka, native
|
||
`<video>`): callbacks bump signals in the control adapters or emit outputs
|
||
whose parent handlers write signals.
|
||
- **Observers** (13 Intersection/Resize/Mutation observers) and **document
|
||
and window listeners** (~40): signals or DOM only. `@HostListener`
|
||
bindings are Angular listeners and mark their view.
|
||
- **Timers** (~130 `setTimeout`/`setInterval`/rAF/`queueMicrotask`): all
|
||
write signals, touch the DOM or focus, or have no UI state, apart from the
|
||
two in the table above.
|
||
- **Dialogs and snackbars** (20 `afterClosed`/`onAction` sites): signals,
|
||
stores, outputs or navigation, apart from the one in the table above.
|
||
- No production code uses `NgZone.onStable`, `onMicrotaskEmpty`, `isStable`,
|
||
`ApplicationRef.tick()`, `Zone.current` or `ngDoCheck`.
|
||
|
||
## Build, tests and runtime details
|
||
|
||
- `provideServiceWorker(..., { registrationStrategy:
|
||
'registerWhenStable:30000' })`: under zoneless "stable" means no pending
|
||
tasks; the 30 s bound registers the worker even if a task stays pending.
|
||
- [x] `change-detection-tick-counter.ts` wraps `ApplicationRef._tick`, which
|
||
the zoneless scheduler also calls, so the counters stay comparable.
|
||
- Specs that need zone.js: `fakeAsync` in
|
||
`playlist-switcher.component.spec.ts` and `stalker-live-navigation.spec.ts`,
|
||
`waitForAsync` in 13 files. They keep `zone.js/testing` (and the `zone.js`
|
||
package) until rewritten; the build polyfills do not affect them.
|
||
`apps/remote-control-web` still bootstraps with zone.js.
|
||
- [x] Unreferenced leftovers deleted in the flip PR:
|
||
`apps/web/src/polyfills.ts`, `apps/web/src/polyfills-test.ts`,
|
||
`apps/web/src/setup-jest.ts` (no project, tsconfig or Jest config uses
|
||
them).
|
||
|
||
## Zoneless flag
|
||
|
||
Between #1824 and the flip, `app.config.ts` took its change-detection
|
||
providers from `environments/change-detection.providers.ts`, and two
|
||
`*-zoneless` web configurations swapped in `provideZonelessChangeDetection()`
|
||
so the journeys and the Electron E2E suite could run zoneless before anything
|
||
shipped it. The flip PR removed the flag: `app.config.ts` provides
|
||
`provideZonelessChangeDetection()` directly, `apps/web` builds without the
|
||
`zone.js` polyfill, and `performance-build-config.spec.ts` fails if either
|
||
comes back.
|
||
|
||
First measurement (macOS, 2026-10-04, the six OnPush PRs merged locally on
|
||
the flag branch; the same integration build measured with the flag off and
|
||
on, five iterations each):
|
||
|
||
| Counter | flag off | flag on |
|
||
| --- | --- | --- |
|
||
| `renderer.cdTicksToFirstCard` | 22, 20, 22, 21, 21 | 7, 7, 7, 7, 7 |
|
||
| `renderer.cdTicksIdle30s` | 4, 4, 4, 4, 4 | 3, 3, 3, 3, 3 |
|
||
| `renderer.cdTicksToFirstPage` | 22 (all) | 8 (all) |
|
||
| `renderer.cdTicksToPlaying` | 16, 15, 15, 17, 15 | 6, 8, 10, 10, 7 |
|
||
| DOM mutations J1 / J2 / J3 | 553 / 1,603 / 6,182 | 553 / 1,603 / 6,182 (one J3 iteration 6,199, as on master) |
|
||
| `spawnToFirstCardMs` p50 | 3,071 | 828 |
|
||
| `clickToFirstPageMs` p50 | 82.8 | 80.4 |
|
||
| `clickToLoadedMetadataMs` / `clickToPlayingMs` p50 | 94.8 / 268.7 | 158.3 / 409.3 |
|
||
|
||
J1 and J2 tick counts become deterministic without the zone.js one-tick
|
||
race, and the DOM mutations are unchanged, so nothing renders differently.
|
||
J3's tick count still varies with player events and its wall-clock
|
||
numbers rose locally; the machine was shared with other runs (load 26 to 58
|
||
during these two runs, master itself read 363.8 ms `clickToPlayingMs` p50
|
||
earlier the same day), so judge J3 on the CI runner before the flip.
|
||
|
||
Electron E2E suite on `electron-e2e-zoneless` (all specs except the
|
||
packaged frame-copy ones): 203 passed, 7 skipped, 3 failed. `epg-guide`
|
||
failed on every run and is fixed above; `playlist-auto-refresh` passed on
|
||
`--repeat-each=2`; `dash-clearkey` "reopens from recent and favorites" is the
|
||
known local flake (it fails as often on master). The IPC-driven paths
|
||
passed zoneless: external-player launch states and the MPV/VLC DASH
|
||
fallbacks in `dash-clearkey`, the MPV double-click gate in `settings`,
|
||
`remote-control`, `picture-in-picture` and `stream-info`. Their state reaches
|
||
the renderer over IPC into signals and never ran in the zone. Embedded MPV
|
||
playback itself is covered only by the packaged frame-copy E2E: it passed on a
|
||
packaged zoneless Linux x64 build from throwaway draft #1832 (see the comment
|
||
on #1824), with every packaging job and 8 of 9 cross-platform E2E shards green
|
||
(the ninth failed `dashboard-hero-legibility`, which failed the same day on
|
||
zone.js PRs too).
|
||
|
||
## Measuring a PR
|
||
|
||
Build `electron-performance` and run the journeys as described in
|
||
[performance journeys](performance-journeys.md), then paste
|
||
`renderer.cdTicksToFirstCard`, `renderer.cdTicksIdle30s`,
|
||
`renderer.cdTicksToFirstPage` and `renderer.cdTicksToPlaying` before and
|
||
after. Until the flip, while zone.js was on, removing Eager did not change
|
||
the number of ticks, only the work per tick. Since the flip (#1929)
|
||
`renderer.cdTicksToFirstCard` (5, with one tick of slack for the lazy
|
||
Material chunk since #1931), `renderer.cdTicksIdle30s` (3) and
|
||
`renderer.cdTicksToFirstPage` (9) are ratchet baselines: a change that adds a
|
||
tick to launch, to the idle dashboard or to opening a source fails the
|
||
journeys job.
|
||
|
||
Canonical runner (`Performance ratchet` dispatched on #1929, three runs, five
|
||
iterations each) against `master` the same day with zone.js:
|
||
|
||
| Counter | `master` (zone.js) | #1929 (zoneless) |
|
||
| --- | --- | --- |
|
||
| `renderer.cdTicksToFirstCard` | 31 (2026-10-04) | 5 |
|
||
| `renderer.cdTicksIdle30s` | 1 to 4 | 3 |
|
||
| `renderer.cdTicksToFirstPage` | 22 | 9 |
|
||
| `renderer.cdTicksToPlaying` | 15 to 17 | 7 or 8 |
|
||
| `renderer.initialBytes` | 1,596,201 | 1,545,931 |
|
||
| J3 `clickToPlayingMs` p50 | 151.6, 159.2, 185.7 | 128.0, 178.6, 189.5 |
|
||
|
||
J3 wall clock is in the same range, so the local rise measured on the flag
|
||
PR was machine load.
|