mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
Merge remote-tracking branch 'origin/master' into claude/parental-control-feature-31dde2
# Conflicts: # apps/electron-backend/src/main.ts
This commit is contained in:
commit
a030e1af2f
63 files changed
+4917
-262
No files matched your search
@@ -87,6 +87,11 @@ ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0
|
||||
- **No content deletion**: Hiding a category only affects sidebar visibility; the category and its content remain in the database
|
||||
- **Display order**: The sidebar defaults to server order. Users can switch the
|
||||
category panel to `A-Z` or `Z-A` from the sort menu next to category search.
|
||||
- **Selection scrolling**: The panel centers the rendered selected row after
|
||||
selection changes. Electron selects by local SQLite category ID; the row's
|
||||
`data-category-id` can contain its provider ID. Those IDs are not
|
||||
interchangeable when locating the scroll target, including after filtering
|
||||
hidden categories or sorting.
|
||||
- **All-hidden recovery**: Once the selected Xtream type is loaded, the manage
|
||||
categories button remains available even if every visible category has been
|
||||
hidden. The sidebar category list is filtered, but the dialog reads all
|
||||
|
||||
@@ -4,8 +4,10 @@ IPTVnator measures performance through a small set of everyday user journeys.
|
||||
Each journey has deterministic counters that are asserted exactly, and
|
||||
wall-clock timings that are recorded as evidence. Counters are ratcheted in CI:
|
||||
a committed baseline may only be lowered, and only with the measured output as
|
||||
evidence. This document is the contract for that loop; `tools/performance/`
|
||||
holds the scripts.
|
||||
evidence. This document is the contract for that loop. The journey harness lives in
|
||||
`apps/electron-backend-e2e/src/journeys` and
|
||||
`apps/electron-backend-e2e/src/performance/journey-*.ts`; the ratchet scripts
|
||||
live in `tools/performance/`.
|
||||
|
||||
## Journeys
|
||||
|
||||
@@ -16,9 +18,159 @@ holds the scripts.
|
||||
| J3 `playback` | click on a channel | HTML5 `playing` event |
|
||||
| J4 `search` | six-character query typed into global search | results list settled |
|
||||
|
||||
Only the J1 counter `renderer.initialBytes` is instrumented today. The other
|
||||
journeys and counters follow the plan in `.plans/` and are added one thread at
|
||||
a time; each thread names its journey and counter in the PR description.
|
||||
J1 is instrumented today: `renderer.initialBytes` from the built output, and
|
||||
the runtime counters of the launch benchmark below. J2 to 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.
|
||||
|
||||
## Running the journeys
|
||||
|
||||
```bash
|
||||
pnpm run perf:journeys
|
||||
```
|
||||
|
||||
The script runs the Nx target `electron-backend-e2e:journeys`, which builds the
|
||||
`electron-performance` configuration of the Electron app and the renderer
|
||||
first, starts the Xtream mock server on the dedicated loopback port
|
||||
`127.0.0.1:3231` (override with `IPTVNATOR_JOURNEY_XTREAM_MOCK_PORT`), and runs
|
||||
`playwright.journeys.config.ts` with one worker. Each run writes one file:
|
||||
|
||||
```
|
||||
dist/performance/journeys/<YYYYMMDDTHHMMSSZ>/summary.json
|
||||
```
|
||||
|
||||
The file is never overwritten; a second run in the same second fails instead.
|
||||
`IPTVNATOR_JOURNEY_MEASURED_ITERATIONS` lowers the five measured iterations
|
||||
for a quick local check; the warm-up iteration always runs. Numbers from a
|
||||
laptop are previews: the Linux CI runner is the canonical measurer for
|
||||
baselines, as it is for `renderer.initialBytes`.
|
||||
|
||||
## J1 `launch`: launch to usable
|
||||
|
||||
The profile holds one M3U source and one Xtream portal, both served by the
|
||||
Xtream mock (`/playlist.m3u` and `player_api.php` on the same origin). The
|
||||
profile is seeded once per run through the app's own "Add playlist" dialogs,
|
||||
then every iteration copies that seeded data directory into a fresh temporary
|
||||
directory and spawns a fresh Electron process on it. One warm-up iteration is
|
||||
recorded but excluded from the summary; five measured iterations follow. The
|
||||
app lands on `/workspace/dashboard`, so the first card is a card of the
|
||||
"Recent sources" rail; an `app-playlist-item` row on `/workspace/sources`
|
||||
also ends the journey for profiles that disable the dashboard.
|
||||
|
||||
The journey ends at the first `MutationObserver` batch in which all of the
|
||||
following hold: the location is below `/workspace`, `#initial-splash` is no
|
||||
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:
|
||||
|
||||
- `journey-renderer-gate.cjs` is loaded into the main process with `-r`, the
|
||||
mechanism Playwright uses for its own loader. Playwright resolves
|
||||
`electron.launch()` while the app is already creating its window, and
|
||||
Electron reports no page until a navigation commits, so an init script
|
||||
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.
|
||||
- `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
|
||||
scripts and emits one JSON blob under `window.__iptvnatorJourneyProbe`.
|
||||
- `journey-main-ipc-capture.ts` subscribes to the preload's renderer-API trace
|
||||
channel (`IPTVNATOR_DEBUG_TRACE_EVENT`, enabled with
|
||||
`IPTVNATOR_TRACE_IPC=1`) through `electronApp.evaluate`, also before the
|
||||
release. The record refuses an iteration whose gate timed out, saw a second
|
||||
load, or released before the probe was in place.
|
||||
|
||||
### Counters
|
||||
|
||||
| Counter | Source |
|
||||
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `renderer.ipcCallsToFirstCard` | `start` trace events the preload emits for every bridge invocation (listener registrations `on*`/`remove*` excluded, as in `wrapElectronApi`). The renderer probe fires one sentinel `dbGetAppPlaylist('__iptvnator-journey-sentinel__')` at the terminal moment; renderer-to-main IPC is ordered, so events before the sentinel are the exact count. |
|
||||
| `renderer.domMutationsToFirstCard` | `MutationRecord`s (not callback batches) from a `MutationObserver` on the document element with `childList`, `attributes`, `characterData` and `subtree`. When the init script runs before `<html>` exists the observer watches `document`, which the blob reports in `capabilities.observedTarget`. |
|
||||
| `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. |
|
||||
|
||||
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
|
||||
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.
|
||||
- `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
|
||||
|
||||
| Entry | Derivation |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `spawnToDidFinishLoadMs.p50/.p90` | `performance.timeOrigin + loadEventEnd` of the navigation entry (the main frame's `load`, which is what `did-finish-load` reports) minus the test-side timestamp taken just before `electron.launch`. |
|
||||
| `spawnToFirstCardMs.p50/.p90` | Terminal epoch of the renderer probe minus the same spawn timestamp. |
|
||||
|
||||
Percentiles use linear interpolation over the five measured iterations. The
|
||||
spawn timestamp includes Playwright's own launch overhead and the gate's
|
||||
`about:blank` detour: Playwright holds `app.whenReady()` until its CDP session
|
||||
is attached, and the real document loads only after the probes are in place,
|
||||
so absolute values are larger than a bare launch. They are comparable between runs of the same
|
||||
harness, which is what the ratchet needs. The main process start
|
||||
(`Date.now() - process.uptime()`) is recorded per iteration under
|
||||
`evidence.epochs` for cross-checks.
|
||||
|
||||
### Summary schema
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"generatedAt": "2026-09-26T11:02:14.318Z",
|
||||
"harness": {
|
||||
"platform": "darwin",
|
||||
"electron": "43.3.0",
|
||||
"measuredIterations": 5,
|
||||
"warmupIterations": 1
|
||||
},
|
||||
"journeys": {
|
||||
"launch": {
|
||||
"counters": { "renderer.ipcCallsToFirstCard": 12 },
|
||||
"counterStability": {
|
||||
"renderer.ipcCallsToFirstCard": {
|
||||
"stable": true,
|
||||
"values": [12, 12, 12, 12, 12]
|
||||
}
|
||||
},
|
||||
"wallClock": {
|
||||
"spawnToFirstCardMs.p50": 1234.5,
|
||||
"spawnToFirstCardMs.p90": 1300.1
|
||||
},
|
||||
"unavailable": { "renderer.cdTicksToFirstCard": "reason" },
|
||||
"iterations": [
|
||||
{
|
||||
"index": 0,
|
||||
"warmup": true,
|
||||
"pid": 1,
|
||||
"counters": {},
|
||||
"wallClock": {},
|
||||
"evidence": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`journeys.<id>.counters.<name>` and `journeys.<id>.wallClock.<name>` are plain
|
||||
numbers so `tools/performance/check-journey-ratchet.mjs` can compare them with
|
||||
`tools/performance/journey-baselines.json`. A J1 baseline is added once the
|
||||
numbers are stable on the CI runner; until then the summary is evidence only.
|
||||
|
||||
## `renderer.initialBytes`
|
||||
|
||||
@@ -65,17 +217,17 @@ counter:
|
||||
|
||||
```json
|
||||
{
|
||||
"journeys": {
|
||||
"launch": {
|
||||
"renderer.initialBytes": {
|
||||
"value": 2739510,
|
||||
"unit": "bytes",
|
||||
"updatedAt": "2026-09-26",
|
||||
"evidencePr": 1693,
|
||||
"measuredWith": "pnpm nx build web && pnpm run perf:initial-bytes"
|
||||
}
|
||||
}
|
||||
"journeys": {
|
||||
"launch": {
|
||||
"renderer.initialBytes": {
|
||||
"value": 2739510,
|
||||
"unit": "bytes",
|
||||
"updatedAt": "2026-09-26",
|
||||
"evidencePr": 1693,
|
||||
"measuredWith": "pnpm nx build web && pnpm run perf:initial-bytes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -144,3 +296,24 @@ trade-off, say so in the PR and let the maintainer decide.
|
||||
the test file in `tools/performance/project.json`.
|
||||
4. Validate the counter before it becomes a guardrail: one PR must show that
|
||||
lowering it moved wall-clock in the same journey.
|
||||
|
||||
## Adding a journey
|
||||
|
||||
1. Add `apps/electron-backend-e2e/src/journeys/<journey>.journey.ts`. Seed the
|
||||
profile through the app's dialogs, spawn a fresh process per iteration
|
||||
with `measureLaunchJourney` as the model, and drive the journey's start
|
||||
action with Playwright.
|
||||
2. Give the journey its own probe options (`cardSelector`, `routeFragment`,
|
||||
terminal condition) or extend `journey-renderer-probe.ts` when the end
|
||||
condition is not "an element became visible". Keep the probe
|
||||
self-contained: Playwright serializes it with `toString()`.
|
||||
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
|
||||
`unavailable` with the reason.
|
||||
4. Add the journey under `journeys.<id>` in the summary through
|
||||
`summarizeJourneyIterations`; the schema needs no change.
|
||||
5. Cover the probe with jsdom fixtures and the record and summary code with
|
||||
`node:test` (`pnpm nx run electron-backend-e2e:test-performance-harness`).
|
||||
6. Validate a counter before it becomes a guardrail: one PR must show that
|
||||
lowering it moved wall-clock in the same journey.
|
||||
@@ -436,8 +436,13 @@ Publishing the GitHub release is manual. That publication automatically
|
||||
verifies its Snap assets and uploads them to `edge`; installed-Snap smoke and
|
||||
candidate/stable promotion remain manual (see
|
||||
`tools/packaging/validate-snap-release-boundary.mjs`). Keep the blog post a
|
||||
draft during artifact verification, then publish it in a follow-up commit and
|
||||
verify the website deployment.
|
||||
draft during artifact verification. After the release is public and its assets
|
||||
are verified, publish the blog and advance
|
||||
`apps/website/released-version.json` to that published version in the same
|
||||
follow-up commit. Run `WEBSITE_SKIP_RELEASE_FETCH=1 pnpm nx test website --skip-nx-cache`,
|
||||
compare the generated download links with the public release assets, and
|
||||
verify the website deployment. The fallback pin must never follow the
|
||||
development/nightly version in the root `package.json`.
|
||||
|
||||
If a Store upload fails after publication, run `publish-snap.yaml` from
|
||||
`master` with its `tag` input set to the existing public stable tag, for example
|
||||
|
||||
@@ -135,6 +135,16 @@ After an E2E run, generate the semantic summary with:
|
||||
pnpm run coverage:e2e:summary
|
||||
```
|
||||
|
||||
CI runs the Electron suite as three Playwright shards per OS
|
||||
(`--shard=<n>/3`, split by spec file because the suite is sequential). Each
|
||||
shard uploads `playwright-report-electron-<os>-<n>`; the follow-up
|
||||
`Electron E2E summary` job downloads the shards of each OS into their own
|
||||
directory and runs the summary per OS with `--input=<directory>` and
|
||||
`--output-dir=coverage/e2e/<os>`. A directory input merges every
|
||||
`results.json` beneath it and fails when a shard is missing or duplicated, so
|
||||
the summary never reports a partial run as complete. Tests for that merge live
|
||||
in `tools/coverage/e2e-shard-reports.test.mjs` (`pnpm run coverage:tools:test`).
|
||||
|
||||
For local investigation only, Chromium browser V8 coverage can be explored with:
|
||||
|
||||
```bash
|
||||
@@ -160,6 +170,7 @@ pnpm nx build web
|
||||
pnpm run perf:initial-bytes # breakdown only
|
||||
pnpm run perf:initial-bytes:check # measure, then compare with the committed baseline
|
||||
pnpm nx test performance-tools
|
||||
pnpm run perf:journeys # J1 launch benchmark, writes dist/performance/journeys/<timestamp>/summary.json
|
||||
```
|
||||
|
||||
`perf:initial-bytes` reads the built `dist/apps/web/index.html` and sums the
|
||||
@@ -168,7 +179,10 @@ bytes on the initial path (the J1 counter `renderer.initialBytes`).
|
||||
`tools/performance/journey-baselines.json`; baselines only move down. CI runs
|
||||
the same check in the `Initial bytes ratchet` job of `ci.yml` for PRs that
|
||||
target `master` and for `master` pushes (dispatch it with
|
||||
`gh workflow run ci.yml --ref <branch>` for a stacked branch). The contract, what counts and how to add a counter are in the
|
||||
`gh workflow run ci.yml --ref <branch>` for a stacked branch). `perf:journeys` builds the `electron-performance` configuration and runs the
|
||||
J1 launch benchmark against the Xtream mock; its probe specs run with
|
||||
`pnpm nx run electron-backend-e2e:test-performance-harness`. The contract, what
|
||||
counts and how to add a counter or a journey are in the
|
||||
[performance journeys](performance-journeys.md) document.
|
||||
|
||||
## Logging
|
||||
|
||||
@@ -32,6 +32,7 @@ IPTVNATOR_TRACE_STARTUP=1 pnpm nx serve electron-backend
|
||||
- `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_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`
|
||||
|
||||
- Settings, portal request/response, and trace payloads must use
|
||||
`@iptvnator/shared/logging` or the redacting portal logger before reaching
|
||||
@@ -93,8 +94,27 @@ classifying a zero rendered-frame signal as an infrastructure flake.
|
||||
|
||||
## Main-process ownership
|
||||
|
||||
The entry point is `apps/electron-backend/src/main.ts`; it bootstraps the database,
|
||||
registers events and creates the main window. The preload is
|
||||
The process entry is `apps/electron-backend/src/main.entry.ts` (built to
|
||||
`dist/apps/electron-backend/main.js`): it enables the V8 compile cache under
|
||||
`userData/v8-compile-cache` and then requires the application bundle,
|
||||
`main.app.js`, built from `apps/electron-backend/src/main.ts`. Before the window
|
||||
loads, `main.ts` registers only what the renderer can call before its first paint
|
||||
(window state, the close guard, playlist-open requests, request-header shims);
|
||||
everything else, including the database, portal, EPG, download, player,
|
||||
remote-control and update IPC, lives in
|
||||
`apps/electron-backend/src/app/startup/deferred-events.ts`, built as the
|
||||
`deferred-events.js` chunk and loaded inside the window's `did-start-loading`
|
||||
listener. That import and its registrations finish within the same task, so no
|
||||
renderer `invoke` can find a missing handler (`app/startup/deferred-bootstrap.ts`
|
||||
holds the scheduler and its test); the startup trace reports it as
|
||||
`deferred-events:start` and `deferred-events:done`. The cache is
|
||||
disposable; `IPTVNATOR_DISABLE_COMPILE_CACHE=1` turns it off and
|
||||
`IPTVNATOR_COMPILE_CACHE_DIR` relocates it (E2E runs keep it inside
|
||||
`IPTVNATOR_E2E_DATA_DIR`). nx-electron packages the backend through an
|
||||
allowlist, so `apps/electron-backend/project.json` lists `main.app.js` and
|
||||
`deferred-events.js` under the `files` option of the `package` and `make`
|
||||
targets, and `verify:package-layout` fails when any of the entry files is missing
|
||||
from `app.asar`. The preload is
|
||||
`apps/electron-backend/src/app/api/main.preload.ts`, with handlers under
|
||||
`apps/electron-backend/src/app/events/`. The window follows the saved startup mode
|
||||
(normal/maximized/fullscreen); `--fullscreen` overrides a single launch. Use
|
||||
|
||||
@@ -14,7 +14,7 @@ are not prerequisites for reading repository contracts.
|
||||
| Bootstrap, project placement, dependencies, aliases and lint configuration; root Nx config and project-local project.json files | [Nx boundaries](../architecture/nx-workspace-boundaries.md), [security overrides](../architecture/dependency-security-overrides.md) | [Nx architecture](../../.codex/skills/iptvnator-nx-architecture/SKILL.md) |
|
||||
| Angular conventions; docs and skills maintenance | [Agent workflow](../development/agent-workflow.md) | Use the area's skill below |
|
||||
| Unit, E2E, lint and coverage; `tools/coverage` | [Validation map](../architecture/validation-map.md) | Use the area's validation section |
|
||||
| Performance journeys, counters and the CI ratchet; `tools/performance` | [Performance journeys](../architecture/performance-journeys.md) | Read the contract directly |
|
||||
| Performance journeys, counters, benchmark probes and the CI ratchet; `apps/electron-backend-e2e/src/journeys`, `apps/electron-backend-e2e/src/performance`, `tools/performance` | [Performance journeys](../architecture/performance-journeys.md) | Read the contract directly |
|
||||
| Electron entry/events/preload and CDP; `apps/electron-backend` | [Debugging and trace flags](../development/electron-debugging.md), [Electron security](../architecture/electron-security.md) | Use the available global electron skill for automation |
|
||||
| Releases, notes, screenshots, native assets, Linux manager metadata; `tools/release` | [Release pipeline](../architecture/release-pipeline.md), [note format](../../.changes/README.md) | [Release notes](../../.codex/skills/release-notes/SKILL.md), [release cut](../../.codex/skills/release-cut/SKILL.md) |
|
||||
|
||||
|
||||
Reference in new issue
Block a user