Merge master, dedupe the kept copy, and retire superseded switches

Master had moved ten commits ahead. Two conflicts, both resolved without
losing either side: the `XTREAM_PROBE_URL` handler this PR extracted into
`events/stream-probe.ts` stays extracted (master added performance capture
nearby but never touched that handler), and the mock-server scenario table
takes the union of master's `performance` row and this branch's `multisrc`
pair.

Two findings fixed alongside it.

Pinning the copy the route is already on makes discovery return that very
row, so prepending the current source listed one stream twice — a phantom
copy in the grouping and a chip that counted it.

And handing the "playing" badge back to the route row left an in-flight
switch valid, so a slow resolution could arrive afterwards and replace the
playback the user had just started with Play, Resume or Restart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5 committed 2026-07-28 08:44:49 +02:00
commit 9f0a8b4f19
385 files changed
+58306 -2264

No files matched your search

+15
View File
@@ -271,6 +271,10 @@ regular development experiment flag) and one process-wide capability decision
has succeeded. On Linux x64 that decision validates the profile manifest,
regular-file/access modes, the complete declared bundled closure and hashes,
then runs `iptvnator_mpv_helper --runtime-probe` with a three-second timeout.
That short budget belongs to the application gate alone, because it blocks the
main process and a timeout there degrades to the native-view fallback;
`tools/packaging/verify-linux-frame-copy-runtime.mjs` runs the same probe under
a deliberately larger bound described under "Same-Version Desktop Release Gate".
The probe loads dependencies through the normal ELF loader, initializes an
idle libmpv client, creates EGL/OpenGL plus mpv render contexts, then
creates, maps, validates, and destroys a minimal `16x16` shared-memory ring
@@ -951,6 +955,17 @@ packaging passes with `IPTVNATOR_EMBEDDED_MPV_PLATFORM=linux`,
`IPTVNATOR_REQUIRE_EMBEDDED_MPV=1`, and one exact
`IPTVNATOR_LINUX_FRAME_COPY_PROFILE`. Each produced artifact is extracted and
verified, and the x64 helper probe runs in the intended runtime environment.
That extracted-artifact probe uses its own 15-second bound rather than the
application gate's three seconds: nothing waits on it but the CI job, which
already has a job-level timeout, while a premature kill would report a healthy
package as broken. Cold sandboxes — Flatpak most of all — can spend seconds
merely loading libmpv plus EGL/GL/GBM. A hard timeout is also the only probe
outcome that says nothing about the payload, so the verifier repeats it once
(two attempts in total, announced on stderr) before failing. Every other
outcome — spawn error, termination by signal, nonzero exit, or a malformed
protocol line — remains fail-closed on the first attempt, so a wrapper launched
instead of the real ELF, a missing helper, or a hung helper still fails
verification.
The packaged x64 Playwright smoke first runs its fixture-contract target and
passes Chromium `--ignore-gpu-blocklist` so Mesa llvmpipe can expose WebGL2 in
CI. That launch-only flag does not bypass any manifest, hash, loader, or helper
+89
View File
@@ -56,6 +56,95 @@ There is intentionally **no URL validation** (upstream removed it in 0.15.0): an
The behavioral contract is guarded by `apps/web/src/app/iptv-playlist-parser.contract.spec.ts` (jest maps the module to the real parser source) and by the fork's own test suite.
## Initial URL Import Performance Benchmark (Electron)
The Electron E2E project has a deterministic initial-import benchmark for
10,000-, 50,000-, and 100,000-channel M3U playlists. It uses only generated
fixtures served by an ephemeral `127.0.0.1` HTTP server; provider URLs,
credentials, and playlist data must never be used. Each formal scenario runs
one warm-up, five measured iterations, and one diagnostic iteration in a fresh
Electron process and data directory:
```bash
perf_output="$PWD/dist/performance/$(date -u +%Y%m%dT%H%M%SZ)-m3u-import"
IPTVNATOR_PERF_OUTPUT_DIR="$perf_output" \
IPTVNATOR_PERF_VARIANT=baseline \
pnpm nx run electron-backend-e2e:benchmark-m3u-import
```
Use a new output directory and `IPTVNATOR_PERF_VARIANT=after` for the identical
post-change run. Formal runs require a clean worktree and record the commit,
source-state hash, runtime, and exact fixture identity. A development smoke run
uses one warm-up, one measured iteration, and one diagnostic iteration per
size:
```bash
IPTVNATOR_PERF_OUTPUT_DIR="$PWD/dist/performance/<timestamp>-m3u-import-smoke" \
IPTVNATOR_PERF_VARIANT=smoke \
IPTVNATOR_PERF_SMOKE=1 \
pnpm nx run electron-backend-e2e:benchmark-m3u-import
```
Smoke runs may use a dirty worktree and validate only the harness; they cannot
support a performance claim. If another local application owns port 9222, smoke
only may set `IPTVNATOR_PERF_CDP_PORT` to an unused loopback port; formal runs
fail closed unless CDP uses `127.0.0.1:9222`. Raw captures and JSON results stay
under the gitignored `dist/performance/` tree; preflight rejects a symbolic
link in any existing output-path component before creating artifacts. Headline
distributions contain only the five measured runs: warm-up and diagnostic
profiles are never mixed into them.
The benchmark attributes these non-additive intervals:
| Field | Boundary |
| --- | --- |
| `dataAcquireMs` | loopback response acquisition |
| `m3uParsingMs` | parser call |
| `normalizationMs` | parsed-item normalization |
| `mainToRendererCloneProxyMs` | sum of normalized import-result delivery plus the upsert and GET main-response-to-preload-success legs |
| `storeImportDispatchMs` | renderer import dispatch |
| `rendererToMainCloneProxyMs` | sum of the upsert and GET preload-source-to-main-request legs |
| `mainToDatabaseWorkerCloneProxyMs` | sum of the upsert and GET main-request-to-worker-receive legs |
| `playlistSerializationMs` | database-worker playlist JSON serialization |
| `sqliteWriteMs` | SQLite upsert, including its autocommit |
| `sqliteReadMs` | SQLite read of the newly persisted playlist |
| `playlistDeserializationMs` | database-worker `parseAppPlaylist`, including playlist JSON parsing |
| `databaseWorkerToMainCloneProxyMs` | sum of the upsert and GET worker-response-post-to-main-response legs |
| `storePublishChannelsMs` | renderer channel publication |
| `angularRenderingMs` | publication end to the terminal two-frame paint proof |
`ipcStructuredCloneProxyMs` is the explicit sum of the four directional proxy
fields. Each directional field can combine the applicable initial-result,
upsert, and `DB_GET_APP_PLAYLIST` legs. The worker stamps
`responsePostedEpochMs` after request profiling is finalized and immediately
before each response is posted; the database-worker-to-main proxy ends when
main receives that response. These fields are attribution aids, not an additive
waterfall: scheduler work and gaps can remain inside total wall time, and a
proxy can include response construction, dispatch overhead, and structured
clone work. Initial import does not create indexes; `indexAndCommitMs` is
therefore `N/A` with
`indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write`.
Instrumentation is development-only, opt-in, fail-neutral, and count-only. It
must not scan or log playlist payloads to generate metadata. Renderer
long-task, frame-gap, and heartbeat samples are clipped to the measured
operation boundary. Main capture stops only after the upsert and route-reload
GET responses plus both asynchronous preload success markers have arrived, so
return-clone attribution cannot race capture shutdown. Formal comparison fails
closed after writing raw results if exact-window renderer RSS, database-worker
peak heap/external samples, or the database worker's explicit post-GC heap is
missing or incoherent. Both initial-import database requests in every measured
run must also contain coherent event-loop delay, event-loop utilization, and
thread-CPU metrics; the validity record exposes the exact expected and valid
request counts rather than silently dropping nullable samples. An iteration
failure idempotently stops renderer timers, closes trace listeners/output, and
starts best-effort probe/session teardown without waiting on a wedged renderer
before Electron is closed. A partial capture-start failure performs the same
rollback before it escapes to the benchmark lifecycle.
Diagnostic artifacts require separate renderer, main, and database-worker CPU
profiles plus renderer/main/database-worker heap snapshots and a Chromium
trace; raw profiles remain ignored.
## Playlist Refresh And Startup Auto-Update (Electron)
Two paths re-download an M3U playlist from its original source:
+103 -1
View File
@@ -128,6 +128,8 @@ Each enabled request gets a fresh event-loop-delay histogram and records:
- `requestReceivedEpochMs`, `workStartedEpochMs`, `workEndedEpochMs`, and
`histogramFlushedEpochMs`
- `responsePostedEpochMs`, sampled after profiling finalization and immediately
before the worker posts the response to main
- worker-thread CPU user/system microseconds from `process.threadCpuUsage()`
- event-loop utilization across the exact work interval
- event-loop-delay max/p95/p99 from the request's own histogram
@@ -146,6 +148,94 @@ profiling queue. If captures overlap, every overlapping response carries
`invalidReason: "overlapping-database-worker-requests"` and all attributable
CPU, ELU, and event-loop-delay values are `null`. This avoids assigning shared
worker activity to one request while preserving normal worker concurrency.
`responsePostedEpochMs` remains a separate response boundary: the initial M3U
benchmark subtracts it from main's response receipt to attribute the
database-worker-to-main structured-clone proxy without folding that interval
into worker execution.
Initial M3U import profiling requires exact operation-specific phase pairs:
- `DB_UPSERT_APP_PLAYLIST`: `serialize.playlist`, then `sqlite.write`. The
first covers playlist JSON serialization; the second covers the SQLite
upsert and its autocommit.
- `DB_GET_APP_PLAYLIST`: `sqlite.read`, then `deserialize.playlist`. The first
covers the awaited single-row SQLite select; the second covers
`parseAppPlaylist`, including JSON parsing and persisted-field hydration.
Missing, partial, reordered, or cross-operation phase sequences fail closed.
Markers carry item counts only and do not scan or copy the payload to compute
profiling metadata. The import creates no indexes, so there is no separate
index/transaction-commit phase. Across the upsert and GET requests, the formal
benchmark reports renderer-to-main, main-to-database-worker,
database-worker-to-main, and main-to-renderer structured-clone proxies; their
explicit sum is `ipcStructuredCloneProxyMs`. See
[M3U Playlist Module Architecture](./m3u-playlist-module.md#initial-url-import-performance-benchmark-electron)
for the complete cross-process attribution.
The same request envelope exposes count-only Xtream database phases for these
operations:
- `DB_GET_CATEGORIES`: `sqlite.categories.read`
- `DB_SAVE_CATEGORIES`: `normalize.categories`, then
`sqlite.categories.write-transactions`
- `DB_GET_CONTENT`: `sqlite.content.read`
- `DB_SAVE_CONTENT`: `sqlite.content.category-map-read`, then
`normalize.content`, then `sqlite.content.write-transactions`
- `DB_CLEAR_XTREAM_IMPORT_CACHE`:
`sqlite.xtream-cache-clear.write-transactions`
- `DB_SEARCH_CONTENT`: `sqlite.search.query`, then `normalize.search-rank`
- `DB_DELETE_XTREAM_CONTENT`:
`sqlite.xtream-delete.collect-user-data`, then
`sqlite.xtream-delete.write-transactions`
- `DB_DELETE_PLAYLIST`: `sqlite.playlist-delete.collect-ids`, then
`sqlite.playlist-delete.write-transactions`
Read and query phases cover the exact awaited Drizzle query. Normalization
phases cover the existing synchronous transforms. The content-write phase is
one aggregate pair around every existing 100-row transaction, cancellation
checkpoint, and progress callback; it is not an exact measurement of SQLite
commit time. Cache clear similarly uses one aggregate pair around all existing
content and category chunk loops and transactions, including their JavaScript
and autocommit overhead. A zero-category cache clear still emits one pair with
`itemCount: 0` and performs no extra SQL.
The Xtream-delete collection span includes its existing ordered category,
favorite, recently-viewed, and content-ID work. Its `itemCount` deliberately
counts only content and category deletion candidates; favorite,
recently-viewed, and hidden-category user data is timed but is not added to
that count. The matching write count uses the same deletion-candidate
definition. Playlist-delete collection counts every collected favorite,
recently-viewed, playback-position, download, content, and category ID. Its
write count adds the final playlist row. Both write spans include every
existing cooperative checkpoint, 100-row transaction, progress callback, and,
for playlist deletion, the final playlist-row autocommit; they are not exact
SQLite commit-time measurements.
Successful end markers carry only row/item counts. Error or cancellation still
closes the active phase without metadata and preserves the original error.
Disabled profiling passes no adapter into the operations, so it performs no
phase-event or metadata-callback allocation. Worker concurrency, SQL, chunk
sizes, transaction boundaries, progress ordering, and cancellation checkpoints
are unchanged. `DB_GLOBAL_SEARCH` is deliberately not instrumented: it
interleaves queries and ranking across sources, so these single-query phases
would be misleading.
Formal initial-import comparison also requires both request-scoped captures in
every measured run to have coherent event-loop delay, event-loop utilization,
and worker-thread CPU values with no unavailable or invalid reason. Summary
validity records the exact expected and valid request counts; nullable metrics
remain in raw results but cannot be silently omitted from comparison
distributions.
The main-process benchmark samples the database worker's V8
`used_heap_size` and `external_memory` independently. Raw output includes a
valid-sample count for each metric. A peak is numeric only after at least one
finite, non-negative isolate sample; otherwise it is `null` with a fixed
unavailability reason. An initialized zero is never used as evidence of a
successful sample. Formal initial-import comparisons require valid peak
samples from exactly one database worker in every measured run. Worker RSS is
not available per thread and remains part of the separately reported Electron
main-process RSS together with native and SQLite memory.
### Opt-in post-GC heap capture
@@ -224,6 +314,15 @@ If a worker operation is canceled:
Cancellation is cooperative and chunk-based. Already committed SQLite batches
stay committed.
With the exact `IPTVNATOR_PERF_WORKER_PROFILING=1` opt-in, receipt of a cancel
for a correlated active request also emits a
`performance-cancel-received` worker diagnostic containing only safe
operation/request IDs and its epoch. The functional cancellation flag is set
first, this receipt remains distinct from the later authoritative
`cancelled` event, and `DatabaseWorkerClient` ignores it without settling or
exposing the pending request. Disabled profiling performs no receipt clock or
transport work, and `DatabaseWorkerClient.cancel()` remains fire-and-return.
## Renderer Contract
The preload bridge keeps the existing database methods but adds scoped worker
@@ -681,7 +780,10 @@ For deterministic E2E timing, tests may set:
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS=20
```
This delay is test-only and disabled by default.
This artificial delay is test-only and disabled by default. At the default
value of `0`, every cancellable batch checkpoint still yields one event-loop
turn without adding a timer delay. That yield lets the worker receive a queued
cancel message before starting the next batch.
### Useful verification commands
+7 -2
View File
@@ -248,7 +248,10 @@ for "Up".
One row inside the excluded playlist is kept when the caller names it
(`keepContentId`): a pin can point at another copy of the film in the playlist
being viewed, and dropping that row would leave the preference pointing at
nothing. The host therefore reads the pin BEFORE discovery.
nothing. The host therefore reads the pin BEFORE discovery. When that pin is
the route's *own* row, the kept copy and `currentSourceRow()` are the same
stream, so `applyDiscoveredSources` drops the duplicate rather than listing it
twice.
### Same title, different film
@@ -384,7 +387,9 @@ play.
Whichever source ends up playing, the "playing" badge follows it: starting the
route's own stream (Play, Resume, Restart, or the fallback after a pin does not
apply) hands the badge back to the route row, or the picker and caption go on
naming an alternative that is no longer running. And a source started through
naming an alternative that is no longer running. That hand-back also
invalidates a switch still resolving — the user chose the route stream, and an
older resolution arriving afterwards would replace what they just asked for. And a source started through
the picker or a pin is recorded in Recently Viewed exactly as an ordinary Play
is — it is the same film, watched.
+252 -19
View File
@@ -6,7 +6,9 @@
Xtream Codes API protocol. It is used for:
- **Local development** — run a full portal without a real Xtream subscription
- **E2E testing** — Playwright spins it up alongside the Angular dev server
- **E2E testing** — Playwright uses it for both web and Electron workflows
- **Performance investigation** — a locked-down loopback control plane
prepares and identifies a fixed synthetic 100k catalog before capture
---
@@ -20,6 +22,9 @@ credentialsToSeed(u, p) ←── deterministic polynomial hash
│
▼
faker.seed(seed) ←── all faker calls use same seed per credentials
│
├── performance:performance?
│ └── index-derived generator (no Faker/runtime clock)
│
▼
generateCategories() ←── live / vod / series categories
@@ -43,19 +48,28 @@ Re-requesting with the same credentials returns the exact same data until
```
apps/xtream-mock-server/
├── project.json ← Nx targets: serve (port 3211), serve-with-watch
├── project.json ← Nx serve, watch, lint, and test targets
├── public/
│ └── marketing/ ← committed fictional release artwork PNGs
├── tsconfig.json
└── src/
├── main.ts ← Express app bootstrap, all routes wired up
├── main.ts ← env validation, HTTP lifecycle, safe logging
└── app/
├── server.ts ← side-effect-free Express app factory/routes
├── server-lifecycle.ts ← bounded, idempotent HTTP shutdown
├── scenarios.ts ← Credential → ScenarioConfig mapping
├── data-store.ts ← Lazy cache, per-credentials generation
├── performance-control.ts ← bounded request lifecycle controller
├── performance-interception-lifecycle.ts ← reset/shutdown generation
├── performance-control-routes.ts ← token-gated control HTTP API
├── performance-control-validation.ts ← strict request validation
├── performance-control.types.ts ← safe manifest/state contracts
├── performance-manifest.ts ← fixed-order catalog hash/counts
├── generators/
│ ├── categories.generator.ts
│ ├── live.generator.ts ← Live streams + EPG listings
│ ├── marketing.generator.ts ← Fictional release screenshot fixture
│ ├── performance.generator.ts ← local-only deterministic 100k fixture
│ ├── vod.generator.ts ← VOD streams + VodDetails
│ └── series.generator.ts ← Series items + SeriesInfo
├── handlers/
@@ -95,6 +109,134 @@ Response: `{ payload: <data>, action: <action> }`
This mirrors the backend proxy in `apps/electron-backend` so the same
Angular service code works in both environments.
### Performance control endpoint
The control plane is absent by default. It is mounted only when
`IPTVNATOR_XTREAM_MOCK_CONTROL=1`; startup then requires a non-empty
`IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN` and a literal loopback `HOST`
(`127.0.0.1` or `::1`). Every `/__control/*` request must carry that exact value
in `x-iptvnator-performance-token`, including `OPTIONS` preflight requests.
Configuration is validated before the HTTP listener opens. Normal development
mode preserves the legacy wildcard bind when `HOST` is unset; control mode
instead defaults to `127.0.0.1` and rejects an explicitly configured
non-loopback host. The Nx serve targets do not pin `PORT`, so an explicit shell
value reaches the parser; its no-value default remains `3211`.
Use a dedicated port rather than the normal `3211` E2E server:
```bash
HOST=127.0.0.1 \
PORT=3221 \
IPTVNATOR_XTREAM_MOCK_CONTROL=1 \
IPTVNATOR_XTREAM_MOCK_CONTROL_TOKEN=local-benchmark-token \
pnpm nx run xtream-mock-server:serve
```
The strict control API is:
```text
POST /__control/prepare
POST /__control/reset
POST /__control/barriers
POST /__control/barriers/:id/release
POST /__control/delays
GET /__control/state
```
`prepare` accepts exactly `{"scenario":"performance-100k"}` and materializes
the fixed `performance:performance` fixture before application capture. It
returns only this manifest:
```json
{
"epoch": 1,
"scenario": "performance-100k",
"seed": 91001,
"counts": {
"categories": { "live": 60, "vod": 20, "series": 20, "total": 100 },
"items": {
"live": 60000,
"vod": 20000,
"series": 20000,
"total": 100000
}
},
"bytes": 123,
"catalogSha256": "64-lowercase-hex-characters"
}
```
`bytes` is the UTF-8 byte length and `catalogSha256` is the SHA-256 of one JSON
object whose property order is fixed as:
1. live categories
2. VOD categories
3. series categories
4. live catalog
5. VOD catalog
6. series catalog
Credentials, server origins, response envelopes, and EPG/detail caches are not
part of the hash input.
`reset` accepts exactly `{"mode":"observations"}` or `{"mode":"all"}`.
Observation reset clears active/held rules, per-identity occurrences, and the
ledger while retaining the prepared manifest and epoch. All-state reset also
calls the data-store reset, clears the manifest, and increments the epoch.
Either mode first advances an internal observation generation and detaches all
pre-reset response listeners, so a late `finish` or `close` cannot repopulate
the freshly cleared ledger. Held barrier/delay clients retain the safe `409`
reset response where possible; unmatched active responses are aborted.
Barrier and delay rules have an ID plus the exact match tuple:
```text
(epoch, scenario, transport, canonicalAction, categoryId, occurrence)
```
`transport` is `direct` or `proxy`. Empty actions canonicalize to
`get_account_info`; the dispatcher's legacy `get_simple_date_table` alias is
allowlisted. Occurrences are counted per tuple excluding occurrence, so
parallel requests for different categories cannot consume each other's rule.
Numeric category IDs must use canonical decimal spelling. Signs, whitespace,
leading zeroes, non-decimal notation, unsafe integers, and values outside the
closed scenario ranges are rejected; invalid incoming aliases collapse to the
single `all` observation identity and cannot expand the bounded state map.
Rules are one-shot and match before dispatch or JSON serialization. Barrier
lifecycle is `arrived → blocked → responded|aborted`; delay lifecycle is
`arrived → delayed → responded|aborted`. Real request abort events release held
state; a normally consumed request's `close` event is not treated as an abort.
The state response contains only epoch, the safe manifest, active rules, held
IDs/count, bounded occurrence counts, and bounded lifecycle entries with
monotonic timestamps. It never stores or returns the token, username, password,
raw URL/query, request/response payload, titles, or catalog arrays. Unknown
incoming actions are represented only as `unknown`.
Control JSON is limited to 16 KiB. Bodies reject unknown/missing/wrong-type
fields. Each observation epoch accepts at most 32 rules, and the serialized
ledger retains at most 128 entries. Occurrence state has 512 slots: enough for
the complete closed scenario/action/category/transport identity domain without
eviction or counter restart; an unexpected overflow fails closed. IDs,
scenarios, transports, actions, categories, occurrences, and delays
(`0..5000` ms) use closed validation. Duplicate IDs/matches and already-past
occurrences fail closed.
The legacy unauthenticated `POST /reset` remains available in normal mode. It
returns `410` while the control plane is enabled, preventing cache invalidation
that would leave the controller's prepared manifest stale. Performance runs
must use the token-authenticated `/__control/reset`.
Barriers and delays exist for deterministic coordination and smoke tests only.
Formal performance captures must prove that both rule sets are empty and must
never add an artificial delay to the timed application path.
`SIGINT` and `SIGTERM` use one bounded, idempotent shutdown path. The
application hook invalidates active observations, settles barriers and delays,
and clears their timers/listeners before the HTTP listener closes. The shared
HTTP helper then closes idle and active connections, so an unreleased control
rule cannot keep the mock process alive.
### M3U fixture endpoint
```
@@ -113,8 +255,12 @@ GET /movie/<username>/<password>/<streamId>.<ext>
GET /series/<username>/<password>/<streamId>.<ext>
```
All redirect to a publicly available HLS test stream
(`https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8`).
In normal mode all redirect to a publicly available HLS test stream
(`https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8`). With the control plane
enabled, every stream/timeshift route for the performance credentials returns
`410` without a redirect or outbound request. `/playlist.m3u` is also `410` in
that mode. This keeps formal performance fixtures local-only; normal
control-disabled E2E behavior is unchanged.
---
@@ -242,19 +388,20 @@ sometimes only respond to that misspelled action.
## Scenarios
| Key (`username:password`) | Seed | Categories | Items/cat | Account status |
| ------------------------- | ---- | ------------------------ | --------- | -------------- |
| `user1:pass1` | 1001 | 8 each | 40 | active |
| `large:large` | 9999 | 20 each | 200 | active |
| `stress:stress` | 7777 | 16 each | 120 | active |
| `series:series` | 2002 | live:3, vod:4, series:15 | 30 | active |
| `minimal:minimal` | 3003 | 2 each | 5 | active |
| `epg:epg` | 6006 | live:2, vod:1, series:1 | 3 | active |
| `emptyvod:emptyvod` | 7007 | 2 each | 5 | active |
| `marketing:marketing` | 8020 | live:4, vod:4, series:4 | curated | active |
| `expired:expired` | 4004 | 4 each | 10 | Expired |
| `inactive:inactive` | 5005 | 4 each | 10 | Disabled |
| `<any other>` | hash | 6 each | 30 | active |
| Key (`username:password`) | Seed | Categories | Items/cat | Account status |
| ------------------------- | ----- | -------------------------- | --------- | -------------- |
| `user1:pass1` | 1001 | 8 each | 40 | active |
| `large:large` | 9999 | 20 each | 200 | active |
| `stress:stress` | 7777 | 16 each | 120 | active |
| `performance:performance` | 91001 | live:60, vod:20, series:20 | 1,000 | active |
| `series:series` | 2002 | live:3, vod:4, series:15 | 30 | active |
| `minimal:minimal` | 3003 | 2 each | 5 | active |
| `epg:epg` | 6006 | live:2, vod:1, series:1 | 3 | active |
| `emptyvod:emptyvod` | 7007 | 2 each | 5 | active |
| `marketing:marketing` | 8020 | live:4, vod:4, series:4 | curated | active |
| `expired:expired` | 4004 | 4 each | 10 | Expired |
| `inactive:inactive` | 5005 | 4 each | 10 | Disabled |
| `<any other>` | hash | 6 each | 30 | active |
### `epg:epg` fixture details
@@ -296,6 +443,91 @@ This scenario is reserved for release screenshots and marketing materials:
- the SVG renderer in `marketing.generator.ts` remains the fallback for missing
assets, live logos, season covers, and episode thumbnails
### `performance:performance` fixture details
This scenario is reserved for local performance captures:
- exactly 60 live categories/60,000 channels, 20 VOD categories/20,000 movies,
and 20 series categories/20,000 series
- exactly 1,000 catalog items in every category
- fixed IDs, timestamps, ratings, names, and one lazy season/episode generated
from indexes rather than Faker, `Date.now()`, or `Math.random()`
- empty/local-only artwork and direct-source fields
- VOD and series detail records are materialized only on request
- preparation and manifest hashing happen before the timed application request
### Electron Xtream performance benchmark
Run a formal benchmark from a clean worktree with the standard CDP endpoint
available at `127.0.0.1:9222`:
```bash
perf_output="$PWD/dist/performance/$(date -u +%Y%m%dT%H%M%SZ)-xtream"
IPTVNATOR_PERF_OUTPUT_DIR="$perf_output" \
IPTVNATOR_PERF_VARIANT=baseline \
NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false \
pnpm nx run electron-backend-e2e:benchmark-xtream --skip-nx-cache
```
The target starts an isolated control-enabled mock server on
`127.0.0.1:3221` and uses only the synthetic `performance:performance`
credentials and fixture. It covers initial import, refresh, deletion,
cancellation during import, and navigation/search/UI interaction during a
background operation. Every scenario has one warm-up, five measured
iterations, and one diagnostic iteration. Only the five measured iterations
of a clean formal run can have `validForComparison: true`.
Capture setup and operation measurement are separate boundaries. The harness
first installs and arms the main, renderer, and worker capture infrastructure;
it then starts the main measurement and renderer operation window immediately
before triggering the user operation. Profiler attachment, fixture preparation,
and other setup outside that operation window are not application work. Because
`database.worker` is persistent but created lazily, the harness awaits the
read-only `window.electron.dbGetAppPlaylists()` preload call immediately after
installing main capture. Its result is discarded: this pre-arm only guarantees
that the exact worker can be profiled and happens before seed or measured
capture.
Diagnostic profiles are evidence envelopes, not comparison totals. Starting
Chromium tracing and the CPU profilers can add a short pre-trigger setup segment
that contains no application workload; the raw boundary timestamps preserve
that segment explicitly. Formal summaries compare only the five measured runs,
never warm-up or diagnostic totals.
CPU scopes in the summary are intentionally not interchangeable. Electron main
CPU uses `process.threadCpuUsage()` and is labeled
`electron-main-thread`. Database-worker CPU is the sum of thread CPU from valid,
non-overlapping request work and is labeled
`sum-valid-request-thread-cpu`; it is neither process-wide CPU nor a whole-worker
sample.
Scenarios that need an existing portal seed it outside the measured iteration.
Seed completion is authoritative only after a successful
`store.xtream-import-terminal` renderer phase marker, the matching terminal DOM
gate, and two consecutive painted `requestAnimationFrame` frames. The harness
then verifies main-process settlement before rolling over to the measured
capture; merely seeing the catalog or loading overlay disappear is not a seed
terminal.
For an end-to-end wiring check, run smoke mode with one comparison-ineligible
measured iteration per scenario. An unused loopback CDP port may be selected
when the formal port is occupied:
```bash
perf_output="$PWD/dist/performance/$(date -u +%Y%m%dT%H%M%SZ)-xtream-smoke"
IPTVNATOR_PERF_OUTPUT_DIR="$perf_output" \
IPTVNATOR_PERF_VARIANT=smoke \
IPTVNATOR_PERF_SMOKE=1 \
IPTVNATOR_PERF_CDP_PORT=9322 \
NX_TASKS_RUNNER_DYNAMIC_OUTPUT=false \
pnpm nx run electron-backend-e2e:benchmark-xtream --skip-nx-cache
```
The manifest, summary, per-iteration JSON, traces, CPU profiles, and heap
snapshots stay under the git-ignored `dist/performance/` tree. Never commit
those artifacts, and never substitute real credentials, portal URLs, or
provider data.
---
## Playwright Integration
@@ -337,4 +569,5 @@ await page.route('**/localhost:3000/xtream**', async (route) => {
`OPENAI_API_KEY=... pnpm release:artwork:generate`, inspect the generated PNGs,
and finish with `pnpm release:artwork:validate`
- **Adjust data volume**: Change `itemsPerCategory`, `seasonsPerSeries`, or `episodesPerSeason` per scenario
- **Custom stream URLs**: Edit the HLS stub redirect in `main.ts`
- **Custom stream URLs**: Edit the normal-mode HLS stub redirect in
`app/server.ts`; never enable it for the performance fixture
File diff suppressed because it is too large. Load diff