mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
Merge remote-tracking branch 'origin/master' into perf/cd-tick-counters
# Conflicts: # apps/electron-backend-e2e/src/performance/journey-renderer-probe.spec.ts # apps/electron-backend-e2e/src/performance/journey-renderer-probe.ts
This commit is contained in:
commit
d9446db053
72 files changed
+3976
-604
No files matched your search
@@ -91,7 +91,9 @@ themes and fullscreen; frame-copy and built-in shared controls keep their
|
||||
light-on-dark overlay palette — the fixed `--pc-*` token set of the shared
|
||||
dock (accent blue, cyan, violet, the `--pc-live` / `--pc-danger` reds and a
|
||||
light text ramp), never the app theme. The overlay styles in
|
||||
`player-controls/` never read a `--mat-sys-*` token.
|
||||
`player-controls/` never read a `--mat-sys-*` token, and their keyboard focus
|
||||
is a 2px `--pc-text` outline rather than Material's theme-coloured focus layer
|
||||
(`player-controls-keyboard.e2e.ts` checks it in both themes).
|
||||
|
||||
EPG timeline, list, empty states and programme details use the library-local
|
||||
`libs/ui/epg/src/lib/_epg-theme.scss` palette, based on app surfaces, separators,
|
||||
@@ -627,6 +629,13 @@ and `--app-selection-color`; hover uses the app's neutral surface treatment.
|
||||
Keep these treatments in the shared season components and detail-action
|
||||
partial so Xtream and Stalker share the same behavior.
|
||||
|
||||
The season header's actions wrap onto their own row, starting under the
|
||||
"Seasons and Episodes" heading, before the heading itself would wrap. The
|
||||
detail pane is narrower than the window (context panel, the sticky Back lane),
|
||||
so the header's own width decides, not a viewport breakpoint. A translation
|
||||
wider than the pane itself wraps rather than ellipsizing: unlike a fixed-height
|
||||
panel title, a content heading has room to wrap and should not lose words.
|
||||
|
||||
Browser regression coverage measures the composited neutral edges and selected
|
||||
toggle fill, in addition to capturing light/dark grid and list screenshots.
|
||||
Hero action edges use matching pixels from rendered screenshots with the border
|
||||
|
||||
@@ -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 is instrumented by its own
|
||||
spec (below). J3 and J4 follow the plan in `.plans/` and are added one thread
|
||||
at a time; each thread names its journey and counter in the PR description.
|
||||
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.
|
||||
|
||||
## Running the journeys
|
||||
|
||||
@@ -448,6 +448,13 @@ values, so a new counter needs no schema change. A J1 runtime baseline is added
|
||||
once its counter is deterministic on the CI runner; the launch counters are
|
||||
not yet (see [Ratchet](#ratchet)), so the summary is evidence only.
|
||||
|
||||
J3 adds the `journeys.playback` entry with the same shape and no schema
|
||||
version change: `counters` and `wallClock` hold only plain numbers, and its
|
||||
iterations carry `evidence.media` (the video element at `playing`) and
|
||||
`evidence.epochs.loadedMetadata` / `.playing`. The renderer probe blob gained
|
||||
a `media` field (`null` for J1 and J2), which the probe's
|
||||
`schemaVersion` 1 readers ignore.
|
||||
|
||||
## J2 `open-source`: open a source to a browsable list
|
||||
|
||||
`open-source.journey.ts` reuses the J1 profile and process pattern: the
|
||||
@@ -550,6 +557,132 @@ give a click-to-settled count; that is left to a follow-up.
|
||||
|
||||
All epochs are taken in the renderer, so neither entry crosses a process clock.
|
||||
|
||||
## J3 `playback`: start playback to the first frame
|
||||
|
||||
`playback.journey.ts` follows J2: the profile is seeded once through the
|
||||
"Add playlist" dialogs (`seedLaunchJourneyProfile` with
|
||||
`PLAYBACK_JOURNEY_SEED`), every iteration copies it, spawns a fresh process
|
||||
through `runLaunchJourney` without main-process counters, and hands the
|
||||
running app to `measurePlaybackJourney` in
|
||||
`src/journeys/playback-journey-app.ts`. One warm-up and five measured
|
||||
iterations.
|
||||
|
||||
**Profile.** J2's M3U source plus an Xtream portal ("Journey live portal") on
|
||||
the mock's `live-fallback:live-fallback` account, behind the same request
|
||||
ledger proxy. Seeding also selects **Settings > Playback > Video player >
|
||||
HTML5 video player** and **Stream format > ts** through the settings page
|
||||
(`configureLiveFormat`), so live URLs end in `.ts`. Embedded MPV and external
|
||||
players are out of scope.
|
||||
|
||||
**Stream.** `/live/live-fallback/live-fallback/10000.ts` returns
|
||||
`apps/xtream-mock-server/src/fixtures/live.mpegts` from disk: six seconds of
|
||||
160x90 H.264 baseline video and AAC audio in MPEG-TS, about 300 KB. The HTML5
|
||||
player plays `.ts` through mpegts.js, which transmuxes to fragmented MP4 for
|
||||
Media Source Extensions; H.264 and AAC are among the codecs Electron's
|
||||
Chromium decodes on every platform, including the Linux runner, and the
|
||||
Electron E2E for the live-format fallback already plays this fixture there.
|
||||
Two other choices were rejected: the `marketing` and `marketing2` accounts
|
||||
serve live URLs from local bytes, but those bytes are zero-filled (a fixture
|
||||
for download screenshots, not media), so no player ever fires `playing`; every
|
||||
other account redirects streams to a public HLS test stream. The record
|
||||
fails an iteration whose click-to-`playing` window has no `.ts` request for
|
||||
a live stream, so a player that played something else is never measured.
|
||||
|
||||
**No request leaves the machine.** The generated live catalog's channel and
|
||||
category logos point at `picsum.photos`. Before navigating, the journey
|
||||
registers `session.defaultSession.webRequest.onBeforeRequest` for
|
||||
`*://picsum.photos/*` from the test side and cancels those requests (the app
|
||||
registers no `onBeforeRequest` listener of its own, so none is replaced). A
|
||||
logo therefore never loads, or fails, at a moment that depends on the
|
||||
runner's network; the number cancelled is kept as
|
||||
`evidence.externalArtworkCancelled`. Every other request goes to the mock
|
||||
through the ledger.
|
||||
|
||||
**Start.** After J1 has ended, the test clicks the portal's dashboard card,
|
||||
the **Live TV** link and the first category (not measured), then installs the
|
||||
IPC capture with a start sentinel, arms the probe, hovers the first
|
||||
`app-live-stream-layout [data-test-id="channel-item"]` and waits for the same
|
||||
1 s quiet as J2 (`src/performance/journey-click-settle.ts`, shared with J2).
|
||||
J1's capture is detached and the channel is clicked. The probe's capture-phase
|
||||
`click` listener stamps the start and sends
|
||||
`cancelSourceProbe('__iptvnator-journey-playback-start__')`. The record
|
||||
rejects activity between the settle snapshot and the click exactly as J2
|
||||
does.
|
||||
|
||||
**End.** The probe runs with `media: { endEvent: 'playing', phaseEvents:
|
||||
['loadedmetadata'] }`. Media events do not bubble, but a capture-phase
|
||||
listener on `window` sees them before any listener of the app. The first
|
||||
`playing` event after the start on an element matching
|
||||
`app-web-player-view video` ends the journey: pending mutation records are
|
||||
taken synchronously, the end sentinel
|
||||
`cancelSourceProbe('__iptvnator-journey-playback-end__')` is sent, and the
|
||||
element's state is recorded (`evidence.media`: `readyState`, `paused`,
|
||||
`currentTime`, intrinsic size, and `currentSrcScheme`, which is `blob` for
|
||||
Media Source playback). The first `loadedmetadata` on such an element after
|
||||
the start is recorded as a phase. A visible video element does not end the
|
||||
journey, and media events before the click or on other elements are
|
||||
ignored.
|
||||
|
||||
### Counters
|
||||
|
||||
| Counter | Source |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `renderer.ipcCallsToPlaying` | Bridge `start` trace events between the start and end sentinels, as `renderer.ipcCallsToFirstPage` in J2. |
|
||||
| `renderer.httpRequestsToPlaying` | Requests the ledger proxy received from the click stamp until the `playing` stamp, from either process (the stream request comes from the renderer, Xtream API calls from the main process). Both stamps are `performance.timeOrigin + performance.now()` of processes on the same host clock, as in J2. Unlike J2's counter the window ends at the terminal, not at a quiet mock: a live stream has no quiet end. Later requests are kept as `evidence.httpRequestsAfterPlayingByRoute`, the ones in the window as `evidence.httpRequestsByRoute`. |
|
||||
| `renderer.domMutationsToPlaying` | `MutationRecord`s from the click until the `playing` event, including records still queued when it fires. |
|
||||
| `renderer.cdTicksToPlaying` | `ApplicationRef` ticks from the click until the `playing` event, read like `renderer.cdTicksToFirstPage` in J2 (see [Change-detection ticks](#change-detection-ticks)). Not yet measured on a run; the counter shipped after J3's first measurements. |
|
||||
| `renderer.layoutShiftScore` | All `layout-shift` entries from the click until the `playing` event, including `hadRecentInput` ones (as J2), rounded to three decimals. Entries delivered up to the post-paint cutoff are read, but only those that started by the event count. |
|
||||
| `renderer.longTasks` | `longtask` entries over 50 ms whose time range overlaps the window from the click to the `playing` event, so the task that dispatched the event counts. Evidence until shown to be stable on the runner. |
|
||||
|
||||
`renderer.httpRequestsToPlaying` compares the ledger's arrival stamps
|
||||
(test process) with the renderer's click and `playing` stamps. Both are
|
||||
`performance.timeOrigin + performance.now()` on the same host clock, but the
|
||||
two processes' time origins can differ by a fraction of a millisecond, so
|
||||
every iteration records `evidence.httpBoundaryMarginsMs`: the distance of
|
||||
the nearest request on either side of the click and of `playing`. A margin
|
||||
of a few milliseconds means a clock difference could move that request
|
||||
across the boundary. Locally the first request after the click arrives 3-6
|
||||
ms after its stamp (the click causes it, so it cannot precede the click) and
|
||||
the nearest request to `playing` is more than 170 ms away; both are well
|
||||
above a sub-millisecond origin difference. `evidence.httpRequestsAfterPlayingByRoute` covers a fixed
|
||||
window of 1 s after `playing` (the test waits that long before reading the
|
||||
ledger), not a quiet mock as in J2: a live stream has no quiet end.
|
||||
|
||||
One counter is listed under `unavailable`.
|
||||
`renderer.ipcSerialDepthToPlaying` is missing because the serial-depth
|
||||
helper (see [Startup work before the first card](#startup-work-before-the-first-card))
|
||||
was not on `master` when J3 landed; J3 adopts it once the J1 thread adds it.
|
||||
`main.sqlStatementsToPlaying` is not measured for the same reason as J2's
|
||||
SQL counter.
|
||||
|
||||
### Wall-clock
|
||||
|
||||
| Entry | Derivation |
|
||||
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `clickToPlayingMs.p50/.p90` | `playing` epoch minus start epoch. |
|
||||
| `clickToLoadedMetadataMs.p50/.p90` | First `loadedmetadata` epoch minus start epoch: player setup, the stream request and the first transmuxed init segment. |
|
||||
|
||||
The difference of the two is stream start: buffering until the element can
|
||||
play. All epochs are taken in the renderer.
|
||||
|
||||
### First measurement
|
||||
|
||||
Local, macOS, 2026-09-30 (two `perf:journeys` runs, five measured iterations
|
||||
each): every counter identical in all ten, `renderer.ipcCallsToPlaying` 4
|
||||
(`getEpgMapping`, `xtreamRequest`, `updateRemoteControlStatus`,
|
||||
`setUserAgent`), `renderer.httpRequestsToPlaying` 2 (the `.ts` stream and
|
||||
`get_simple_data_table`), `renderer.domMutationsToPlaying` 6,188,
|
||||
`renderer.layoutShiftScore` 0.001, `renderer.longTasks` 0; P50
|
||||
click→`loadedmetadata` 92-94 ms and click→`playing` 239-257 ms. The warm-up
|
||||
iteration of the first run took the cold path (888 ms to `playing`, one more
|
||||
`updateRemoteControlStatus` call); warm-ups are excluded. About 6,000 of the
|
||||
mutations come from the EPG timeline rendering about 240 programme blocks
|
||||
from the `get_simple_data_table` response before the first frame. Whether
|
||||
that response and its render land before `playing` is a race on a slower
|
||||
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.
|
||||
|
||||
## `renderer.initialBytes`
|
||||
|
||||
The bytes a browser fetches before Angular can bootstrap, read from the built
|
||||
@@ -845,10 +978,12 @@ reports slow imports of non-Latin playlists.
|
||||
continues from J1 with `runLaunchJourney` and lets the app settle first,
|
||||
as `open-source-journey-app.ts` does.
|
||||
2. Give the journey its own probe options (`cardSelector`,
|
||||
`companionSelectors`, `routeFragment`, `startClick` for a click start) or
|
||||
extend `journey-renderer-probe.ts` when the end condition is not "elements
|
||||
became visible". Use a state key and sentinel ids of its own. Keep the
|
||||
probe self-contained: Playwright serializes it with `toString()`.
|
||||
`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
|
||||
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
|
||||
`<journey>-journey-record.ts` under `src/performance/`; name counters
|
||||
`renderer.*` or `main.*`, and list counters you cannot measure under
|
||||
|
||||
@@ -297,7 +297,8 @@ It owns only transient presentation behavior:
|
||||
- `ControlsSettings` — the settings panel's groups, on/modified state and
|
||||
open/close transitions (`controls-settings-groups.ts` holds the pure
|
||||
group-availability rule);
|
||||
- `app-player-settings-panel` — the panel / bottom sheet presentation;
|
||||
- `app-player-settings-panel` — the panel / bottom sheet presentation,
|
||||
whose radio groups use `settings-radio-group.directive.ts`;
|
||||
- `ControlsUpNext` and `app-player-up-next-card` — the "Up next" card's
|
||||
gate and presentation; and
|
||||
- `controls-view-model.ts` — derived display state.
|
||||
@@ -346,7 +347,11 @@ app's `--app-selection-color` is a different blue that would fight the video.
|
||||
measured below 3:1 against the lighter fills.
|
||||
- **Icon buttons** are 40px with a 12px radius (32px / 9px compact) through
|
||||
Material's `--mat-icon-button-*` tokens; their hover is a flat
|
||||
`rgba(255,255,255,.1)` layer.
|
||||
`rgba(255,255,255,.1)` layer. Keyboard focus draws a 2px `--pc-text`
|
||||
outline (`:focus-visible`), and Material's focus state layer is switched
|
||||
off: its colour comes from the app theme, and the light theme's dark layer
|
||||
left no visible focus on video. The settings panel's icon buttons use the
|
||||
same ring, inset by 2px because the panel body clips at its edge.
|
||||
|
||||
### Timeline segments
|
||||
|
||||
@@ -458,7 +463,10 @@ the panel the moment the last group disappears.
|
||||
|
||||
- **Roomy wide dock** (≥ 960px): two **value chips** precede `tune` — subtitles
|
||||
(`closed_caption` + the selected track's label, or "Off") and speed
|
||||
(`speed` + `1.25×`). Audio and aspect ratio have no chip: they are
|
||||
(`speed` + `1.25×`). A chip's accessible name and tooltip carry its
|
||||
value (`SUBTITLES_TOOLTIP` "Subtitles: English", `SPEED_TOOLTIP`
|
||||
"Speed: 1.25×"); `tune` has `aria-haspopup="dialog"` and
|
||||
`aria-expanded`. Audio and aspect ratio have no chip: they are
|
||||
panel-only. A chip click opens the panel **focused on its group**
|
||||
(`settingsFocus`; the group scrolls into view and wears a brief ring);
|
||||
right-click or long-press on the subtitle chip toggles subtitles without
|
||||
@@ -476,12 +484,34 @@ the panel the moment the last group disappears.
|
||||
picture and recording stay in the compact dock — the mock shows only
|
||||
`tune` + fullscreen there, but those two are engine features a viewer
|
||||
needs without opening anything.
|
||||
- **Inside**: list groups (audio, subtitles, quality) use `menuitemradio`
|
||||
rows with a check mark and a cyan selection; segmented groups (speed,
|
||||
aspect) use `radio` items with a violet selection, and a selected default
|
||||
(`1×`, the first aspect preset) stays neutral. The subtitle group carries
|
||||
the load-file action and the delay / size / color sections that the
|
||||
popover used to hold (same `player-controls-load-subtitle`,
|
||||
- **Inside**: every choice is a `radio` in a `radiogroup`
|
||||
(`SettingsRadioGroupDirective` / `SettingsRadioDirective`). List groups
|
||||
(audio, subtitles, quality) are rows with a check mark and a cyan
|
||||
selection; segmented groups (speed, aspect, subtitle size) have a violet
|
||||
selection, and a selected default (`1×`, the first aspect preset) stays
|
||||
neutral. Each group is one Tab stop — the checked option as the engine
|
||||
reports it, else the first — and arrows, Home and End move focus with a
|
||||
CDK `FocusKeyManager` (wrapping; the horizontal arrows follow `direction`)
|
||||
and check the option they reach, as a native radio group does: the
|
||||
directive clicks it, so the template's handler applies the choice, even
|
||||
onto an option the engine still reports as checked: returning to it must
|
||||
cancel a switch that is still pending. Focus
|
||||
changes also write the roving `tabindex` immediately, because a quick
|
||||
Shift+Tab, Tab can arrive before change detection updates the bindings. The dialog is named by its `h2` title (`aria-labelledby`), each
|
||||
radio group by its `h3` group heading or `h4` subheading, and the delay
|
||||
buttons form a labelled `group`. Headings read `--pc-text-secondary` on
|
||||
`--pc-glass-bg-dense` (`rgba(12,16,23,.86)`), 4.5:1 or more even over a
|
||||
white frame. They wrap with `overflow-wrap: anywhere` and `hyphens: auto`,
|
||||
so one long word (German "Wiedergabegeschwindigkeit") breaks inside the
|
||||
sheet's 84px heading column. Hyphenation needs a `lang` on `<html>`.
|
||||
With subtitles on but no track marked selected yet (the engine can report
|
||||
the switch before the track list), the subtitle chip reads "On"
|
||||
(`SUBTITLES_ON`), never "Off".
|
||||
Every focused option shows the `--pc-text` ring; a selected swatch has a
|
||||
white border with a dark inner gap, and focus adds an outer ring. The
|
||||
subtitle group carries the load-file action (a plain button outside the
|
||||
radio group) and the delay / size / color sections that the popover used
|
||||
to hold (same `player-controls-load-subtitle`,
|
||||
`player-controls-subtitle-delay`, `player-controls-subtitle-style` test
|
||||
ids). The panel is a `role="dialog"` with `tabindex="-1"`: opened from
|
||||
the keyboard (the opener is `:focus-visible`) it takes focus, a pointer
|
||||
@@ -1639,6 +1669,7 @@ libs/ui/playback/src/lib/player-controls/
|
||||
├── player-settings-panel.component.ts
|
||||
├── player-settings-panel.component.html
|
||||
├── player-settings-panel.component.scss
|
||||
├── settings-radio-group.directive.ts
|
||||
├── controls-fullscreen.ts
|
||||
├── controls-menu-selection.ts
|
||||
├── controls-menu-state.ts
|
||||
|
||||
@@ -30,6 +30,22 @@ The button has an opaque app-themed surface, visible keyboard focus, an Escape
|
||||
shortcut hint via native `title` and Electron `no-drag` hit testing. The hint
|
||||
does not create an overlay that could consume the first Escape press.
|
||||
|
||||
The control floats, so the content makes room for it: while it is shown, every
|
||||
content column (hero content, watch-state player, episodes, About and extras)
|
||||
starts after a 72 px lane — 16 px inset, 40 px button, 16 px gap — and no scroll
|
||||
position puts the arrow over a heading or the player. The shell derives the lane
|
||||
from the button's geometry and publishes it as `--detail-back-lane`, which
|
||||
`content-hero` and `content-about` read too; hosts with `backAvailable=false`
|
||||
keep their own inset. The lane is horizontal because watch state is short of
|
||||
height, not width. A pane narrower than 400 px is the exception — a phone, or a
|
||||
desktop pane beside the context panel — because a 72 px column there would clip
|
||||
the player's controls. The pane decides, not the viewport: a `ResizeObserver`
|
||||
on the shell's border box (scrollbar-independent) sets `shell-host--compact`,
|
||||
and the wrapper becomes an in-flow 56 px sticky bar on `--app-header-bg` that
|
||||
content scrolls beneath and that takes the taps over it. Electron E2E
|
||||
(`detail-back-lane.e2e.ts`) sweeps both states with the lane at 1280 and
|
||||
780 px and with the bar at 700 and 375 px.
|
||||
|
||||
The sticky control is route-level Back in both states: it emits `backClicked`
|
||||
whether or not inline playback is active, so the arrow keeps one meaning and
|
||||
the list is one click away while watching. Only Escape unwinds one level: watch
|
||||
|
||||
@@ -776,6 +776,11 @@ tag a language at all, so in practice it is usually silent — which is the
|
||||
honest state, and the same one the rest of this feature takes when it does not
|
||||
know.
|
||||
|
||||
Undetermined language tags (`und`, including regional forms such as `und-US`)
|
||||
are explicitly treated as unknown after normalization. This must hold whether
|
||||
the runtime's `Intl.Locale` returns no language or preserves `und`: neither
|
||||
case establishes a factual dub difference against a known language.
|
||||
|
||||
Switching sources through `startResolvedPlayback` closes the external session
|
||||
it LAUNCHED first — tracked separately so refreshes and overlapping handoffs
|
||||
cannot disown it within the same route session. The retained identity is ignored
|
||||
|
||||
@@ -579,7 +579,11 @@ for the serve target's `--tsconfig` flag and is required for the `@iptvnator/*`
|
||||
path aliases. `project-config.spec.ts` pins which configs start which mock,
|
||||
and fails if any of them launches a mock through Nx or a new config starts one
|
||||
without being listed there. The `serve` targets remain the entry point
|
||||
for starting a mock by hand. The web-e2e `web-backend` entry uses the same
|
||||
for starting a mock by hand, and no E2E target may depend on them: with no
|
||||
Nx-launched server, `@nx/playwright` infers the atomized `e2e-ci--*` targets
|
||||
as non-parallel, and Nx refuses to run a non-parallel task that depends on a
|
||||
continuous `serve` task. The same spec guards the `e2e*` entries of `nx.json`
|
||||
`targetDefaults` and every target in the `apps/*-e2e` `project.json` files. The web-e2e `web-backend` entry uses the same
|
||||
launch form; see [PWA web backend](pwa-self-hosted.md#web-backend).
|
||||
|
||||
### Request Interception
|
||||
|
||||
Reference in new issue
Block a user