chore(stalker): merge master compatibility updates

This commit is contained in:
4gray committed 2026-07-27 20:21:56 +02:00
commit 9163f2d515
317 files changed
+38962 -5017

No files matched your search

+13 -2
View File
@@ -69,9 +69,20 @@ node tools/release/build-release-notes.mjs --consume
```
The release version comes from the root `package.json` — bump it first, then
generate. `--version 0.24.0` overrides it for a dry run before the bump.
generate. `--version 0.24.0` overrides it to preview a release before the bump:
Only `--consume` deletes anything; every other mode is a safe dry run.
```bash
pnpm run release:notes:github --version 0.24.0
```
A bare `--` separator is accepted and ignored, so the npm habit of
`pnpm run release:notes:github -- --version 0.24.0` works too: pnpm forwards
that separator to the script rather than consuming it the way npm does.
`--validate` and `--format github` only read and print. `--format changelog`
and `--format blog` write their target file (rerunning `changelog` for the same
version replaces that section rather than duplicating it). Only `--consume`
deletes anything.
The release sequence is: bump the version → `release:notes:changelog` →
`release:notes:blog` → `--consume` → commit → tag → push. The tag build then
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: backup
issues: [1017]
---
Backups carry hidden Xtream categories correctly. An export used to lose which
categories you had hidden, and restoring such a backup then hid every category
of that kind.
+9
View File
@@ -0,0 +1,9 @@
---
type: internal
area: deps
---
Updated 43 dependencies, including the HTTP client behind every playlist and
portal request — that one closes seven advisories that affect the shipped app,
among them a proxy-credential leak on redirects. Also refreshes the ArtPlayer,
hls.js, Video.js and Shaka player engines. No behaviour change intended.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: downloads
---
Downloads can be paused and picked up later. A paused transfer keeps what it
already fetched and continues from that point instead of starting over, and
downloads cut short by a crash or a closed app come back as paused rather than
lost.
+7
View File
@@ -0,0 +1,7 @@
---
type: fix
area: electron
---
The desktop remote-control server now blocks crafted static paths from escaping
bundled web files on Windows.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: embedded-mpv
---
Experimental Embedded MPV can draw video inside the app window instead of into a
separate layer pinned on top of it, so menus, dialogs and the player controls
stop being swallowed by the picture. Available on macOS (Apple Silicon), Windows
and Linux x64; switching it on needs a restart.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: epg
---
Channels whose guide never matched can be mapped by hand: right-click a channel
in any list and pick "Map EPG channel" to attach it to a channel from your
uploaded XMLTV guide. The mapping is remembered and used everywhere the guide is
read — M3U playlists, Xtream and Stalker portals alike.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: epg
---
A database error while reading or saving a manual EPG channel mapping no longer
surfaces as a failed request. Looking up, saving, deleting, and searching
mappings now fall back quietly, so a transient storage hiccup can no longer take
down the EPG panel or the "Map EPG channel" dialog.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: favorites
issues: [1137]
---
Favorites stop losing changes. The custom drag-and-drop order of an Xtream
playlist's own favorites is saved again, and two favorites or history entries
added at almost the same moment no longer quietly overwrite each other.
+7
View File
@@ -0,0 +1,7 @@
---
type: feature
area: i18n
issues: [1192]
---
IPTVnator speaks Hungarian, its 19th language — contributed by @htibcsike.
+10
View File
@@ -0,0 +1,10 @@
---
type: feature
area: m3u
issues: [86, 614, 656, 733, 752]
---
MPEG-DASH channels play in the built-in player, ClearKey-encrypted ones
included — the keys are read from the playlist's #KODIPROP lines, whether they
sit above or below the channel entry. Streams locked with Widevine or PlayReady
still cannot be played, but they now say so instead of failing silently.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: m3u
issues: [1189]
---
Playlists with very long stream URLs — Pluto TV style lists that carry a session
token in every link — import in full again instead of collapsing into a single
channel.
@@ -0,0 +1,9 @@
---
type: feature
area: playback
---
An optional new set of player controls that looks and behaves the same in the
HTML5, Video.js and ArtPlayer players, with picture-in-picture and, in
fullscreen, the name of what you are watching. Enable it in Settings → Playback;
left off, each of the three keeps its own controls exactly as before.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: playback
---
The inline player on movie and series pages fills the whole content area like a
theater: the video sits centered in black instead of leaving a strip of app
background beside it. In the built-in web players, an optional ambient mode
fills that space with a blurred, dimmed copy of the poster.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: playback
---
A series playing inline on a wide window shows an "Up Next" rail beside the
video: the rest of the season and the start of the next one, the current episode
highlighted, watch progress on every card. Click one to jump straight to it.
Built-in web players only; switch the rail off in Settings → Playback.
+10
View File
@@ -0,0 +1,10 @@
---
type: fix
area: playlists
issues: [931]
---
One unreachable playlist no longer holds up the rest. Refreshes give up after 30
seconds and run a few at a time, so your other playlists still update, and the
message on startup names how many actually failed instead of always claiming
success.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: search
issues: [1161]
---
Searching for names that carry punctuation inside them — "A&E", "X-Men",
"L'Equipe" — finds them anywhere in a title, including channels the provider
prefixes, like "US: A&E".
@@ -0,0 +1,9 @@
---
type: feature
area: settings
---
A new setting drops the country prefix from live channel names, turning
"UK - BBC One" into "BBC One" in lists, the guide and the player. Movie and
series titles are left alone, and names that only look like a prefix — "Sky -
Sports F1" — stay intact.
+9
View File
@@ -0,0 +1,9 @@
---
type: perf
area: stalker
---
Detail pages for anything that is not a series — a movie, a live channel, a VOD
item that only looks like a series — no longer fire an episode-list request
before they can show anything, so they open faster on every route in, from
browsing and search to Favorites, Recent and the dashboard.
@@ -0,0 +1,9 @@
---
type: fix
area: stalker
---
Series opened from Favorites, Recent or the dashboard show their current
episodes. One saved back when a single episode existed used to keep showing that
one episode forever; the list is refreshed from the portal in the background
instead.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: stalker
---
The Live TV section loads its full channel list up front: search covers every
channel instead of only the page you are on, genres show how many channels they
hold, and Live TV opens on a grid of all channels. Guide data for the visible
rows loads in bulk, so it shows up without playing a channel first.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: tmdb
---
Series pages show whether a show has ended or is still returning, so you know
before committing to it. Directors and creators became clickable avatar chips
like the cast — they open the person's page, where directing credits now sit
alongside acting ones.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: tmdb
---
Titles that providers dress up with language or quality tags — "|ALB| Fallout",
"4K-DE - The Pitt (2025)", "Breaking Bad-eng" — now match against TMDB, so they
get artwork, plot and cast like the rest of the catalog. Shows split into one
entry per season also pull the season that entry really contains.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: xtream
issues: [1138]
---
Catch-up is available from Favorites and Recent, not just Live TV, so an
archived programme is reachable wherever the channel is. The programme currently
on air can also be restarted from the beginning.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: xtream
---
Portals sitting behind Cloudflare or a similar firewall connect again. Those
setups answered the app with a challenge page instead of data, so "Test
connection" failed on portals that worked fine in every other player; requests
now identify themselves the way an ordinary IPTV player does.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: xtream
---
Continue Watching resumes a series where you left it: opening one from the
dashboard starts the exact episode at its saved position, and the series page
offers "Play episode N" instead of always starting at the first one. Episodes
launched in MPV or VLC count towards this too.
+12 -12
View File
@@ -56,7 +56,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -322,7 +322,7 @@ jobs:
test -s "${RUNTIME_ROOT}/source-archive-binding.json"
- name: Upload staged Linux runtime
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: linux-embedded-mpv-runtime
path: vendor/embedded-mpv/linux-x64
@@ -330,7 +330,7 @@ jobs:
retention-days: 7
- name: Upload Linux runtime source compliance
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: linux-frame-copy-runtime-sources
path: dist/compliance/linux-frame-copy-runtime-sources.tar.xz
@@ -370,7 +370,7 @@ jobs:
steps: &electron-build-steps
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -424,7 +424,7 @@ jobs:
- name: Download pinned Linux Embedded MPV runtime
if: matrix.os == 'linux'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: linux-embedded-mpv-runtime
path: vendor/embedded-mpv/linux-x64
@@ -1187,7 +1187,7 @@ jobs:
- name: Upload artifacts (macOS)
if: matrix.os == 'macos'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: macos-${{ matrix.arch }}-artifacts
path: |
@@ -1199,7 +1199,7 @@ jobs:
- name: Upload system-runtime Linux artifacts
if: matrix.os == 'linux' && matrix.linux_profile == 'system'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: linux-system-artifacts
path: |
@@ -1211,7 +1211,7 @@ jobs:
- name: Upload portable-runtime Linux artifacts
if: matrix.os == 'linux' && matrix.linux_profile == 'portable'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: linux-portable-artifacts
path: |
@@ -1223,7 +1223,7 @@ jobs:
- name: Upload Flatpak-runtime Linux artifacts
if: matrix.os == 'linux' && matrix.linux_profile == 'flatpak'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: linux-flatpak-artifacts
path: |
@@ -1232,7 +1232,7 @@ jobs:
- name: Upload artifacts (Windows)
if: matrix.os == 'windows'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: windows-artifacts
path: |
@@ -1294,10 +1294,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Download all artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: artifacts
+5 -5
View File
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
# Image pinned by digest (tag 1.7.12). False positives are
# suppressed in .github/actionlint.yaml; shellcheck runs at
@@ -53,7 +53,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
# The gate scripts are dependency-free Node, so this job skips
# pnpm install entirely and stays cheap.
@@ -92,7 +92,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
# nx affected needs the merge-base with the PR target branch.
fetch-depth: 0
@@ -133,7 +133,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -167,7 +167,7 @@ jobs:
- name: Upload unit coverage artifact
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: unit-coverage
path: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. PR runs analyze the merge
# commit checked out above (the modern default); JavaScript is
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Prepare Docker metadata
id: docker-meta
+4 -4
View File
@@ -49,7 +49,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -87,7 +87,7 @@ jobs:
- name: Upload Electron Test Results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: playwright-report-electron-${{ matrix.os }}
path: |
@@ -103,7 +103,7 @@ jobs:
NX_SKIP_NX_CACHE: true
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -131,7 +131,7 @@ jobs:
- name: Upload Web Test Results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: playwright-report-web-ubuntu
path: |
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout released tooling
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.event.release.tag_name }}
persist-credentials: false
@@ -121,7 +121,7 @@ jobs:
printf 'receipt-sha256=%s\n' "${RECEIPT_SHA256}" >> "${GITHUB_OUTPUT}"
- name: Transfer verified release assets
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: verified-snap-release-assets
path: /var/lib/iptvnator-snap-release/assets
@@ -139,7 +139,7 @@ jobs:
steps:
- name: Download verified release assets
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: verified-snap-release-assets
path: ${{ runner.temp }}/verified-snap-release-assets
+3 -1
View File
@@ -16,6 +16,7 @@ This file provides guidance to coding agents working in this repository.
- Use scoped path aliases from `tsconfig.base.json` such as `@iptvnator/services`, `@iptvnator/shared/interfaces`, and `@iptvnator/ui/components`. Do not add new imports from legacy bare aliases such as `services`, `shared-interfaces`, `components`, `m3u-state`, or `database`.
- Every Nx project should keep `scope:*`, `domain:*`, and `type:*` tags in `project.json` so `@nx/enforce-module-boundaries` remains useful for humans and agents.
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
- ESLint enforces `max-lines` on TypeScript files (target under 300, hard maximum 400). Files that predate the rule are baselined in `tools/eslint/max-lines-baseline.mjs`; after splitting a file, regenerate it with `node tools/eslint/generate-max-lines-baseline.mjs`. Never add new files to the baseline — the list must only shrink. A new file that genuinely cannot be split (for example a function serialized into another process) instead carries its own file-wide `/* eslint-disable max-lines -- <why> */`; the generator skips those files, so a justified exemption never lands in the baseline.
- Repository-specific skills are committed under `.codex/skills/`. Claude Code only discovers skills under `.claude/skills/`, so `release-notes` and `release-cut` are mirrored there and the two copies must be kept in sync; every other entry in `.claude/skills/` is personal and stays gitignored. If an external agent does not support skills, treat those files as concise ownership docs.
## Documentation After Changes
@@ -84,7 +85,8 @@ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow lifecycle and unresponsive events
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console output into the Electron terminal
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only event-loop metrics in database and playlist-refresh worker responses; the performance benchmark sets it automatically, and production launches must leave it unset
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC request/completion markers plus count-only M3U acquire/parse/normalize 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 phase events, 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
- Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
+13 -6
View File
@@ -141,7 +141,8 @@ Useful narrower flags:
- `IPTVNATOR_TRACE_WINDOW=1` traces BrowserWindow navigation/load lifecycle
- `IPTVNATOR_TRACE_PLAYER=1` traces external-player activity and bounded Embedded MPV runtime-probe stderr
- `IPTVNATOR_TRACE_RENDERER_CONSOLE=1` mirrors renderer console logs into the Electron terminal
- `IPTVNATOR_PERF_WORKER_PROFILING=1` enables development/test-only event-loop metrics in database and playlist-refresh worker responses; the performance benchmark sets it automatically, and production launches must leave it unset
- `IPTVNATOR_PERF_CAPTURE=1` enables development/test-only, redacted preload IPC request/completion markers plus count-only M3U acquire/parse/normalize 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 phase events, 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
Settings, portal request/response, and trace payloads must use
`@iptvnator/shared/logging` or the redacting portal logger before reaching
@@ -220,7 +221,11 @@ Nx module-boundary tags, the legacy bare-alias ban, and a `max-lines` ESLint
rule (hard maximum 400 lines per TypeScript file). Pre-existing oversized files
are baselined in `tools/eslint/max-lines-baseline.mjs`; regenerate the baseline
with `node tools/eslint/generate-max-lines-baseline.mjs` after splitting a file.
Never add new files to the baseline.
Never add new files to the baseline — the list must only shrink. A new file
that genuinely cannot be split (for example a function serialized into another
process) instead carries its own file-wide
`/* eslint-disable max-lines -- <why> */`; the generator skips those files, so
a justified exemption never lands in the baseline.
## Architecture
@@ -256,6 +261,7 @@ This is an Nx monorepo with the following structure:
- **shared/logging** - Dependency-free structured redaction for diagnostic logs
- **shared/database** - Canonical Drizzle schema and DB connection (used by the Electron backend)
- **shared/m3u-utils** - M3U playlist utilities
- **shared/marketing-fixtures** - Provider-neutral fictional movie metadata shared by the Xtream and Stalker marketing mocks
- **shared/testing** - Shared test helpers
- **ui/components** - Reusable UI components (incl. channel list)
- **ui/epg** - EPG UI (timeline ribbon, multi-EPG, progress panel, program dialogs)
@@ -355,10 +361,11 @@ Key patterns:
- **Factory injection**: `provideXtreamDataSource()` selects Electron or PWA implementation at runtime
Data strategies by environment:
| Environment | Strategy |
|-------------|----------|
| Environment | Strategy |
| ------------ | ------------------------------------------------------- |
| **Electron** | DB-first: Check DB → fetch API if missing → cache to DB |
| **PWA** | API-only: Always fetch from API, store in memory |
| **PWA** | API-only: Always fetch from API, store in memory |
**M3U Playlist Module Architecture**:
@@ -608,7 +615,7 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use
- **Event handlers**: `apps/electron-backend/src/app/events/`
- `database.events.ts` - Database CRUD operations
- `playlist.events.ts` - Playlist import/update
- `epg.events.ts` - EPG IPC registration and freshness/fetch orchestration; worker lifecycle lives in `epg-worker.service.ts`, DB lookups in `epg-query.service.ts`
- `epg.events.ts` - EPG IPC registration; freshness/fetch orchestration lives in `epg-fetch.service.ts`, manual channel-mapping resolution and CRUD in `epg-mapping.service.ts`, worker lifecycle in `epg-worker.service.ts`, DB lookups in `epg-query.service.ts`
- `xtream.events.ts` - Xtream Codes API
- `stalker.events.ts` - Legacy simple/PWA-compatible Stalker portal API
- `stalker-session.events.ts` - Typed full-portal session IPC; the
+19 -2
View File
@@ -12,14 +12,31 @@
"e2e": {
"dependsOn": ["electron-backend:build-e2e"]
},
"test-performance-harness": {
"executor": "nx:run-commands",
"options": {
"cwd": "apps/electron-backend-e2e",
"command": "pnpm exec tsx --test src/performance/*.spec.ts"
}
},
"benchmark-m3u-refresh-cancellation": {
"dependsOn": ["electron-backend:build-e2e"],
"dependsOn": ["electron-backend:build-performance"],
"executor": "nx:run-commands",
"cache": false,
"parallelism": false,
"options": {
"cwd": "apps/electron-backend-e2e",
"command": "pnpm exec playwright test --config=playwright.performance.config.ts"
"command": "pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts"
}
},
"benchmark-m3u-import": {
"dependsOn": ["electron-backend:build-performance"],
"executor": "nx:run-commands",
"cache": false,
"parallelism": false,
"options": {
"cwd": "apps/electron-backend-e2e",
"command": "pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-import.performance.ts"
}
},
"packaged-frame-copy-smoke": {
@@ -0,0 +1,122 @@
import type { ElectronApplication, Page } from '@playwright/test';
import {
closeElectronApp,
expect,
launchElectronApp,
test,
type LaunchedElectronApp,
} from './electron-test-fixtures';
import {
installMainCapture,
rolloverMainCapture,
startMainCapture,
stopMainCapture,
} from './performance/m3u-refresh-main-capture';
import type { MainCaptureMetrics } from './performance/m3u-refresh-cancellation-contract';
import type { RendererWindowIdentity } from './performance/renderer-window-rss-session';
test('captures the exact database worker post-GC heap in built Electron', async ({
dataDir,
}) => {
const launchedApps: LaunchedElectronApp[] = [];
try {
const app = await launchElectronApp(dataDir, {
args: ['--js-flags=--expose-gc'],
env: {
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
IPTVNATOR_PERF_CAPTURE: '1',
IPTVNATOR_PERF_WORKER_PROFILING: '1',
},
});
launchedApps.push(app);
await installMainCapture(app.electronApp);
const rendererWindowIdentity = await resolveRendererWindowIdentity(
app.electronApp,
app.mainWindow
);
const captureOptions = {
diagnostic: false,
outputDirectory: dataDir,
rendererWindowIdentity,
} as const;
await startMainCapture(app.electronApp, captureOptions);
await requestDatabaseWorker(app);
const rollover = await rolloverMainCapture(
app.electronApp,
captureOptions
);
expect(rollover.nextCaptureStarted).toBe(true);
expect(rollover.nextCaptureUnavailableReason).toBeNull();
const firstDatabaseWorker = expectExactDatabaseWorker(
rollover.completedCapture
);
await requestDatabaseWorker(app);
const secondDatabaseWorker = expectExactDatabaseWorker(
await stopMainCapture(app.electronApp)
);
expect(secondDatabaseWorker.ordinal).toBe(firstDatabaseWorker.ordinal);
await startMainCapture(app.electronApp, captureOptions);
const noDatabasePhase = await stopMainCapture(app.electronApp);
expect(
noDatabasePhase.workers.filter(
(worker) => worker.kind === 'database.worker'
)
).toHaveLength(0);
} finally {
await Promise.all(launchedApps.map((app) => closeElectronApp(app)));
}
});
async function requestDatabaseWorker(app: LaunchedElectronApp): Promise<void> {
await app.mainWindow.evaluate(() => window.electron.dbGetAppPlaylists());
}
function expectExactDatabaseWorker(capture: MainCaptureMetrics) {
const databaseWorkers = capture.workers.filter(
(worker) => worker.kind === 'database.worker'
);
expect(databaseWorkers).toHaveLength(1);
const databaseWorker = databaseWorkers[0];
expect(databaseWorker?.ordinal).toBeGreaterThan(0);
expect(databaseWorker?.postGcHeapUnavailableReason).toBeNull();
expect(databaseWorker?.postGcHeapUsedBytes).toBeGreaterThan(0);
expect(databaseWorker?.requests).toHaveLength(1);
expect(databaseWorker?.requests[0]).toEqual(
expect.objectContaining({
invalidReason: null,
operation: 'DB_GET_APP_PLAYLISTS',
performanceCaptureUnavailableReason: null,
playlistId: null,
requestId: expect.any(String),
success: true,
})
);
return databaseWorker;
}
async function resolveRendererWindowIdentity(
electronApp: ElectronApplication,
page: Page
): Promise<RendererWindowIdentity> {
const browserWindowHandle = await electronApp.browserWindow(page);
try {
return await browserWindowHandle.evaluate((browserWindow) => {
const exactWindow = browserWindow as unknown as {
readonly id: number;
readonly webContents: { readonly id: number };
};
return {
browserWindowId: exactWindow.id,
webContentsId: exactWindow.webContents.id,
};
});
} finally {
await browserWindowHandle.dispose();
}
}
@@ -0,0 +1,8 @@
import { test } from '@playwright/test';
import { runM3uImportBenchmark } from './performance/m3u-import.benchmark';
// eslint-disable-next-line playwright/expect-expect -- Assertions run inside the shared benchmark lifecycle.
test('profiles initial M3U imports at 10k, 50k, and 100k', async () => {
await runM3uImportBenchmark();
});
@@ -0,0 +1,359 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
import {
installDatabaseRequestIdentityCaptureInMain,
type DatabaseRequestIdentityCaptureApi,
} from './database-request-identity-capture';
const CHANNEL = 'IPTVNATOR_PERF_CAPTURE_MARKER';
let nextStateId = 1;
function createCapture(): {
api: DatabaseRequestIdentityCaptureApi;
ipcMain: EventEmitter;
} {
const ipcMain = new EventEmitter();
const stateKey = `__identityCapture${nextStateId}`;
nextStateId += 1;
installDatabaseRequestIdentityCaptureInMain({ ipcMain } as never, stateKey);
const api = (globalThis as unknown as Record<string, unknown>)[
stateKey
] as DatabaseRequestIdentityCaptureApi;
api.start();
return { api, ipcMain };
}
function marker(overrides: Record<string, unknown> = {}) {
return {
correlationState: 'correlated',
invalidReason: null,
ipcCallId: 2,
method: 'dbGetAppPlaylist',
operationId: 'refresh-operation-1',
phase: 'start',
playlistId: 'playlist-1',
sourceEpochMs: 100,
...overrides,
};
}
test('correlates the real preload marker to a DB request whose payload omits operationId', () => {
const { api, ipcMain } = createCapture();
ipcMain.emit(CHANNEL, {}, marker());
const identity = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-1',
type: 'request',
});
assert.deepEqual(identity, {
ipcCallId: 2,
operationId: 'refresh-operation-1',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
});
test('retains the initial-import upsert marker when its operationId is intentionally uncorrelated', () => {
const { api, ipcMain } = createCapture();
ipcMain.emit(
CHANNEL,
{},
marker({
correlationState: 'uncorrelated',
ipcCallId: 7,
method: 'dbUpsertAppPlaylist',
operationId: null,
sourceEpochMs: 1_077.25,
})
);
const identity = api.matchDatabaseRequest({
operation: 'DB_UPSERT_APP_PLAYLIST',
payload: { _id: 'playlist-1' },
requestId: 'initial-import-upsert',
type: 'request',
});
assert.deepEqual(identity, {
ipcCallId: 7,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-uncorrelated',
sourceEpochMs: 1_077.25,
});
});
test('captures exact successful preload completion markers for DB return-clone attribution', () => {
const { api, ipcMain } = createCapture();
ipcMain.emit(
CHANNEL,
{},
marker({
correlationState: 'uncorrelated',
ipcCallId: 7,
method: 'dbUpsertAppPlaylist',
operationId: null,
phase: 'success',
sourceEpochMs: 140.25,
})
);
ipcMain.emit(
CHANNEL,
{},
marker({
correlationState: 'uncorrelated',
ipcCallId: 8,
operationId: null,
phase: 'success',
sourceEpochMs: 175.5,
})
);
assert.equal(api.successMarkerCount(), 2);
assert.deepEqual(api.takeSuccessMarkers(), [
{
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
operationId: null,
playlistId: 'playlist-1',
sourceEpochMs: 140.25,
},
{
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
operationId: null,
playlistId: 'playlist-1',
sourceEpochMs: 175.5,
},
]);
assert.equal(api.successMarkerCount(), 0);
assert.deepEqual(api.takeSuccessMarkers(), []);
});
test('fails closed for malformed or invalid successful preload markers and resets completions', () => {
const { api, ipcMain } = createCapture();
for (const invalid of [
{ invalidReason: 'out-of-order' },
{ correlationState: 'invalid' },
{ ipcCallId: 0 },
{ playlistId: '' },
{ sourceEpochMs: Number.NaN },
{ method: 'refreshPlaylist' },
{ correlationState: 'correlated', operationId: null },
]) {
ipcMain.emit(
CHANNEL,
{},
marker({
phase: 'success',
...invalid,
})
);
}
assert.equal(api.successMarkerCount(), 0);
assert.deepEqual(api.takeSuccessMarkers(), []);
ipcMain.emit(
CHANNEL,
{},
marker({
phase: 'success',
})
);
api.stop();
api.start();
assert.equal(api.successMarkerCount(), 0);
assert.deepEqual(api.takeSuccessMarkers(), []);
});
test('resolves a response-retained request identity when the marker arrives later and resets between captures', () => {
const { api, ipcMain } = createCapture();
const identity = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-before-marker',
type: 'request',
});
assert.equal(
identity.operationIdUnavailableReason,
'preload-performance-marker-missing'
);
const responseOutcome = { identity };
ipcMain.emit(CHANNEL, {}, marker());
assert.deepEqual(responseOutcome.identity, {
ipcCallId: 2,
operationId: 'refresh-operation-1',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
api.stop();
ipcMain.emit(CHANNEL, {}, marker({ operationId: 'marker-while-stopped' }));
api.start();
const afterReset = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-after-reset',
type: 'request',
});
assert.deepEqual(afterReset, {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-missing',
sourceEpochMs: null,
});
});
test('fails closed for missing and ambiguous preload markers', () => {
const missingCapture = createCapture();
const missing = missingCapture.api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-missing',
type: 'request',
});
assert.deepEqual(missing, {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-missing',
sourceEpochMs: null,
});
const ambiguousCapture = createCapture();
ambiguousCapture.ipcMain.emit(CHANNEL, {}, marker());
ambiguousCapture.ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 3,
operationId: 'refresh-operation-2',
})
);
const ambiguous = ambiguousCapture.api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-ambiguous',
type: 'request',
});
assert.deepEqual(ambiguous, {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-ambiguous',
sourceEpochMs: null,
});
});
test('quarantines a key after response-before-marker ambiguity', () => {
const { api, ipcMain } = createCapture();
const requestA = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-a',
type: 'request',
});
const requestB = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-b',
type: 'request',
});
ipcMain.emit(
CHANNEL,
{},
marker({ ipcCallId: 2, operationId: 'refresh-operation-a' })
);
ipcMain.emit(
CHANNEL,
{},
marker({ ipcCallId: 3, operationId: 'refresh-operation-b' })
);
const requestC = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'request-c',
type: 'request',
});
ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 4,
operationId: 'other-playlist-operation',
playlistId: 'playlist-2',
})
);
const otherPlaylist = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-2' },
requestId: 'other-playlist-request',
type: 'request',
});
ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 5,
method: 'dbUpsertAppPlaylist',
operationId: 'other-db-operation',
})
);
const otherOperation = api.matchDatabaseRequest({
operation: 'DB_UPSERT_APP_PLAYLIST',
payload: { _id: 'playlist-1' },
requestId: 'other-operation-request',
type: 'request',
});
const ambiguousIdentity = {
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: 'preload-performance-marker-ambiguous',
sourceEpochMs: null,
};
assert.deepEqual(requestA, ambiguousIdentity);
assert.deepEqual(requestB, ambiguousIdentity);
assert.deepEqual(requestC, ambiguousIdentity);
assert.deepEqual(otherPlaylist, {
ipcCallId: 4,
operationId: 'other-playlist-operation',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
assert.deepEqual(otherOperation, {
ipcCallId: 5,
operationId: 'other-db-operation',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
api.stop();
api.start();
ipcMain.emit(
CHANNEL,
{},
marker({
ipcCallId: 6,
operationId: 'fresh-capture-operation',
})
);
const freshCaptureIdentity = api.matchDatabaseRequest({
operation: 'DB_GET_APP_PLAYLIST',
payload: { playlistId: 'playlist-1' },
requestId: 'fresh-capture-request',
type: 'request',
});
assert.deepEqual(freshCaptureIdentity, {
ipcCallId: 6,
operationId: 'fresh-capture-operation',
operationIdUnavailableReason: null,
sourceEpochMs: 100,
});
});
@@ -0,0 +1,354 @@
import type { ElectronApplication } from '@playwright/test';
export const DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY =
'__iptvnatorM3uRefreshDatabaseRequestIdentityCapture';
export interface DatabaseRequestIdentity {
ipcCallId: number | null;
operationId: string | null;
operationIdUnavailableReason: string | null;
sourceEpochMs: number | null;
}
export interface PreloadDatabaseSuccessMarker {
ipcCallId: number;
operation: 'DB_GET_APP_PLAYLIST' | 'DB_UPSERT_APP_PLAYLIST';
operationId: string | null;
playlistId: string;
sourceEpochMs: number;
}
export interface DatabaseRequestIdentityCaptureApi {
matchDatabaseRequest(message: unknown): DatabaseRequestIdentity;
start(): void;
stop(): void;
successMarkerCount(): number;
takeSuccessMarkers(): readonly PreloadDatabaseSuccessMarker[];
}
interface DatabaseRequestIdentityElectron {
readonly ipcMain: {
on(
channel: string,
listener: (event: unknown, marker: unknown) => void
): void;
};
}
export function installDatabaseRequestIdentityCaptureInMain(
{ ipcMain }: DatabaseRequestIdentityElectron,
stateKey: string
): void {
type JsonRecord = Record<string, unknown>;
interface PendingMarker {
readonly ipcCallId: number;
readonly operation: string;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
readonly playlistId: string;
readonly sourceEpochMs: number;
}
interface PendingRequest {
readonly identity: DatabaseRequestIdentity;
readonly operation: string;
readonly playlistId: string;
}
interface QuarantinedKey {
readonly operation: string;
readonly playlistId: string;
}
const target = globalThis as unknown as Record<string, unknown>;
if (target[stateKey] !== undefined) {
return;
}
const markerChannel = 'IPTVNATOR_PERF_CAPTURE_MARKER';
const markerMethodToOperation: Readonly<Record<string, string>> = {
dbGetAppPlaylist: 'DB_GET_APP_PLAYLIST',
dbUpsertAppPlaylist: 'DB_UPSERT_APP_PLAYLIST',
};
let active = false;
let pendingMarkers: PendingMarker[] = [];
let pendingRequests: PendingRequest[] = [];
let quarantinedKeys: QuarantinedKey[] = [];
let successMarkers: PreloadDatabaseSuccessMarker[] = [];
const readRecord = (value: unknown): JsonRecord | null =>
typeof value === 'object' && value !== null
? (value as JsonRecord)
: null;
const readPlaylistId = (
operation: string,
payload: unknown
): string | null => {
const value = readRecord(payload);
if (!value) {
return null;
}
const playlistId =
operation === 'DB_UPSERT_APP_PLAYLIST'
? value['_id']
: value['playlistId'];
return typeof playlistId === 'string' && playlistId.length > 0
? playlistId
: null;
};
const matches = (
candidate: { operation: string; playlistId: string },
operation: string,
playlistId: string
): boolean =>
candidate.operation === operation &&
candidate.playlistId === playlistId;
const markAmbiguous = (requests: PendingRequest[]): void => {
for (const request of requests) {
request.identity.ipcCallId = null;
request.identity.operationId = null;
request.identity.operationIdUnavailableReason =
'preload-performance-marker-ambiguous';
request.identity.sourceEpochMs = null;
}
};
const unavailableIdentity = (reason: string): DatabaseRequestIdentity => ({
ipcCallId: null,
operationId: null,
operationIdUnavailableReason: reason,
sourceEpochMs: null,
});
const identityFromMarker = (
marker: PendingMarker
): DatabaseRequestIdentity => ({
ipcCallId: marker.ipcCallId,
operationId: marker.operationId,
operationIdUnavailableReason: marker.operationIdUnavailableReason,
sourceEpochMs: marker.sourceEpochMs,
});
const applyMarker = (
identity: DatabaseRequestIdentity,
marker: PendingMarker
): void => {
identity.ipcCallId = marker.ipcCallId;
identity.operationId = marker.operationId;
identity.operationIdUnavailableReason =
marker.operationIdUnavailableReason;
identity.sourceEpochMs = marker.sourceEpochMs;
};
const isQuarantined = (operation: string, playlistId: string): boolean =>
quarantinedKeys.some((key) => matches(key, operation, playlistId));
const quarantine = (operation: string, playlistId: string): void => {
if (!isQuarantined(operation, playlistId)) {
quarantinedKeys.push({ operation, playlistId });
}
const requestCandidates = pendingRequests.filter((request) =>
matches(request, operation, playlistId)
);
markAmbiguous(requestCandidates);
pendingRequests = pendingRequests.filter(
(request) => !matches(request, operation, playlistId)
);
pendingMarkers = pendingMarkers.filter(
(marker) => !matches(marker, operation, playlistId)
);
};
ipcMain.on(markerChannel, (_event, input) => {
if (!active) {
return;
}
const marker = readRecord(input);
const correlationState = marker?.['correlationState'];
const operation =
typeof marker?.['method'] === 'string'
? markerMethodToOperation[marker['method']]
: undefined;
const operationId = marker?.['operationId'];
const playlistId = marker?.['playlistId'];
const ipcCallId = marker?.['ipcCallId'];
const sourceEpochMs = marker?.['sourceEpochMs'];
const validMarkerIdentity =
operation !== undefined &&
typeof playlistId === 'string' &&
playlistId.length > 0 &&
Number.isSafeInteger(ipcCallId) &&
Number(ipcCallId) >= 1 &&
typeof sourceEpochMs === 'number' &&
Number.isFinite(sourceEpochMs) &&
sourceEpochMs >= 0;
if (
marker?.['phase'] === 'success' &&
(correlationState === 'correlated' ||
correlationState === 'uncorrelated') &&
marker['invalidReason'] === null &&
validMarkerIdentity &&
(correlationState !== 'correlated' ||
(typeof operationId === 'string' &&
operationId.length > 0))
) {
successMarkers.push({
ipcCallId: Number(ipcCallId),
operation: operation as PreloadDatabaseSuccessMarker['operation'],
operationId:
correlationState === 'correlated'
? String(operationId)
: null,
playlistId,
sourceEpochMs,
});
return;
}
if (
!marker ||
marker['phase'] !== 'start' ||
(correlationState !== 'correlated' &&
correlationState !== 'uncorrelated') ||
marker['invalidReason'] !== null
) {
return;
}
const correlatedOperationId =
correlationState === 'correlated' &&
typeof operationId === 'string' &&
operationId.length > 0
? operationId
: null;
if (
operation === undefined ||
(correlationState === 'correlated' &&
correlatedOperationId === null) ||
typeof playlistId !== 'string' ||
playlistId.length === 0 ||
!Number.isSafeInteger(ipcCallId) ||
Number(ipcCallId) < 1 ||
typeof sourceEpochMs !== 'number' ||
!Number.isFinite(sourceEpochMs) ||
sourceEpochMs < 0
) {
return;
}
const pendingMarker: PendingMarker = {
ipcCallId: Number(ipcCallId),
operation,
operationId: correlatedOperationId,
operationIdUnavailableReason:
correlationState === 'correlated'
? null
: 'preload-performance-marker-uncorrelated',
playlistId,
sourceEpochMs,
};
if (isQuarantined(operation, playlistId)) {
return;
}
const requestCandidates = pendingRequests.filter((request) =>
matches(request, operation, playlistId)
);
if (requestCandidates.length === 1) {
const request = requestCandidates[0];
if (!request) {
return;
}
applyMarker(request.identity, pendingMarker);
pendingRequests = pendingRequests.filter(
(candidate) => candidate !== request
);
return;
}
if (requestCandidates.length > 1) {
quarantine(operation, playlistId);
return;
}
pendingMarkers.push(pendingMarker);
pendingMarkers.sort((left, right) => left.ipcCallId - right.ipcCallId);
});
const api: DatabaseRequestIdentityCaptureApi = {
matchDatabaseRequest: (input) => {
const message = readRecord(input);
const operation = message?.['operation'];
const playlistId =
typeof operation === 'string'
? readPlaylistId(operation, message?.['payload'])
: null;
if (
!active ||
(operation !== 'DB_GET_APP_PLAYLIST' &&
operation !== 'DB_UPSERT_APP_PLAYLIST') ||
playlistId === null
) {
return unavailableIdentity(
'preload-performance-marker-not-applicable'
);
}
if (isQuarantined(operation, playlistId)) {
return unavailableIdentity(
'preload-performance-marker-ambiguous'
);
}
const markerCandidates = pendingMarkers.filter((marker) =>
matches(marker, operation, playlistId)
);
if (markerCandidates.length === 1) {
const marker = markerCandidates[0];
if (!marker) {
return unavailableIdentity(
'preload-performance-marker-missing'
);
}
pendingMarkers = pendingMarkers.filter(
(candidate) => candidate !== marker
);
return identityFromMarker(marker);
}
if (markerCandidates.length > 1) {
quarantine(operation, playlistId);
return unavailableIdentity(
'preload-performance-marker-ambiguous'
);
}
const identity = unavailableIdentity(
'preload-performance-marker-missing'
);
pendingRequests.push({
identity,
operation,
playlistId,
});
return identity;
},
start: () => {
pendingMarkers = [];
pendingRequests = [];
quarantinedKeys = [];
successMarkers = [];
active = true;
},
stop: () => {
active = false;
pendingMarkers = [];
pendingRequests = [];
quarantinedKeys = [];
successMarkers = [];
},
successMarkerCount: () => successMarkers.length,
takeSuccessMarkers: () => {
const captured = Object.freeze([...successMarkers]);
successMarkers = [];
return captured;
},
};
target[stateKey] = api;
}
export async function installDatabaseRequestIdentityCapture(
electronApp: ElectronApplication
): Promise<void> {
await electronApp.evaluate(
installDatabaseRequestIdentityCaptureInMain,
DATABASE_REQUEST_IDENTITY_CAPTURE_STATE_KEY
);
}
@@ -0,0 +1,98 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
function section(startMarker: string, endMarker: string): string {
const start = source.indexOf(startMarker);
const end = source.indexOf(endMarker, start);
assert.ok(start >= 0, `missing start marker: ${startMarker}`);
assert.ok(end > start, `missing end marker: ${endMarker}`);
return source.slice(start, end);
}
test('counts only finite nonnegative worker heap and external-memory samples per capture generation', () => {
const workerRecord = section(
'interface WorkerRecord',
'interface TimelineRecord'
);
const createRecord = section(
'const record: WorkerRecord = {',
'nextWorkerOrdinal += 1'
);
const sampleWorker = section(
'const sampleWorker =',
'const resetWorkerForCapture'
);
const resetWorker = section(
'const resetWorkerForCapture',
'const startWorker'
);
assert.match(workerRecord, /externalMemorySampleCount: number/);
assert.match(workerRecord, /heapUsedSampleCount: number/);
assert.match(createRecord, /externalMemorySampleCount: 0/);
assert.match(createRecord, /heapUsedSampleCount: 0/);
assert.match(resetWorker, /record\.externalMemorySampleCount = 0/);
assert.match(resetWorker, /record\.heapUsedSampleCount = 0/);
assert.match(sampleWorker, /const heapUsedSample = stats\.used_heap_size/);
assert.match(
sampleWorker,
/Number\.isFinite\(heapUsedSample\)[\s\S]*heapUsedSample >= 0/
);
assert.match(sampleWorker, /record\.heapUsedSampleCount \+= 1/);
assert.match(
sampleWorker,
/const externalMemorySample = stats\.external_memory/
);
assert.match(
sampleWorker,
/Number\.isFinite\(externalMemorySample\)[\s\S]*externalMemorySample >= 0/
);
assert.match(sampleWorker, /record\.externalMemorySampleCount \+= 1/);
assert.doesNotMatch(sampleWorker, /used_heap_size \?\? 0/);
assert.doesNotMatch(sampleWorker, /external_memory \?\? 0/);
});
test('raw worker metrics distinguish missing samples from measured zero-byte peaks', () => {
const transport = section(
'const workers = currentWorkerRecords',
'const transport: MainCaptureGenerationTransport'
);
assert.match(
transport,
/externalMemorySampleCount:\s*record\.externalMemorySampleCount/
);
assert.match(
transport,
/heapUsedSampleCount:\s*record\.heapUsedSampleCount/
);
assert.match(
transport,
/peakExternalBytes:\s*record\.externalMemorySampleCount > 0[\s\S]*\?\s*record\.externalPeak[\s\S]*:\s*null/
);
assert.match(
transport,
/peakHeapUsedBytes:\s*record\.heapUsedSampleCount > 0[\s\S]*\?\s*record\.heapPeak[\s\S]*:\s*null/
);
assert.match(
transport,
/peakExternalUnavailableReason:\s*record\.externalMemorySampleCount > 0[\s\S]*\?\s*null[\s\S]*:\s*'worker-external-memory-samples-missing'/
);
assert.match(
transport,
/peakHeapUsedUnavailableReason:\s*record\.heapUsedSampleCount > 0[\s\S]*\?\s*null[\s\S]*:\s*'worker-heap-used-samples-missing'/
);
assert.doesNotMatch(
transport,
/peakExternalBytes: record\.externalPeak[,}]/
);
assert.doesNotMatch(transport, /peakHeapUsedBytes: record\.heapPeak[,}]/);
});
@@ -0,0 +1,121 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
assessDatabaseWorkerPeakMemoryValidity,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON,
} from './database-worker-peak-memory-validity';
describe('database worker peak-memory validity', () => {
it('requires successful heap and external-memory samples in every measured run', () => {
const validity = assessDatabaseWorkerPeakMemoryValidity([
iteration('warmup', 'warmup', worker(0, 0)),
iteration('run-01', 'measured', worker(3, 4)),
iteration('diagnostic', 'diagnostic', worker(0, 0)),
]);
assert.deepEqual(validity, {
invalidMeasuredRuns: [],
measuredRunCount: 1,
validForComparison: true,
validMeasuredRunCount: 1,
});
});
it('fails closed for absent samples instead of accepting initialized zero peaks', () => {
const missingCounts = assessDatabaseWorkerPeakMemoryValidity([
iteration('run-01', 'measured', {
kind: 'database.worker',
peakExternalBytes: 0,
peakHeapUsedBytes: 0,
}),
iteration('run-02', 'measured', worker(0, 1)),
iteration('run-03', 'measured', worker(1, 0)),
]);
assert.deepEqual(missingCounts.invalidMeasuredRuns, [
{
reason: DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_SAMPLES_MISSING,
runId: 'run-01',
},
{
reason: DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_SAMPLES_MISSING,
runId: 'run-02',
},
{
reason: DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_SAMPLES_MISSING,
runId: 'run-03',
},
]);
assert.equal(missingCounts.validForComparison, false);
assert.equal(missingCounts.validMeasuredRunCount, 0);
});
it('reports stable worker-cardinality and malformed-peak reasons', () => {
const validity = assessDatabaseWorkerPeakMemoryValidity([
{ kind: 'measured', main: { workers: [] }, runId: 'missing' },
{
kind: 'measured',
main: { workers: [worker(1, 1), worker(1, 1)] },
runId: 'multiple',
},
iteration('heap-invalid', 'measured', {
...worker(1, 1),
peakHeapUsedBytes: Number.NaN,
}),
iteration('external-invalid', 'measured', {
...worker(1, 1),
peakExternalBytes: -1,
}),
iteration('heap-reason-incoherent', 'measured', {
...worker(1, 1),
peakHeapUsedUnavailableReason:
'worker-heap-used-samples-missing',
}),
iteration('external-reason-incoherent', 'measured', {
...worker(1, 1),
peakExternalUnavailableReason:
'worker-external-memory-samples-missing',
}),
]);
assert.deepEqual(
validity.invalidMeasuredRuns.map(({ reason }) => reason),
[
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.DATABASE_WORKER_MISSING,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_PEAK_INVALID,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_PEAK_INVALID,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_PEAK_INVALID,
DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_PEAK_INVALID,
]
);
});
});
function iteration(
runId: string,
kind: string,
databaseWorker: Record<string, unknown>
) {
return {
kind,
main: { workers: [databaseWorker] },
runId,
};
}
function worker(
heapUsedSampleCount: number,
externalMemorySampleCount: number
) {
return {
externalMemorySampleCount,
heapUsedSampleCount,
kind: 'database.worker',
peakExternalBytes: 600,
peakExternalUnavailableReason: null,
peakHeapUsedBytes: 1_500,
peakHeapUsedUnavailableReason: null,
};
}
@@ -0,0 +1,114 @@
export const DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON = {
EXTERNAL_MEMORY_PEAK_INVALID:
'database-worker-external-memory-peak-invalid',
EXTERNAL_MEMORY_SAMPLES_MISSING:
'database-worker-external-memory-samples-missing',
HEAP_PEAK_INVALID: 'database-worker-heap-peak-invalid',
HEAP_SAMPLES_MISSING: 'database-worker-heap-samples-missing',
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
DATABASE_WORKER_MISSING: 'database-worker-missing',
} as const;
export type DatabaseWorkerPeakMemoryInvalidReason =
(typeof DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON)[keyof typeof DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON];
export interface DatabaseWorkerPeakMemoryValidity {
readonly invalidMeasuredRuns: readonly {
readonly reason: DatabaseWorkerPeakMemoryInvalidReason;
readonly runId: string;
}[];
readonly measuredRunCount: number;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
interface PeakMemoryWorker {
readonly externalMemorySampleCount?: unknown;
readonly heapUsedSampleCount?: unknown;
readonly kind: string;
readonly peakExternalBytes: unknown;
readonly peakExternalUnavailableReason?: unknown;
readonly peakHeapUsedBytes: unknown;
readonly peakHeapUsedUnavailableReason?: unknown;
}
interface PeakMemoryIteration {
readonly kind: string;
readonly main: {
readonly workers: readonly PeakMemoryWorker[];
};
readonly runId: string;
}
export function assessDatabaseWorkerPeakMemoryValidity(
iterations: readonly PeakMemoryIteration[]
): DatabaseWorkerPeakMemoryValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === 'measured'
);
const invalidMeasuredRuns: {
reason: DatabaseWorkerPeakMemoryInvalidReason;
runId: string;
}[] = [];
for (const iteration of measured) {
const workers = iteration.main.workers.filter(
(worker) => worker.kind === 'database.worker'
);
const reason = assessWorkers(workers);
if (reason !== null) {
invalidMeasuredRuns.push({ reason, runId: iteration.runId });
}
}
return Object.freeze({
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
validForComparison:
measured.length > 0 && invalidMeasuredRuns.length === 0,
validMeasuredRunCount: measured.length - invalidMeasuredRuns.length,
});
}
function assessWorkers(
workers: readonly PeakMemoryWorker[]
): DatabaseWorkerPeakMemoryInvalidReason | null {
if (workers.length === 0) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.DATABASE_WORKER_MISSING;
}
if (workers.length !== 1) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.MULTIPLE_DATABASE_WORKERS;
}
const worker = workers[0];
if (!worker) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.DATABASE_WORKER_MISSING;
}
if (!isPositiveSampleCount(worker.heapUsedSampleCount)) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_SAMPLES_MISSING;
}
if (
!isPeak(worker.peakHeapUsedBytes) ||
worker.peakHeapUsedUnavailableReason !== null
) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.HEAP_PEAK_INVALID;
}
if (!isPositiveSampleCount(worker.externalMemorySampleCount)) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_SAMPLES_MISSING;
}
if (
!isPeak(worker.peakExternalBytes) ||
worker.peakExternalUnavailableReason !== null
) {
return DATABASE_WORKER_PEAK_MEMORY_INVALID_REASON.EXTERNAL_MEMORY_PEAK_INVALID;
}
return null;
}
function isPositiveSampleCount(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
}
function isPeak(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0;
}
@@ -0,0 +1,116 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
type RequestDisposition = 'after-cutoff' | 'capture' | 'outside-capture';
interface CutoffApi {
beginCapture(): void;
beginStop(): void;
finishStop(): void;
observeDatabaseRequest(): RequestDisposition;
rolloverCapture?(): void;
snapshot(): {
readonly lateRequestCount: number;
readonly phase: 'active' | 'idle' | 'stopping';
};
}
interface CutoffModule {
createDatabaseWorkerPostGcCutoffApi?: () => CutoffApi;
}
const cutoffModulePromise = import(
new URL('./database-worker-post-gc-cutoff.ts', import.meta.url).href
)
.then((module) => module as CutoffModule)
.catch(() => null);
async function restoreSerializableApi(): Promise<CutoffApi> {
const module = await cutoffModulePromise;
assert.ok(module, 'database worker post-GC cutoff module must exist');
const factory = module.createDatabaseWorkerPostGcCutoffApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => CutoffApi;
return restoredFactory();
}
test('classifies requests after the synchronous capture cutoff without carrying state across generations', async () => {
const api = await restoreSerializableApi();
assert.equal(api.observeDatabaseRequest(), 'outside-capture');
api.beginCapture();
assert.equal(api.observeDatabaseRequest(), 'capture');
api.beginStop();
assert.equal(api.observeDatabaseRequest(), 'after-cutoff');
assert.equal(api.observeDatabaseRequest(), 'after-cutoff');
assert.deepEqual(api.snapshot(), {
lateRequestCount: 2,
phase: 'stopping',
});
api.finishStop();
assert.equal(api.observeDatabaseRequest(), 'outside-capture');
api.beginCapture();
assert.deepEqual(api.snapshot(), {
lateRequestCount: 0,
phase: 'active',
});
});
test('atomically rolls a clean cutoff into the next generation without erasing contamination', async () => {
const clean = await restoreSerializableApi();
assert.equal(typeof clean.rolloverCapture, 'function');
clean.beginCapture();
clean.beginStop();
clean.rolloverCapture?.();
assert.deepEqual(clean.snapshot(), {
lateRequestCount: 0,
phase: 'active',
});
assert.equal(clean.observeDatabaseRequest(), 'capture');
const contaminated = await restoreSerializableApi();
assert.equal(typeof contaminated.rolloverCapture, 'function');
contaminated.beginCapture();
contaminated.beginStop();
assert.equal(contaminated.observeDatabaseRequest(), 'after-cutoff');
assert.throws(
() => contaminated.rolloverCapture?.(),
/database-worker-post-gc-capture-contaminated/
);
assert.deepEqual(contaminated.snapshot(), {
lateRequestCount: 1,
phase: 'stopping',
});
});
test('main capture arms cutoff before awaiting and rejects late DB work before startWorker can restart profiling', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const stopStart = source.indexOf('stop: async (');
const cutoffStart = source.indexOf(
'databaseWorkerPostGcCutoffApi.beginStop()',
stopStart
);
const activeFalse = source.indexOf('state.active = false', stopStart);
const firstAwait = source.indexOf('await ', stopStart);
const observeRequest = source.indexOf(
'databaseWorkerPostGcCutoffApi.observeDatabaseRequest()'
);
const startWorker = source.indexOf('startWorker(record)', observeRequest);
assert.ok(stopStart >= 0);
assert.ok(cutoffStart > stopStart && cutoffStart < firstAwait);
assert.ok(activeFalse > stopStart && activeFalse < firstAwait);
assert.ok(observeRequest >= 0 && observeRequest < startWorker);
assert.match(source, /database-worker-activity-after-cutoff/);
});
@@ -0,0 +1,74 @@
export type DatabaseWorkerPostGcRequestDisposition =
'after-cutoff' | 'capture' | 'outside-capture';
export interface DatabaseWorkerPostGcCutoffSnapshot {
readonly lateRequestCount: number;
readonly phase: 'active' | 'idle' | 'stopping';
}
export interface DatabaseWorkerPostGcCutoffApi {
beginCapture(): void;
beginStop(): void;
finishStop(): void;
observeDatabaseRequest(): DatabaseWorkerPostGcRequestDisposition;
rolloverCapture(): void;
snapshot(): DatabaseWorkerPostGcCutoffSnapshot;
}
export function createDatabaseWorkerPostGcCutoffApi(): DatabaseWorkerPostGcCutoffApi {
let lateRequestCount = 0;
let phase: DatabaseWorkerPostGcCutoffSnapshot['phase'] = 'idle';
const api: DatabaseWorkerPostGcCutoffApi = {
beginCapture(): void {
if (phase !== 'idle') {
throw new Error(
phase === 'stopping'
? 'database-worker-post-gc-stop-in-progress'
: 'database-worker-post-gc-capture-already-active'
);
}
lateRequestCount = 0;
phase = 'active';
},
beginStop(): void {
if (phase !== 'active') {
throw new Error('database-worker-post-gc-capture-not-active');
}
phase = 'stopping';
},
finishStop(): void {
phase = 'idle';
},
observeDatabaseRequest(): DatabaseWorkerPostGcRequestDisposition {
if (phase === 'active') {
return 'capture';
}
if (phase === 'stopping') {
lateRequestCount += 1;
return 'after-cutoff';
}
return 'outside-capture';
},
rolloverCapture(): void {
if (phase !== 'stopping') {
throw new Error('database-worker-post-gc-stop-not-in-progress');
}
if (lateRequestCount > 0) {
throw new Error('database-worker-post-gc-capture-contaminated');
}
lateRequestCount = 0;
phase = 'active';
},
snapshot(): DatabaseWorkerPostGcCutoffSnapshot {
return Object.freeze({ lateRequestCount, phase });
},
};
return Object.freeze(api);
}
@@ -0,0 +1,392 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
type AncillaryFailureStage = 'heap-snapshot' | 'post-gc-probe' | 'profile-stop';
type PostGcOutcome =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: string;
};
interface FinalizationInput {
readonly finalizationKey: object;
readonly joinFinalSample: () => Promise<void>;
readonly probePostGc: () => Promise<PostGcOutcome>;
readonly reportAncillaryFailure: (
stage: AncillaryFailureStage,
error: unknown
) => void;
readonly stopProfile: () => Promise<void>;
readonly stopSampling: () => void;
readonly takeHeapSnapshot?: () => Promise<void>;
}
interface FinalizationApi {
finalize(input: FinalizationInput): Promise<PostGcOutcome>;
}
interface FinalizationModule {
createDatabaseWorkerPostGcFinalizationApi?: () => FinalizationApi;
}
const finalizationModulePromise = import(
new URL('./database-worker-post-gc-finalization.ts', import.meta.url).href
)
.then((module) => module as FinalizationModule)
.catch(() => null);
function deferred(): {
readonly promise: Promise<void>;
readonly resolve: () => void;
} {
let resolve!: () => void;
const promise = new Promise<void>((resolvePromise) => {
resolve = resolvePromise;
});
return { promise, resolve };
}
async function restoreSerializableApi(): Promise<FinalizationApi> {
const module = await finalizationModulePromise;
assert.ok(module, 'database worker post-GC finalization module must exist');
const factory = module.createDatabaseWorkerPostGcFinalizationApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => FinalizationApi;
return restoredFactory();
}
test('coordinates database worker post-GC finalization in order and single-flight', async () => {
const api = await restoreSerializableApi();
const finalSampleGate = deferred();
const events: string[] = [];
let probeCalls = 0;
const successfulPostGc = {
postGcHeapUsedBytes: 98_304,
unavailableReason: null,
} as const;
const input: FinalizationInput = {
finalizationKey: {},
joinFinalSample: async () => {
events.push('final-sample:start');
await finalSampleGate.promise;
events.push('final-sample:joined');
},
probePostGc: async () => {
probeCalls += 1;
events.push('post-gc-probe');
return successfulPostGc;
},
reportAncillaryFailure: () => {
assert.fail('the successful path has no ancillary failures');
},
stopProfile: async () => {
events.push('profile-stop');
},
stopSampling: () => {
events.push('sampling-stop');
},
takeHeapSnapshot: async () => {
events.push('heap-snapshot');
},
};
const firstFinalization = api.finalize(input);
const concurrentFinalization = api.finalize(input);
await Promise.resolve();
assert.deepEqual(events, ['sampling-stop', 'final-sample:start']);
assert.equal(probeCalls, 0);
finalSampleGate.resolve();
const [firstResult, concurrentResult] = await Promise.all([
firstFinalization,
concurrentFinalization,
]);
assert.deepEqual(events, [
'sampling-stop',
'final-sample:start',
'final-sample:joined',
'profile-stop',
'post-gc-probe',
'heap-snapshot',
]);
assert.equal(probeCalls, 1);
assert.deepEqual(firstResult, successfulPostGc);
assert.deepEqual(concurrentResult, successfulPostGc);
});
test('reports profile and snapshot failures without erasing successful post-GC', async () => {
const api = await restoreSerializableApi();
const events: string[] = [];
const profileError = new Error('profile-stop-failed');
const snapshotError = new Error('heap-snapshot-failed');
const failures: {
readonly error: unknown;
readonly stage: AncillaryFailureStage;
}[] = [];
const result = await api.finalize({
finalizationKey: {},
joinFinalSample: async () => {
events.push('final-sample:joined');
},
probePostGc: async () => {
events.push('post-gc-probe');
return {
postGcHeapUsedBytes: 131_072,
unavailableReason: null,
};
},
reportAncillaryFailure: (stage, error) => {
failures.push({ error, stage });
},
stopProfile: async () => {
events.push('profile-stop');
throw profileError;
},
stopSampling: () => {
events.push('sampling-stop');
},
takeHeapSnapshot: async () => {
events.push('heap-snapshot');
throw snapshotError;
},
});
assert.deepEqual(events, [
'sampling-stop',
'final-sample:joined',
'profile-stop',
'post-gc-probe',
'heap-snapshot',
]);
assert.deepEqual(failures, [
{ error: profileError, stage: 'profile-stop' },
{ error: snapshotError, stage: 'heap-snapshot' },
]);
assert.deepEqual(result, {
postGcHeapUsedBytes: 131_072,
unavailableReason: null,
});
});
test('fails closed when the post-GC probe throws without relabelling profile artifacts', async () => {
const api = await restoreSerializableApi();
const probeError = new Error('probe-failed');
const failures: {
readonly error: unknown;
readonly stage: AncillaryFailureStage;
}[] = [];
const result = await api.finalize({
finalizationKey: {},
joinFinalSample: async () => undefined,
probePostGc: async () => {
throw probeError;
},
reportAncillaryFailure: (stage, error) => {
failures.push({ error, stage });
},
stopProfile: async () => undefined,
stopSampling: () => undefined,
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'capture-failed',
});
assert.deepEqual(failures, [{ error: probeError, stage: 'post-gc-probe' }]);
});
test('the Electron main capture wires exact DB selection and explicit-GC finalization', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
assert.match(
source,
/databaseWorkerPostGcSelectionApiFactorySource:\s+createDatabaseWorkerPostGcSelectionApi\.toString\(\)/
);
assert.match(
source,
/databaseWorkerPostGcProbeApiFactorySource:\s+createDatabaseWorkerPostGcProbeApi\.toString\(\)/
);
assert.match(
source,
/databaseWorkerPostGcFinalizationApiFactorySource:\s+createDatabaseWorkerPostGcFinalizationApi\.toString\(\)/
);
assert.match(source, /new workerThreads\.MessageChannel\(\)/);
assert.match(
source,
/databaseWorkerPostGcSelectionApi\.select\(\s*currentWorkerRecords,\s*state\.captureGeneration\s*\)/
);
assert.match(source, /databaseWorkerPostGcFinalizationApi\s*\.finalize\(/);
assert.match(source, /pendingCount: 0/);
assert.match(source, /record\.pendingCount \+= 1/);
assert.match(
source,
/request\.record\.pendingCount = Math\.max\(\s*0,\s*request\.record\.pendingCount - 1\s*\)/
);
assert.match(source, /samplePromise/);
assert.match(source, /postGcHeapUnavailableReason/);
assert.match(source, /ordinal: nextWorkerOrdinal/);
assert.match(
source,
/`\$\{record\.kind\}-\$\{record\.ordinal\}\.cpuprofile`/
);
assert.doesNotMatch(source, /sampleBusy/);
assert.doesNotMatch(source, /const postSnapshot/);
});
test('main CPU profiling stops at the operation cutoff before worker artifact finalization', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const stopStart = source.indexOf('stop: async (');
const mainProfileStop = source.indexOf("'Profiler.stop'", stopStart);
const databaseSelection = source.indexOf(
'databaseWorkerPostGcSelectionApi.select',
stopStart
);
const databaseFinalization = source.indexOf(
'await finalizeDatabaseWorker',
stopStart
);
assert.ok(stopStart >= 0);
assert.ok(mainProfileStop > stopStart);
assert.ok(mainProfileStop < databaseSelection);
assert.ok(mainProfileStop < databaseFinalization);
});
test('worker CPU profile serialization runs only after the main CPU profiler stops', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const profileStopStart = source.indexOf('const stopWorkerProfile');
const profileStopEnd = source.indexOf(
'const writeWorkerProfile',
profileStopStart
);
const profileStop = source.slice(profileStopStart, profileStopEnd);
const stopStart = source.indexOf('stop: async (');
const mainProfileStop = source.indexOf("'Profiler.stop'", stopStart);
const workerProfileFlush = source.indexOf(
'flushWorkerProfiles(currentWorkerRecords)',
mainProfileStop
);
assert.match(profileStop, /const profileResult = await handle\.stop\(\)/);
assert.match(profileStop, /record\.profileResult = profileResult/);
assert.doesNotMatch(profileStop, /writeFileSync|JSON\.stringify/);
assert.ok(mainProfileStop > stopStart);
assert.ok(workerProfileFlush > mainProfileStop);
});
test('measured termination stays unperturbed while diagnostic capture finalizes its profile before termination', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const terminateStart = source.indexOf(
'WorkerClass.prototype.terminate = function'
);
const terminateEnd = source.indexOf('const inspectorPost', terminateStart);
const terminateBlock = source.slice(terminateStart, terminateEnd);
const terminatingFinalizerStart = source.indexOf(
'const finalizeTerminatingWorker'
);
const terminatingFinalizerEnd = source.indexOf(
'WorkerClass.prototype.postMessage',
terminatingFinalizerStart
);
const terminatingFinalizer = source.slice(
terminatingFinalizerStart,
terminatingFinalizerEnd
);
const measuredTerminate = terminateBlock.indexOf(
'const termination = originalTerminate.call(this)'
);
const measuredFinalization = terminateBlock.indexOf(
'void finalizeTerminatingWorker(record, false)'
);
const diagnosticFinalizationStart = terminateBlock.indexOf(
'void finalizeTerminatingWorker(record, true)'
);
const diagnosticFinalizationWait = terminateBlock.indexOf(
'await waitForTerminatingWorkerFinalization(record)'
);
const diagnosticTerminate = terminateBlock.indexOf(
'return originalTerminate.call(this)',
diagnosticFinalizationWait
);
assert.ok(
diagnosticFinalizationStart >= 0 &&
diagnosticFinalizationStart < diagnosticFinalizationWait &&
diagnosticFinalizationWait < diagnosticTerminate,
'diagnostic capture must start profile finalization, bound its wait, and then terminate the worker'
);
assert.ok(
measuredTerminate >= 0 && measuredTerminate < measuredFinalization,
'measured capture must initiate termination before best-effort finalization'
);
assert.doesNotMatch(terminatingFinalizer, /takeWorkerHeapSnapshot/);
assert.match(
terminatingFinalizer,
/stopWorkerProfile\(record, waitForProfileHandle\)/
);
assert.match(source, /resolvedProfileHandle/);
assert.match(source, /worker-profile-finalization-timeout/);
assert.match(source, /finalizationTimedOut/);
assert.match(source, /profileCaptureKey/);
assert.match(source, /record\.profileCaptureKey !== profileCaptureKey/);
});
test('worker capture failures use an artifact-neutral timeline label', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
assert.match(source, /worker-artifact-error:\$\{stage\}/);
assert.doesNotMatch(source, /worker-profile-error:\$\{stage\}/);
});
test('capture generation resets artifact paths and always emits current DB records', () => {
const source = readFileSync(
new URL('./m3u-refresh-main-capture.ts', import.meta.url),
'utf8'
);
const start = source.indexOf('const startCapture = async (');
const diagnostic = source.indexOf('if (state.diagnostic)', start);
const profileReset = source.indexOf('state.mainProfilePath = null', start);
const snapshotReset = source.indexOf(
'state.mainSnapshotPath = null',
start
);
assert.ok(profileReset > start && profileReset < diagnostic);
assert.ok(snapshotReset > start && snapshotReset < diagnostic);
assert.match(
source,
/record\.captureGeneration === state\.captureGeneration[\s\S]*record\.kind === 'database\.worker'/
);
});
@@ -0,0 +1,90 @@
import type { DatabaseWorkerPostGcProbeUnavailableReason } from './database-worker-post-gc-probe';
import type { DatabaseWorkerPostGcUnavailableReason } from './database-worker-post-gc-selection';
export type DatabaseWorkerPostGcAncillaryFailureStage =
'heap-snapshot' | 'post-gc-probe' | 'profile-stop' | 'profile-write';
export type DatabaseWorkerPostGcFinalizationOutcome =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason:
| DatabaseWorkerPostGcProbeUnavailableReason
| DatabaseWorkerPostGcUnavailableReason;
};
export interface DatabaseWorkerPostGcFinalizationInput {
readonly finalizationKey: object;
readonly joinFinalSample: () => Promise<void>;
readonly probePostGc: () => Promise<DatabaseWorkerPostGcFinalizationOutcome>;
readonly reportAncillaryFailure: (
stage: DatabaseWorkerPostGcAncillaryFailureStage,
error: unknown
) => void;
readonly stopProfile: () => Promise<void>;
readonly stopSampling: () => void;
readonly takeHeapSnapshot?: () => Promise<void>;
}
export interface DatabaseWorkerPostGcFinalizationApi {
finalize(
input: DatabaseWorkerPostGcFinalizationInput
): Promise<DatabaseWorkerPostGcFinalizationOutcome>;
}
export function createDatabaseWorkerPostGcFinalizationApi(): DatabaseWorkerPostGcFinalizationApi {
const finalizations = new WeakMap<
object,
Promise<DatabaseWorkerPostGcFinalizationOutcome>
>();
const helpers = {
async run(
input: DatabaseWorkerPostGcFinalizationInput
): Promise<DatabaseWorkerPostGcFinalizationOutcome> {
input.stopSampling();
await input.joinFinalSample();
try {
await input.stopProfile();
} catch (error: unknown) {
input.reportAncillaryFailure('profile-stop', error);
}
let outcome: DatabaseWorkerPostGcFinalizationOutcome;
try {
outcome = await input.probePostGc();
} catch (error: unknown) {
input.reportAncillaryFailure('post-gc-probe', error);
outcome = {
postGcHeapUsedBytes: null,
unavailableReason: 'capture-failed',
};
}
if (input.takeHeapSnapshot) {
try {
await input.takeHeapSnapshot();
} catch (error: unknown) {
input.reportAncillaryFailure('heap-snapshot', error);
}
}
return outcome;
},
finalize(
input: DatabaseWorkerPostGcFinalizationInput
): Promise<DatabaseWorkerPostGcFinalizationOutcome> {
const existing = finalizations.get(input.finalizationKey);
if (existing) {
return existing;
}
const finalization = helpers.run(input);
finalizations.set(input.finalizationKey, finalization);
return finalization;
},
};
return Object.freeze({ finalize: helpers.finalize });
}
@@ -0,0 +1,469 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
/* eslint-disable max-lines -- The one-shot transport contract keeps all terminal-path fixtures together. */
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
type WorkerUnavailableReason =
'capture-failed' | 'gc-unavailable' | 'profiling-disabled' | 'worker-busy';
type MainUnavailableReason =
| 'post-gc-probe-invalid-response'
| 'post-gc-probe-message-error'
| 'post-gc-probe-port-closed'
| 'post-gc-probe-post-failed'
| 'post-gc-probe-timeout';
type ProbeUnavailableReason = WorkerUnavailableReason | MainUnavailableReason;
type ProbeResult =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: ProbeUnavailableReason;
};
interface ProbePort {
close(): void;
off(event: 'close', listener: () => void): unknown;
off(event: 'message', listener: (message: unknown) => void): unknown;
off(event: 'messageerror', listener: (error: unknown) => void): unknown;
on(event: 'close', listener: () => void): unknown;
on(event: 'message', listener: (message: unknown) => void): unknown;
on(event: 'messageerror', listener: (error: unknown) => void): unknown;
postMessage(message: unknown): void;
start(): void;
}
interface ProbeWorker {
postMessage(message: unknown, transferList: readonly unknown[]): void;
}
interface ProbeTimers {
clearTimeout(handle: unknown): void;
setTimeout(callback: () => void, delayMs: number): unknown;
}
interface ProbeApi {
probe(input: {
readonly createMessageChannel: () => {
readonly port1: ProbePort;
readonly port2: ProbePort;
};
readonly timeoutMs?: number;
readonly timers?: ProbeTimers;
readonly worker: ProbeWorker;
}): Promise<ProbeResult>;
}
interface ProbeModule {
createDatabaseWorkerPostGcProbeApi?: () => ProbeApi;
}
interface ProductionWorkerPostGcModule {
DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON?: Readonly<
Record<string, WorkerUnavailableReason>
>;
}
const probeModulePromise = import(
new URL('./database-worker-post-gc-probe.ts', import.meta.url).href
)
.then((module) => module as ProbeModule)
.catch(() => null);
const productionWorkerPostGcModulePromise = import(
new URL(
'../../../electron-backend/src/app/workers/database-worker-post-gc-heap.ts',
import.meta.url
).href
)
.then((module) => module as ProductionWorkerPostGcModule)
.catch(() => null);
class FakePort extends EventEmitter implements ProbePort {
closeCalls = 0;
peer: FakePort | null = null;
startCalls = 0;
close(): void {
this.closeCalls += 1;
}
postMessage(message: unknown): void {
this.peer?.emit('message', message);
}
start(): void {
this.startCalls += 1;
}
}
class FakeTimers implements ProbeTimers {
readonly cleared: unknown[] = [];
readonly scheduled: {
readonly callback: () => void;
readonly delayMs: number;
readonly handle: number;
}[] = [];
private nextHandle = 1;
clearTimeout(handle: unknown): void {
this.cleared.push(handle);
}
fire(handle: number): void {
const timer = this.scheduled.find(
(candidate) => candidate.handle === handle
);
assert.ok(timer, `timer ${handle} must exist`);
timer.callback();
}
setTimeout(callback: () => void, delayMs: number): number {
const handle = this.nextHandle;
this.nextHandle += 1;
this.scheduled.push({ callback, delayMs, handle });
return handle;
}
}
interface ProbeHarness {
readonly createMessageChannel: () => {
readonly port1: FakePort;
readonly port2: FakePort;
};
readonly port1: FakePort;
readonly port2: FakePort;
readonly timers: FakeTimers;
}
function createProbeHarness(): ProbeHarness {
const port1 = new FakePort();
const port2 = new FakePort();
port1.peer = port2;
port2.peer = port1;
const timers = new FakeTimers();
return {
createMessageChannel: () => ({ port1, port2 }),
port1,
port2,
timers,
};
}
async function restoreSerializableApi(): Promise<ProbeApi> {
const module = await probeModulePromise;
assert.ok(module, 'database worker post-GC probe module must exist');
const factory = module.createDatabaseWorkerPostGcProbeApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => ProbeApi;
return restoredFactory();
}
function assertCoherentResult(result: ProbeResult): void {
const hasHeap =
Number.isSafeInteger(result.postGcHeapUsedBytes) &&
Number(result.postGcHeapUsedBytes) >= 0;
assert.equal(
hasHeap,
result.unavailableReason === null,
'probe result must preserve the heap/reason XOR'
);
}
function cleanupCounts(port: FakePort): Record<string, number> {
return {
close: port.listenerCount('close'),
message: port.listenerCount('message'),
messageerror: port.listenerCount('messageerror'),
};
}
test('serializes the factory and sends the exact one-shot worker request with its transfer port', async () => {
const api = await restoreSerializableApi();
const harness = createProbeHarness();
let postedMessage: unknown;
let postedTransferList: readonly unknown[] | null = null;
let terminateCalls = 0;
const worker = {
postMessage(message: unknown, transferList: readonly unknown[]): void {
postedMessage = message;
postedTransferList = transferList;
const responsePort = (
message as { readonly responsePort: ProbePort }
).responsePort;
responsePort.postMessage({
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 73_728,
unavailableReason: null,
});
},
terminate(): void {
terminateCalls += 1;
},
};
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker,
});
assert.deepEqual(postedMessage, {
type: 'performance:collect-post-gc-heap',
responsePort: harness.port2,
});
assert.deepEqual(postedTransferList, [harness.port2]);
assert.deepEqual(result, {
postGcHeapUsedBytes: 73_728,
unavailableReason: null,
});
assertCoherentResult(result);
assert.equal(harness.port1.startCalls, 1);
assert.equal(harness.port1.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
assert.deepEqual(
harness.timers.scheduled.map((timer) => timer.delayMs),
[5_000]
);
assert.deepEqual(harness.timers.cleared, [1]);
assert.equal(terminateCalls, 0);
});
test('preserves every coherent worker-side unavailable result', async () => {
const api = await restoreSerializableApi();
const productionModule = await productionWorkerPostGcModulePromise;
assert.ok(productionModule, 'production worker post-GC module must load');
const productionReasons =
productionModule.DATABASE_WORKER_POST_GC_HEAP_UNAVAILABLE_REASON;
assert.ok(productionReasons, 'production worker reasons must be exported');
const reasons = Object.values(productionReasons);
assert.deepEqual(
new Set(reasons),
new Set<WorkerUnavailableReason>([
'capture-failed',
'gc-unavailable',
'profiling-disabled',
'worker-busy',
])
);
for (const unavailableReason of reasons) {
const harness = createProbeHarness();
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(message: unknown): void {
(
message as { readonly responsePort: ProbePort }
).responsePort.postMessage({
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason,
});
},
},
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason,
});
assertCoherentResult(result);
}
});
test('fails closed for malformed or incoherent worker responses', async () => {
const api = await restoreSerializableApi();
const malformedResponses: readonly unknown[] = [
null,
[],
{
type: 'wrong-result',
postGcHeapUsedBytes: 1,
unavailableReason: null,
},
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 1,
unavailableReason: 'capture-failed',
},
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason: null,
},
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: null,
unavailableReason: 'unknown-reason',
},
...[-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY].map(
(postGcHeapUsedBytes) => ({
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes,
unavailableReason: null,
})
),
{
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: Number.MAX_SAFE_INTEGER + 1,
unavailableReason: null,
},
];
for (const response of malformedResponses) {
const harness = createProbeHarness();
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(message: unknown): void {
(
message as { readonly responsePort: ProbePort }
).responsePort.postMessage(response);
},
},
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'post-gc-probe-invalid-response',
});
assertCoherentResult(result);
}
});
test('times out once with an injectable deadline and ignores every later terminal signal', async () => {
const api = await restoreSerializableApi();
const harness = createProbeHarness();
let terminateCalls = 0;
const resultPromise = api.probe({
createMessageChannel: harness.createMessageChannel,
timeoutMs: 37,
timers: harness.timers,
worker: {
postMessage(): void {
// Keep the one-shot probe pending until its bounded deadline.
},
terminate(): void {
terminateCalls += 1;
},
},
});
const timer = harness.timers.scheduled[0];
assert.ok(timer);
assert.equal(timer.delayMs, 37);
harness.timers.fire(timer.handle);
const result = await resultPromise;
harness.port1.emit('message', {
type: 'performance:post-gc-heap-result',
postGcHeapUsedBytes: 99,
unavailableReason: null,
});
harness.port1.emit('messageerror', new Error('late message error'));
harness.port1.emit('close');
timer.callback();
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'post-gc-probe-timeout',
});
assertCoherentResult(result);
assert.equal(harness.port1.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
assert.deepEqual(harness.timers.cleared, [timer.handle]);
assert.equal(terminateCalls, 0);
});
test('maps message errors and an early response-port close to fixed reasons', async () => {
const api = await restoreSerializableApi();
const cases = [
{
emit(port: FakePort): void {
port.emit('messageerror', new Error('clone failed'));
},
reason: 'post-gc-probe-message-error',
},
{
emit(port: FakePort): void {
port.emit('close');
},
reason: 'post-gc-probe-port-closed',
},
] as const;
for (const testCase of cases) {
const harness = createProbeHarness();
const resultPromise = api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(): void {
testCase.emit(harness.port1);
},
},
});
const result = await resultPromise;
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: testCase.reason,
});
assertCoherentResult(result);
assert.equal(harness.port1.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
}
});
test('closes both ports and reports a fixed reason when the request cannot be posted', async () => {
const api = await restoreSerializableApi();
const harness = createProbeHarness();
const result = await api.probe({
createMessageChannel: harness.createMessageChannel,
timers: harness.timers,
worker: {
postMessage(): void {
throw new Error('worker already exited');
},
},
});
assert.deepEqual(result, {
postGcHeapUsedBytes: null,
unavailableReason: 'post-gc-probe-post-failed',
});
assertCoherentResult(result);
assert.equal(harness.port1.closeCalls, 1);
assert.equal(harness.port2.closeCalls, 1);
assert.deepEqual(cleanupCounts(harness.port1), {
close: 0,
message: 0,
messageerror: 0,
});
assert.deepEqual(harness.timers.cleared, [1]);
});
@@ -0,0 +1,242 @@
export type DatabaseWorkerPostGcProbeUnavailableReason =
| 'capture-failed'
| 'gc-unavailable'
| 'post-gc-probe-invalid-response'
| 'post-gc-probe-message-error'
| 'post-gc-probe-port-closed'
| 'post-gc-probe-post-failed'
| 'post-gc-probe-timeout'
| 'profiling-disabled'
| 'worker-busy';
export type DatabaseWorkerPostGcProbeResult =
| {
readonly postGcHeapUsedBytes: number;
readonly unavailableReason: null;
}
| {
readonly postGcHeapUsedBytes: null;
readonly unavailableReason: DatabaseWorkerPostGcProbeUnavailableReason;
};
export interface DatabaseWorkerPostGcProbePort {
close(): void;
off(event: 'close', listener: () => void): unknown;
off(event: 'message', listener: (message: unknown) => void): unknown;
off(event: 'messageerror', listener: (error: unknown) => void): unknown;
on(event: 'close', listener: () => void): unknown;
on(event: 'message', listener: (message: unknown) => void): unknown;
on(event: 'messageerror', listener: (error: unknown) => void): unknown;
start(): void;
}
export interface DatabaseWorkerPostGcProbeWorker {
postMessage(message: unknown, transferList: readonly unknown[]): void;
}
export interface DatabaseWorkerPostGcProbeTimers {
clearTimeout(handle: unknown): void;
setTimeout(callback: () => void, delayMs: number): unknown;
}
export interface DatabaseWorkerPostGcProbeInput {
readonly createMessageChannel: () => {
readonly port1: DatabaseWorkerPostGcProbePort;
readonly port2: DatabaseWorkerPostGcProbePort;
};
readonly timeoutMs?: number;
readonly timers?: DatabaseWorkerPostGcProbeTimers;
readonly worker: DatabaseWorkerPostGcProbeWorker;
}
export interface DatabaseWorkerPostGcProbeApi {
probe(
input: DatabaseWorkerPostGcProbeInput
): Promise<DatabaseWorkerPostGcProbeResult>;
}
export function createDatabaseWorkerPostGcProbeApi(): DatabaseWorkerPostGcProbeApi {
type JsonRecord = Record<string, unknown>;
const DEFAULT_TIMEOUT_MS = 5_000;
const REQUEST_TYPE = 'performance:collect-post-gc-heap';
const RESULT_TYPE = 'performance:post-gc-heap-result';
const workerUnavailableReasons = new Set([
'capture-failed',
'gc-unavailable',
'profiling-disabled',
'worker-busy',
]);
const defaultTimers: DatabaseWorkerPostGcProbeTimers = {
clearTimeout(handle: unknown): void {
globalThis.clearTimeout(
handle as ReturnType<typeof globalThis.setTimeout>
);
},
setTimeout(callback: () => void, delayMs: number): unknown {
return globalThis.setTimeout(callback, delayMs);
},
};
const helpers = {
unavailable(
unavailableReason: DatabaseWorkerPostGcProbeUnavailableReason
): DatabaseWorkerPostGcProbeResult {
return Object.freeze({
postGcHeapUsedBytes: null,
unavailableReason,
});
},
normalizeResponse(response: unknown): DatabaseWorkerPostGcProbeResult {
if (
typeof response !== 'object' ||
response === null ||
Array.isArray(response)
) {
return helpers.unavailable('post-gc-probe-invalid-response');
}
const candidate = response as JsonRecord;
const postGcHeapUsedBytes = candidate['postGcHeapUsedBytes'];
const unavailableReason = candidate['unavailableReason'];
if (
candidate['type'] === RESULT_TYPE &&
Number.isSafeInteger(postGcHeapUsedBytes) &&
Number(postGcHeapUsedBytes) >= 0 &&
unavailableReason === null
) {
return Object.freeze({
postGcHeapUsedBytes: Number(postGcHeapUsedBytes),
unavailableReason: null,
});
}
if (
candidate['type'] === RESULT_TYPE &&
postGcHeapUsedBytes === null &&
typeof unavailableReason === 'string' &&
workerUnavailableReasons.has(unavailableReason)
) {
return helpers.unavailable(
unavailableReason as DatabaseWorkerPostGcProbeUnavailableReason
);
}
return helpers.unavailable('post-gc-probe-invalid-response');
},
probe(
input: DatabaseWorkerPostGcProbeInput
): Promise<DatabaseWorkerPostGcProbeResult> {
let channel: ReturnType<typeof input.createMessageChannel>;
try {
channel = input.createMessageChannel();
} catch {
return Promise.resolve(
helpers.unavailable('post-gc-probe-post-failed')
);
}
const { port1, port2 } = channel;
const timers = input.timers ?? defaultTimers;
const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS;
return new Promise((resolve) => {
let settled = false;
let timerHandle: unknown;
let timerScheduled = false;
const callbacks = {
cleanup(): void {
try {
port1.off('message', callbacks.onMessage);
} catch {
// Best-effort profiling cleanup must not escape.
}
try {
port1.off('messageerror', callbacks.onMessageError);
} catch {
// Best-effort profiling cleanup must not escape.
}
try {
port1.off('close', callbacks.onClose);
} catch {
// Best-effort profiling cleanup must not escape.
}
if (timerScheduled) {
try {
timers.clearTimeout(timerHandle);
} catch {
// Best-effort profiling cleanup must not escape.
}
}
try {
port1.close();
} catch {
// The one-shot response port may already be closed.
}
},
settle(result: DatabaseWorkerPostGcProbeResult): void {
if (settled) {
return;
}
settled = true;
callbacks.cleanup();
resolve(result);
},
onClose(): void {
callbacks.settle(
helpers.unavailable('post-gc-probe-port-closed')
);
},
onMessage(message: unknown): void {
callbacks.settle(helpers.normalizeResponse(message));
},
onMessageError(): void {
callbacks.settle(
helpers.unavailable('post-gc-probe-message-error')
);
},
onTimeout(): void {
callbacks.settle(
helpers.unavailable('post-gc-probe-timeout')
);
},
};
try {
port1.on('message', callbacks.onMessage);
port1.on('messageerror', callbacks.onMessageError);
port1.on('close', callbacks.onClose);
port1.start();
timerHandle = timers.setTimeout(
callbacks.onTimeout,
timeoutMs
);
timerScheduled = true;
input.worker.postMessage(
{
type: REQUEST_TYPE,
responsePort: port2,
},
[port2]
);
} catch {
callbacks.settle(
helpers.unavailable('post-gc-probe-post-failed')
);
try {
port2.close();
} catch {
// A synchronous transfer failure may already close it.
}
}
});
},
};
return Object.freeze({ probe: helpers.probe });
}
@@ -0,0 +1,131 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import test from 'node:test';
import {
type CancellationBenchmarkManifest,
type CancellationIterationResult,
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
type WorkerCaptureMetrics,
} from './m3u-refresh-cancellation-contract';
import { createCancellationBenchmarkSummary } from './m3u-refresh-cancellation-report';
function databaseWorker(
postGcHeapUsedBytes: number | null,
postGcHeapUnavailableReason: string | null
): WorkerCaptureMetrics {
return {
kind: PERFORMANCE_WORKER_KIND.DATABASE,
peakExternalBytes: 0,
peakHeapUsedBytes: 0,
postGcHeapUnavailableReason,
postGcHeapUsedBytes,
requests: [],
} as unknown as WorkerCaptureMetrics;
}
function measuredIteration(
workers: readonly WorkerCaptureMetrics[]
): CancellationIterationResult {
return {
cancellationEffectObserved: true,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: {
eventLoopDelay: { maxMs: 0, p95Ms: 0, p99Ms: 0 },
eventLoopUtilization: null,
memory: {
peakHeapUsedBytes: 0,
peakRssBytes: 0,
postGcHeapUsedBytes: null,
postGcRssBytes: null,
},
rendererWindow: {
responsiveEvents: 0,
rss: {
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 2_048,
unavailableReason: null,
validSampleCount: 1,
},
unresponsiveEvents: 0,
windowIdentity: {
browserWindowId: 7,
webContentsId: 11,
},
},
workers,
},
phases: {},
renderer: {
peakHeapUsedBytes: 0,
postGcHeapUsedBytes: null,
probe: {
frameGapsMs: [],
heartbeatDelaysMs: [],
longTasksMs: [],
},
},
runId: 'measured',
} as unknown as CancellationIterationResult;
}
test('summary includes every database isolate instead of silently taking the first', () => {
const unavailable = databaseWorker(null, 'post-gc-probe-invalid-response');
const summary = createCancellationBenchmarkSummary(
{} as CancellationBenchmarkManifest,
[
measuredIteration([
databaseWorker(100, null),
databaseWorker(300, null),
unavailable,
]),
]
);
assert.deepEqual(summary.measured.databaseWorkerPostGcHeapBytes, {
count: 2,
max: 300,
mean: 200,
median: 200,
min: 100,
p95: 290,
p99: 298,
});
assert.deepEqual(
(
summary as unknown as {
readonly validity: {
readonly databaseWorkerPostGc: unknown;
};
}
).validity.databaseWorkerPostGc,
{
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 3,
reason: 'database-worker-unexpected-activity',
runId: 'measured',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
}
);
assert.equal(
(
summary.iterations[0]?.main.workers[2] as WorkerCaptureMetrics & {
readonly postGcHeapUnavailableReason: string | null;
}
)?.postGcHeapUnavailableReason,
'post-gc-probe-invalid-response'
);
});
@@ -0,0 +1,145 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import test from 'node:test';
type WorkerKind = 'database.worker' | 'playlist-refresh.worker';
interface TestWorkerRecord {
readonly captureGeneration: number | null;
readonly finalized: boolean;
readonly kind: WorkerKind;
readonly ordinal: number;
readonly pendingCount: number;
readonly sampleTimer: object | null;
}
type SelectionReason =
| 'database-worker-missing'
| 'database-worker-not-idle'
| 'multiple-database-workers';
interface Selection<T> {
readonly selected: T | null;
readonly unavailableReason: SelectionReason | null;
}
type Selector = <T extends TestWorkerRecord>(
records: readonly T[],
activeGeneration: number
) => Selection<T>;
interface SelectionModule {
createDatabaseWorkerPostGcSelectionApi?: () => {
readonly select: Selector;
};
}
const selectionModulePromise = import(
new URL('./database-worker-post-gc-selection.ts', import.meta.url).href
)
.then((module) => module as SelectionModule)
.catch(() => null);
function workerRecord(
ordinal: number,
overrides: Partial<TestWorkerRecord> = {}
): TestWorkerRecord {
return {
captureGeneration: 7,
finalized: true,
kind: 'database.worker',
ordinal,
pendingCount: 0,
sampleTimer: null,
...overrides,
};
}
async function restoreSerializableSelector(): Promise<Selector> {
const module = await selectionModulePromise;
assert.ok(module, 'database worker post-GC selector module must exist');
const factory = module.createDatabaseWorkerPostGcSelectionApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
assert.doesNotMatch(source, /__name/);
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => {
readonly select: Selector;
};
return restoredFactory().select;
}
test('selects the only current-generation database worker independently of sampling and finalization state', async () => {
const select = await restoreSerializableSelector();
const currentDatabase = workerRecord(4, {
finalized: true,
sampleTimer: null,
});
const previousDatabase = workerRecord(1, {
captureGeneration: 6,
finalized: false,
sampleTimer: {},
});
const currentPlaylist = workerRecord(2, {
finalized: false,
kind: 'playlist-refresh.worker',
sampleTimer: {},
});
const selection = select(
[previousDatabase, currentPlaylist, currentDatabase],
7
);
assert.equal(selection.selected, currentDatabase);
assert.deepEqual(selection, {
selected: currentDatabase,
unavailableReason: null,
});
});
test('reports a missing database worker after ignoring playlists and previous generations', async () => {
const select = await restoreSerializableSelector();
assert.deepEqual(
select(
[
workerRecord(1, { captureGeneration: 6 }),
workerRecord(2, { kind: 'playlist-refresh.worker' }),
],
7
),
{
selected: null,
unavailableReason: 'database-worker-missing',
}
);
});
test('fails closed for multiple current-generation database workers instead of taking the first', async () => {
const select = await restoreSerializableSelector();
const first = workerRecord(1, {
finalized: false,
sampleTimer: {},
});
const second = workerRecord(2, {
finalized: true,
sampleTimer: null,
});
assert.deepEqual(select([first, second], 7), {
selected: null,
unavailableReason: 'multiple-database-workers',
});
});
test('fails closed while the only current-generation database worker is not idle', async () => {
const select = await restoreSerializableSelector();
assert.deepEqual(select([workerRecord(3, { pendingCount: 2 })], 7), {
selected: null,
unavailableReason: 'database-worker-not-idle',
});
});
@@ -0,0 +1,70 @@
export type DatabaseWorkerPostGcUnavailableReason =
| 'database-worker-missing'
| 'database-worker-not-idle'
| 'multiple-database-workers';
export interface DatabaseWorkerPostGcSelectionRecord {
readonly captureGeneration: number | null;
readonly kind: string;
readonly ordinal: number;
readonly pendingCount: number;
}
export interface DatabaseWorkerPostGcSelection<T> {
readonly selected: T | null;
readonly unavailableReason: DatabaseWorkerPostGcUnavailableReason | null;
}
export interface DatabaseWorkerPostGcSelectionApi {
select<T extends DatabaseWorkerPostGcSelectionRecord>(
records: readonly T[],
activeGeneration: number
): DatabaseWorkerPostGcSelection<T>;
}
export function createDatabaseWorkerPostGcSelectionApi(): DatabaseWorkerPostGcSelectionApi {
const helpers = {
select<T extends DatabaseWorkerPostGcSelectionRecord>(
records: readonly T[],
activeGeneration: number
): DatabaseWorkerPostGcSelection<T> {
const currentDatabaseWorkers: T[] = [];
for (const record of records) {
if (
record.captureGeneration === activeGeneration &&
record.kind === 'database.worker'
) {
currentDatabaseWorkers.push(record);
}
}
if (currentDatabaseWorkers.length === 0) {
return Object.freeze({
selected: null,
unavailableReason: 'database-worker-missing',
});
}
if (currentDatabaseWorkers.length > 1) {
return Object.freeze({
selected: null,
unavailableReason: 'multiple-database-workers',
});
}
const selected = currentDatabaseWorkers[0] as T;
if (selected.pendingCount > 0) {
return Object.freeze({
selected: null,
unavailableReason: 'database-worker-not-idle',
});
}
return Object.freeze({
selected,
unavailableReason: null,
});
},
};
return Object.freeze({ select: helpers.select });
}
@@ -0,0 +1,392 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
type IterationKind = 'diagnostic' | 'measured' | 'warmup';
type WorkerKind = 'database.worker' | 'playlist-refresh.worker';
interface TestWorkerCapture {
readonly kind: WorkerKind;
readonly postGcHeapUnavailableReason: string | null;
readonly postGcHeapUsedBytes: number | null;
}
interface TestIteration {
readonly cancellationEffectObserved: boolean;
readonly kind: IterationKind;
readonly main: {
readonly timeline: readonly {
readonly type: string;
}[];
readonly workers: readonly TestWorkerCapture[];
};
readonly runId: string;
}
interface InvalidMeasuredRun {
readonly databaseWorkerCount: number;
readonly reason: string;
readonly runId: string;
}
interface DatabaseWorkerPostGcValidity {
readonly applicableMeasuredRunCount: number;
readonly invalidMeasuredRuns: readonly InvalidMeasuredRun[];
readonly measuredRunCount: number;
readonly notApplicableMeasuredRuns: readonly {
readonly reason: string;
readonly runId: string;
}[];
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
interface ValidityModule {
assessDatabaseWorkerPostGcValidity?: (
iterations: readonly TestIteration[]
) => DatabaseWorkerPostGcValidity;
}
const validityModulePromise = import(
new URL('./database-worker-post-gc-validity.ts', import.meta.url).href
)
.then((module) => module as ValidityModule)
.catch(() => null);
function worker(
postGcHeapUsedBytes: number | null,
postGcHeapUnavailableReason: string | null,
kind: WorkerKind = 'database.worker'
): TestWorkerCapture {
return {
kind,
postGcHeapUnavailableReason,
postGcHeapUsedBytes,
};
}
function iteration(
runId: string,
kind: IterationKind,
workers: readonly TestWorkerCapture[],
timeline: readonly { readonly type: string }[] = [],
cancellationEffectObserved = false
): TestIteration {
return {
cancellationEffectObserved,
kind,
main: { timeline, workers },
runId,
};
}
test('validates database post-GC heap per measured iteration without losing raw reasons', async () => {
const module = await validityModulePromise;
assert.ok(module, 'database worker post-GC validity helper must exist');
const assess = module.assessDatabaseWorkerPostGcValidity;
assert.equal(typeof assess, 'function');
const valid = assess([
iteration('warmup-broken', 'warmup', []),
iteration('diagnostic-broken', 'diagnostic', [
worker(null, 'gc-unavailable'),
worker(100, null),
]),
iteration('measured-valid', 'measured', [
worker(4_096, null),
worker(
null,
'worker-force-terminated-before-gc',
'playlist-refresh.worker'
),
]),
]);
assert.deepEqual(valid, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: true,
validForComparison: true,
validMeasuredRunCount: 1,
});
const compensatingCardinality = assess([
iteration('measured-duplicate', 'measured', [
worker(1_024, null),
worker(2_048, null),
]),
iteration(
'measured-missing',
'measured',
[worker(3_072, null, 'playlist-refresh.worker')],
[{ type: 'db-request' }]
),
]);
assert.deepEqual(compensatingCardinality, {
applicableMeasuredRunCount: 2,
invalidMeasuredRuns: [
{
databaseWorkerCount: 2,
reason: 'multiple-database-workers',
runId: 'measured-duplicate',
},
{
databaseWorkerCount: 0,
reason: 'database-worker-missing',
runId: 'measured-missing',
},
],
measuredRunCount: 2,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
const unavailableWorker = worker(null, 'post-gc-probe-timeout');
const unavailableAndIncoherent = assess([
iteration('measured-unavailable', 'measured', [unavailableWorker]),
iteration('measured-incoherent', 'measured', [
worker(8_192, 'gc-unavailable'),
]),
]);
assert.deepEqual(unavailableAndIncoherent, {
applicableMeasuredRunCount: 2,
invalidMeasuredRuns: [
{
databaseWorkerCount: 1,
reason: 'post-gc-probe-timeout',
runId: 'measured-unavailable',
},
{
databaseWorkerCount: 1,
reason: 'post-gc-capture-invalid',
runId: 'measured-incoherent',
},
],
measuredRunCount: 2,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
assert.equal(
unavailableWorker.postGcHeapUnavailableReason,
'post-gc-probe-timeout'
);
});
test('marks parsing cancellation before persistence as DB N/A without accepting late or missing captured DB work', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessDatabaseWorkerPostGcValidity;
assert.equal(typeof assess, 'function');
const noDatabasePhase = assess([
iteration(
'run-01',
'measured',
[
worker(
null,
'worker-force-terminated-before-gc',
'playlist-refresh.worker'
),
],
[],
true
),
iteration(
'run-02',
'measured',
[
worker(
null,
'worker-force-terminated-before-gc',
'playlist-refresh.worker'
),
],
[],
true
),
]);
assert.deepEqual(noDatabasePhase, {
applicableMeasuredRunCount: 0,
invalidMeasuredRuns: [],
measuredRunCount: 2,
notApplicableMeasuredRuns: [
{
reason: 'operation-cancelled-before-database-phase',
runId: 'run-01',
},
{
reason: 'operation-cancelled-before-database-phase',
runId: 'run-02',
},
],
validForBenchmark: true,
validForComparison: false,
validMeasuredRunCount: 0,
});
const mixedApplicability = assess([
iteration('run-cancelled', 'measured', [], [], true),
iteration('run-persisted', 'measured', [worker(4_096, null)]),
]);
assert.deepEqual(mixedApplicability, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [],
measuredRunCount: 2,
notApplicableMeasuredRuns: [
{
reason: 'operation-cancelled-before-database-phase',
runId: 'run-cancelled',
},
],
validForBenchmark: true,
validForComparison: false,
validMeasuredRunCount: 1,
});
const lateDatabaseWork = assess([
iteration(
'run-late',
'measured',
[],
[{ type: 'db-request-after-capture-cutoff' }],
true
),
]);
assert.deepEqual(lateDatabaseWork, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 0,
reason: 'database-worker-activity-after-cutoff',
runId: 'run-late',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
const unrelatedDatabaseWorker = assess([
iteration(
'run-unrelated-db',
'measured',
[worker(4_096, null)],
[{ type: 'db-request' }],
true
),
]);
assert.deepEqual(unrelatedDatabaseWorker, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 1,
reason: 'database-worker-unexpected-activity',
runId: 'run-unrelated-db',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
const nonCancelLateRequest = assess([
iteration(
'run-non-cancel-late',
'measured',
[worker(8_192, null)],
[{ type: 'db-request-after-capture-cutoff' }]
),
]);
assert.deepEqual(nonCancelLateRequest, {
applicableMeasuredRunCount: 1,
invalidMeasuredRuns: [
{
databaseWorkerCount: 1,
reason: 'database-worker-activity-after-cutoff',
runId: 'run-non-cancel-late',
},
],
measuredRunCount: 1,
notApplicableMeasuredRuns: [],
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
});
test('the benchmark preserves raw artifacts before rejecting an invalid formal comparison', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const validityGuard = source.indexOf(
'summary.validity.databaseWorkerPostGc.validForBenchmark'
);
const cancellationEffectGuard = source.indexOf(
'summary.cancellationEffectRate !== 1'
);
assert.ok(summaryWrite >= 0, 'summary artifact write must exist');
assert.ok(
cancellationEffectGuard >= 0,
'formal cancellation-effect guard must exist'
);
assert.ok(validityGuard >= 0, 'formal validity guard must exist');
assert.ok(
summaryWrite < cancellationEffectGuard && summaryWrite < validityGuard,
'raw summary must be durable before either formal guard fails'
);
assert.match(source, /Cancellation effect was not observed/);
assert.match(source, /Database worker post-GC capture is invalid/);
});
test('the benchmark persists the real seed DB lifecycle and atomically arms the measured generation', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const seedCaptureStart = source.indexOf(
'await startMainCapture(app.electronApp',
source.indexOf('const rendererWindowIdentity')
);
const seedPlaylist = source.indexOf('await seedPlaylist');
const seedSettlement = source.indexOf('await waitForSeedSettlement');
const seedRollover = source.indexOf(
'await rolloverMainCapture(app.electronApp',
seedSettlement
);
const seedArtifactWrite = source.indexOf(
"'seed-main-capture.json'",
seedRollover
);
assert.ok(seedCaptureStart >= 0 && seedCaptureStart < seedPlaylist);
assert.ok(seedPlaylist < seedSettlement);
assert.ok(seedSettlement < seedRollover);
assert.ok(seedRollover < seedArtifactWrite);
assert.doesNotMatch(
source.slice(seedRollover + 1, source.indexOf('const renderer =')),
/await startMainCapture\(app\.electronApp/
);
assert.match(source, /seedRollover\.nextCaptureStarted/);
assert.match(source, /seedRollover\.nextCaptureUnavailableReason/);
assert.match(
source,
/status\.databaseRequests\s*>\s*0[\s\S]*status\.databaseUpsertsCompleted\s*>\s*0[\s\S]*status\.databasePending\s*===\s*0/
);
});
@@ -0,0 +1,141 @@
import {
type DatabaseWorkerPostGcValidity,
type InvalidDatabaseWorkerPostGcMeasuredRun,
WORKER_POST_GC_HEAP_UNAVAILABLE_REASON,
} from './m3u-refresh-cancellation-contract';
export interface DatabaseWorkerPostGcValidityWorker {
readonly kind: string;
readonly postGcHeapUnavailableReason: unknown;
readonly postGcHeapUsedBytes: unknown;
}
export interface DatabaseWorkerPostGcValidityIteration {
readonly cancellationEffectObserved?: boolean;
readonly kind: string;
readonly main: {
readonly timeline?: readonly {
readonly type: string;
}[];
readonly workers: readonly DatabaseWorkerPostGcValidityWorker[];
};
readonly runId: string;
}
const UNAVAILABLE_REASONS = new Set<string>(
Object.values(WORKER_POST_GC_HEAP_UNAVAILABLE_REASON)
);
export function assessDatabaseWorkerPostGcValidity(
iterations: readonly DatabaseWorkerPostGcValidityIteration[]
): DatabaseWorkerPostGcValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === 'measured'
);
const invalidMeasuredRuns: InvalidDatabaseWorkerPostGcMeasuredRun[] = [];
const notApplicableMeasuredRuns: {
readonly reason: 'operation-cancelled-before-database-phase';
readonly runId: string;
}[] = [];
let applicableMeasuredRunCount = 0;
let validMeasuredRunCount = 0;
for (const iteration of measured) {
const databaseWorkers = iteration.main.workers.filter(
(worker) => worker.kind === 'database.worker'
);
const timeline = iteration.main.timeline ?? [];
const lateDatabaseRequest = timeline.some(
(record) => record.type === 'db-request-after-capture-cutoff'
);
const databasePhaseObserved =
lateDatabaseRequest ||
timeline.some((record) => record.type === 'db-request');
if (lateDatabaseRequest) {
applicableMeasuredRunCount += 1;
invalidMeasuredRuns.push({
databaseWorkerCount: databaseWorkers.length,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.DATABASE_WORKER_ACTIVITY_AFTER_CUTOFF,
runId: iteration.runId,
});
continue;
}
if (iteration.cancellationEffectObserved === true) {
if (!databasePhaseObserved && databaseWorkers.length === 0) {
notApplicableMeasuredRuns.push({
reason: 'operation-cancelled-before-database-phase',
runId: iteration.runId,
});
continue;
}
applicableMeasuredRunCount += 1;
invalidMeasuredRuns.push({
databaseWorkerCount: databaseWorkers.length,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.DATABASE_WORKER_UNEXPECTED_ACTIVITY,
runId: iteration.runId,
});
continue;
}
if (databaseWorkers.length === 0) {
applicableMeasuredRunCount += 1;
invalidMeasuredRuns.push({
databaseWorkerCount: 0,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.DATABASE_WORKER_MISSING,
runId: iteration.runId,
});
continue;
}
applicableMeasuredRunCount += 1;
if (databaseWorkers.length !== 1) {
invalidMeasuredRuns.push({
databaseWorkerCount: databaseWorkers.length,
reason: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.MULTIPLE_DATABASE_WORKERS,
runId: iteration.runId,
});
continue;
}
const worker = databaseWorkers[0] as DatabaseWorkerPostGcValidityWorker;
const heap = worker.postGcHeapUsedBytes;
const reason = worker.postGcHeapUnavailableReason;
if (
Number.isSafeInteger(heap) &&
Number(heap) >= 0 &&
reason === null
) {
validMeasuredRunCount += 1;
continue;
}
invalidMeasuredRuns.push({
databaseWorkerCount: 1,
reason:
heap === null &&
typeof reason === 'string' &&
UNAVAILABLE_REASONS.has(reason)
? reason
: WORKER_POST_GC_HEAP_UNAVAILABLE_REASON.INVALID_CAPTURE,
runId: iteration.runId,
});
}
return Object.freeze({
applicableMeasuredRunCount,
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
notApplicableMeasuredRuns: Object.freeze(notApplicableMeasuredRuns),
validForBenchmark:
measured.length > 0 && invalidMeasuredRuns.length === 0,
validForComparison:
measured.length > 0 &&
applicableMeasuredRunCount === measured.length &&
validMeasuredRunCount === measured.length &&
invalidMeasuredRuns.length === 0,
validMeasuredRunCount,
});
}
@@ -0,0 +1,127 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
assessDatabaseWorkerRequestMetricsValidity,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON,
} from './database-worker-request-metrics-validity';
import { createCompleteTestMainCapture } from './m3u-import-report.test-helpers';
import type {
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
describe('database worker request-metrics validity', () => {
it('requires exactly two complete request samples in every measured run', () => {
const worker = completeWorker();
const validity = assessDatabaseWorkerRequestMetricsValidity([
iteration('warmup', 'warmup', [worker]),
iteration('run-01', 'measured', [worker]),
iteration('run-02', 'measured', [worker]),
iteration('diagnostic', 'diagnostic', [worker]),
]);
assert.deepEqual(validity, {
expectedRequestCount: 4,
invalidRequests: [],
measuredRunCount: 2,
validForComparison: true,
validMeasuredRunCount: 2,
validRequestCount: 4,
});
});
it('fails each measured run with missing ELD, ELU, CPU, or raw capture metrics', () => {
const validity = assessDatabaseWorkerRequestMetricsValidity([
invalidRequestIteration('run-eld', {
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
'event-loop-delay-capture-unavailable',
histogramFlushedEpochMs: null,
}),
invalidRequestIteration('run-elu', {
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
'event-loop-utilization-unavailable',
}),
invalidRequestIteration('run-cpu', {
threadCpuSystemMicros: null,
threadCpuUnavailableReason: 'thread-cpu-usage-unavailable',
threadCpuUserMicros: null,
}),
invalidRequestIteration('run-capture', {
performanceCaptureUnavailableReason:
'worker-performance-capture-invalid',
}),
]);
assert.equal(validity.expectedRequestCount, 8);
assert.equal(validity.validRequestCount, 4);
assert.equal(validity.validMeasuredRunCount, 0);
assert.equal(validity.validForComparison, false);
assert.deepEqual(
validity.invalidRequests.map(({ reason }) => reason),
[
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_DELAY_INVALID,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_UTILIZATION_INVALID,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.THREAD_CPU_INVALID,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.WORKER_CAPTURE_INVALID,
]
);
});
it('fails closed for missing, multiple, and wrong request sets', () => {
const worker = completeWorker();
const validity = assessDatabaseWorkerRequestMetricsValidity([
iteration('missing', 'measured', []),
iteration('multiple', 'measured', [worker, worker]),
iteration('wrong-requests', 'measured', [
{
...worker,
requests: worker.requests.slice(0, 1),
},
]),
]);
assert.equal(validity.expectedRequestCount, 6);
assert.equal(validity.validRequestCount, 0);
assert.equal(validity.validForComparison, false);
assert.deepEqual(
validity.invalidRequests.map(({ reason }) => reason),
[
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.DATABASE_WORKER_MISSING,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.REQUEST_SET_INVALID,
]
);
});
});
function completeWorker(): WorkerCaptureMetrics {
const worker = createCompleteTestMainCapture().workers[0];
assert.ok(worker);
return worker;
}
function invalidRequestIteration(
runId: string,
updates: Partial<WorkerRequestPerformanceMetrics>
) {
const worker = completeWorker();
const get = worker.requests[1];
assert.ok(get);
return iteration(runId, 'measured', [
{
...worker,
requests: [worker.requests[0], { ...get, ...updates }],
},
]);
}
function iteration(
runId: string,
kind: string,
workers: readonly WorkerCaptureMetrics[]
) {
return { kind, main: { workers }, runId };
}
@@ -0,0 +1,197 @@
import type {
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
} from './m3u-refresh-cancellation-contract';
export const DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON = {
DATABASE_WORKER_MISSING: 'database-worker-missing',
EVENT_LOOP_DELAY_INVALID: 'event-loop-delay-invalid',
EVENT_LOOP_UTILIZATION_INVALID: 'event-loop-utilization-invalid',
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
REQUEST_SET_INVALID: 'initial-import-request-set-invalid',
THREAD_CPU_INVALID: 'thread-cpu-invalid',
WORKER_CAPTURE_INVALID: 'worker-performance-capture-invalid',
} as const;
export type DatabaseWorkerRequestMetricsInvalidReason =
(typeof DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON)[keyof typeof DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON];
export interface DatabaseWorkerRequestMetricsValidity {
readonly expectedRequestCount: number;
readonly invalidRequests: readonly {
readonly operation: string | null;
readonly reason: DatabaseWorkerRequestMetricsInvalidReason;
readonly runId: string;
}[];
readonly measuredRunCount: number;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
readonly validRequestCount: number;
}
interface RequestMetricsIteration {
readonly kind: string;
readonly main: {
readonly workers: readonly WorkerCaptureMetrics[];
};
readonly runId: string;
}
export function assessDatabaseWorkerRequestMetricsValidity(
iterations: readonly RequestMetricsIteration[]
): DatabaseWorkerRequestMetricsValidity {
const measured = iterations.filter(
(iteration) =>
iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
);
const invalidRequests: {
operation: string | null;
reason: DatabaseWorkerRequestMetricsInvalidReason;
runId: string;
}[] = [];
let validMeasuredRunCount = 0;
let validRequestCount = 0;
for (const iteration of measured) {
const workers = iteration.main.workers.filter(
(worker) => worker.kind === PERFORMANCE_WORKER_KIND.DATABASE
);
if (workers.length === 0) {
invalidRequests.push({
operation: null,
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.DATABASE_WORKER_MISSING,
runId: iteration.runId,
});
continue;
}
const worker = workers.length === 1 ? workers[0] : null;
if (!worker) {
invalidRequests.push({
operation: null,
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.MULTIPLE_DATABASE_WORKERS,
runId: iteration.runId,
});
continue;
}
const requests = requireInitialImportRequestPair(worker);
if (requests === null) {
invalidRequests.push({
operation: null,
reason: DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.REQUEST_SET_INVALID,
runId: iteration.runId,
});
continue;
}
let runIsValid = true;
for (const request of requests) {
const reason = assessRequest(request);
if (reason === null) {
validRequestCount += 1;
} else {
runIsValid = false;
invalidRequests.push({
operation: request.operation,
reason,
runId: iteration.runId,
});
}
}
if (runIsValid) {
validMeasuredRunCount += 1;
}
}
const expectedRequestCount = measured.length * 2;
return Object.freeze({
expectedRequestCount,
invalidRequests: Object.freeze(invalidRequests),
measuredRunCount: measured.length,
validForComparison:
measured.length > 0 &&
validMeasuredRunCount === measured.length &&
validRequestCount === expectedRequestCount,
validMeasuredRunCount,
validRequestCount,
});
}
function requireInitialImportRequestPair(
worker: WorkerCaptureMetrics
): readonly [
WorkerRequestPerformanceMetrics,
WorkerRequestPerformanceMetrics,
] | null {
const upserts = worker.requests.filter(
(request) =>
request.operation === 'DB_UPSERT_APP_PLAYLIST' && request.success
);
const gets = worker.requests.filter(
(request) =>
request.operation === 'DB_GET_APP_PLAYLIST' && request.success
);
return worker.requests.length === 2 &&
upserts.length === 1 &&
upserts[0] &&
gets.length === 1 &&
gets[0]
? [upserts[0], gets[0]]
: null;
}
function assessRequest(
request: WorkerRequestPerformanceMetrics
): DatabaseWorkerRequestMetricsInvalidReason | null {
if (
request.performanceCaptureUnavailableReason !== null ||
request.invalidReason !== null
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.WORKER_CAPTURE_INVALID;
}
if (
request.eventLoopDelayUnavailableReason !== null ||
request.histogramFlushedEpochMs === null ||
!isEventLoopDelay(request.eventLoopDelay)
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_DELAY_INVALID;
}
if (
request.eventLoopUtilizationUnavailableReason !== null ||
!isUtilization(request.eventLoopUtilization)
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.EVENT_LOOP_UTILIZATION_INVALID;
}
if (
request.threadCpuUnavailableReason !== null ||
!isFiniteNonNegative(request.threadCpuSystemMicros) ||
!isFiniteNonNegative(request.threadCpuUserMicros)
) {
return DATABASE_WORKER_REQUEST_METRICS_INVALID_REASON.THREAD_CPU_INVALID;
}
return null;
}
function isEventLoopDelay(
value: WorkerRequestPerformanceMetrics['eventLoopDelay']
): value is NonNullable<WorkerRequestPerformanceMetrics['eventLoopDelay']> {
return (
value !== null &&
isFiniteNonNegative(value.maxMs) &&
isFiniteNonNegative(value.p95Ms) &&
isFiniteNonNegative(value.p99Ms) &&
value.p95Ms <= value.p99Ms &&
value.p99Ms <= value.maxMs
);
}
function isUtilization(value: number | null): value is number {
return isFiniteNonNegative(value) && value <= 1;
}
function isFiniteNonNegative(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
@@ -0,0 +1,193 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
interface DiagnosticCaptureModule {
validateDiagnosticPlaylistWorkerCapture?: (
main: MainCaptureMetrics,
iterationDirectory: string
) => Promise<void>;
}
const diagnosticCaptureModulePromise = import(
new URL('./diagnostic-playlist-worker-capture.ts', import.meta.url).href
)
.then((module) => module as DiagnosticCaptureModule)
.catch(() => null);
function mainCapture(input: {
readonly profilePath: string | null;
readonly snapshotPath?: string | null;
readonly timelineType?: string;
}): MainCaptureMetrics {
return {
timeline: input.timelineType
? [{ epochMs: 1, type: input.timelineType }]
: [],
workers: [
{
kind: 'playlist-refresh.worker',
ordinal: 2,
postGcHeapUnavailableReason:
'worker-force-terminated-before-gc',
postGcHeapUsedBytes: null,
profilePath: input.profilePath,
snapshotPath: input.snapshotPath ?? null,
terminatedEpochMs: 2,
},
],
} as unknown as MainCaptureMetrics;
}
test('accepts one parseable diagnostic playlist-worker profile inside the iteration directory', async () => {
const module = await diagnosticCaptureModulePromise;
assert.ok(
module,
'diagnostic playlist worker capture validator must exist'
);
const validate = module.validateDiagnosticPlaylistWorkerCapture;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-diagnostic-worker-')
);
try {
const profilePath = join(
directory,
'playlist-refresh.worker-2.cpuprofile'
);
await writeFile(
profilePath,
JSON.stringify({ nodes: [{ id: 1 }], samples: [1] })
);
await assert.doesNotReject(() =>
validate?.(mainCapture({ profilePath }), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('fails closed for missing, escaped, malformed, or contaminated diagnostic artifacts', async () => {
const module = await diagnosticCaptureModulePromise;
assert.ok(module);
const validate = module.validateDiagnosticPlaylistWorkerCapture;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-diagnostic-worker-invalid-')
);
try {
await assert.rejects(
() => validate?.(mainCapture({ profilePath: null }), directory),
/diagnostic-playlist-worker-profile-missing/
);
await assert.rejects(
() =>
validate?.(
mainCapture({
profilePath: join(
directory,
'..',
'escaped.cpuprofile'
),
}),
directory
),
/diagnostic-playlist-worker-profile-path-invalid/
);
const malformedPath = join(
directory,
'playlist-refresh.worker-2.cpuprofile'
);
await writeFile(malformedPath, '{"nodes":[]}');
await assert.rejects(
() =>
validate?.(
mainCapture({ profilePath: malformedPath }),
directory
),
/diagnostic-playlist-worker-profile-invalid/
);
await assert.rejects(
() =>
validate?.(
mainCapture({
profilePath: malformedPath,
timelineType:
'worker-artifact-error:profile-stop:failed',
}),
directory
),
/diagnostic-playlist-worker-artifact-error/
);
await assert.rejects(
() =>
validate?.(
mainCapture({
profilePath: malformedPath,
snapshotPath: join(
directory,
'playlist-refresh.worker-2.heapsnapshot'
),
}),
directory
),
/diagnostic-playlist-worker-snapshot-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('the benchmark persists diagnostic raw metrics before requiring a parseable playlist-worker profile', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const resultWrite = source.indexOf(
"writeJson(join(iterationDirectory, 'result.json'), result)"
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const diagnosticValidation = source.indexOf(
'await validateDiagnosticPlaylistWorkerCapture('
);
assert.ok(resultWrite >= 0);
assert.ok(summaryWrite >= 0);
assert.ok(diagnosticValidation > summaryWrite);
});
test('the benchmark requires the diagnostic worker profile to come from an observed cancellation', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const diagnosticCancellationGuard = source.indexOf(
'if (!diagnosticIteration.cancellationEffectObserved)'
);
const diagnosticValidation = source.indexOf(
'await validateDiagnosticPlaylistWorkerCapture('
);
assert.ok(summaryWrite >= 0);
assert.ok(
diagnosticCancellationGuard > summaryWrite,
'raw manifest and summary must remain durable when the diagnostic cancellation is invalid'
);
assert.ok(
diagnosticCancellationGuard < diagnosticValidation,
'the cancellation guard must reject a normal-completion worker profile before artifact validation'
);
});
@@ -0,0 +1,72 @@
import { readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
interface CpuProfile {
readonly nodes?: unknown;
readonly samples?: unknown;
}
export async function validateDiagnosticPlaylistWorkerCapture(
main: MainCaptureMetrics,
iterationDirectory: string
): Promise<void> {
if (
main.timeline.some((record) =>
record.type.startsWith('worker-artifact-error:')
)
) {
throw new Error('diagnostic-playlist-worker-artifact-error');
}
const workers = main.workers.filter(
(worker) => worker.kind === 'playlist-refresh.worker'
);
if (workers.length !== 1) {
throw new Error('diagnostic-playlist-worker-cardinality-invalid');
}
const worker = workers[0];
if (!worker) {
throw new Error('diagnostic-playlist-worker-cardinality-invalid');
}
if (worker.snapshotPath !== null) {
throw new Error('diagnostic-playlist-worker-snapshot-unexpected');
}
if (
worker.postGcHeapUsedBytes !== null ||
worker.postGcHeapUnavailableReason !==
'worker-force-terminated-before-gc' ||
worker.terminatedEpochMs === null
) {
throw new Error('diagnostic-playlist-worker-termination-invalid');
}
if (worker.profilePath === null) {
throw new Error('diagnostic-playlist-worker-profile-missing');
}
const expectedProfilePath = join(
resolve(iterationDirectory),
`playlist-refresh.worker-${worker.ordinal}.cpuprofile`
);
if (resolve(worker.profilePath) !== expectedProfilePath) {
throw new Error('diagnostic-playlist-worker-profile-path-invalid');
}
let profile: CpuProfile;
try {
profile = JSON.parse(
await readFile(expectedProfilePath, 'utf8')
) as CpuProfile;
} catch {
throw new Error('diagnostic-playlist-worker-profile-invalid');
}
if (
!Array.isArray(profile.nodes) ||
profile.nodes.length === 0 ||
!Array.isArray(profile.samples) ||
profile.samples.length === 0
) {
throw new Error('diagnostic-playlist-worker-profile-invalid');
}
}
@@ -0,0 +1,185 @@
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function isCpuProfile(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
const startTime = value['startTime'];
const endTime = value['endTime'];
const nodes = value['nodes'];
const samples = value['samples'];
const timeDeltas = value['timeDeltas'];
if (
!isNonNegativeSafeInteger(startTime) ||
!isNonNegativeSafeInteger(endTime) ||
endTime <= startTime ||
!Array.isArray(nodes) ||
nodes.length === 0 ||
!Array.isArray(samples) ||
samples.length === 0 ||
!Array.isArray(timeDeltas) ||
timeDeltas.length !== samples.length
) {
return false;
}
const nodeIds = new Set<number>();
for (const node of nodes) {
if (!isCpuProfileNode(node) || nodeIds.has(node['id'])) {
return false;
}
nodeIds.add(node['id']);
}
if (
nodes.some((node) =>
(node as Record<string, unknown>)['children'] instanceof Array
? (
(node as Record<string, unknown>)[
'children'
] as unknown[]
).some(
(childId) =>
!isNonNegativeSafeInteger(childId) ||
!nodeIds.has(childId)
)
: false
) ||
samples.some(
(sampleId) =>
!isNonNegativeSafeInteger(sampleId) ||
!nodeIds.has(sampleId)
)
) {
return false;
}
const duration = endTime - startTime;
let elapsed = 0;
for (const delta of timeDeltas) {
if (!Number.isSafeInteger(delta)) {
return false;
}
elapsed += delta as number;
if (
!Number.isSafeInteger(elapsed) ||
elapsed < 0 ||
elapsed > duration
) {
return false;
}
}
return elapsed > 0;
}
export function isHeapSnapshot(value: unknown): boolean {
if (
!isRecord(value) ||
!isRecord(value['snapshot']) ||
!isRecord(value['snapshot']['meta'])
) {
return false;
}
const meta = value['snapshot']['meta'];
const nodeFields = meta['node_fields'];
const nodeTypes = meta['node_types'];
const edgeFields = meta['edge_fields'];
const edgeTypes = meta['edge_types'];
const nodes = value['nodes'];
const edges = value['edges'];
const strings = value['strings'];
return (
isNonEmptyStringArray(nodeFields) &&
isHeapTypeTable(nodeTypes, nodeFields.length) &&
isNonEmptyStringArray(edgeFields) &&
isHeapTypeTable(edgeTypes, edgeFields.length) &&
Array.isArray(nodes) &&
nodes.length > 0 &&
nodes.length % nodeFields.length === 0 &&
Array.isArray(edges) &&
edges.length > 0 &&
edges.length % edgeFields.length === 0 &&
Array.isArray(strings) &&
strings.length > 0
);
}
export function isRendererTrace(value: unknown): boolean {
if (!isRecord(value)) {
return false;
}
const events = value['traceEvents'];
return (
Array.isArray(events) &&
events.length > 0 &&
events.every(
(event) =>
isRecord(event) &&
isNonEmptyString(event['name']) &&
isNonEmptyString(event['ph']) &&
isNonNegativeFiniteNumber(event['ts'])
)
);
}
function isHeapTypeTable(value: unknown, fieldCount: number): boolean {
return (
Array.isArray(value) &&
value.length === fieldCount &&
value.every(
(entry) =>
isNonEmptyString(entry) || isNonEmptyStringArray(entry)
)
);
}
function isCpuProfileNode(
value: unknown
): value is Record<string, unknown> & { readonly id: number } {
if (
!isRecord(value) ||
!isNonNegativeSafeInteger(value['id']) ||
!isRecord(value['callFrame'])
) {
return false;
}
const callFrame = value['callFrame'];
const scriptId = callFrame['scriptId'];
const children = value['children'];
const hitCount = value['hitCount'];
return (
typeof callFrame['functionName'] === 'string' &&
(typeof scriptId === 'string' ||
isNonNegativeSafeInteger(scriptId)) &&
typeof callFrame['url'] === 'string' &&
Number.isSafeInteger(callFrame['lineNumber']) &&
Number.isSafeInteger(callFrame['columnNumber']) &&
(children === undefined ||
(Array.isArray(children) &&
children.every(isNonNegativeSafeInteger))) &&
(hitCount === undefined || isNonNegativeSafeInteger(hitCount))
);
}
function isNonEmptyStringArray(value: unknown): value is readonly string[] {
return (
Array.isArray(value) &&
value.length > 0 &&
value.every(isNonEmptyString)
);
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.length > 0;
}
function isNonNegativeSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function isNonNegativeFiniteNumber(value: unknown): value is number {
return (
typeof value === 'number' && Number.isFinite(value) && value >= 0
);
}
@@ -0,0 +1,234 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
type InitialImportDiagnosticCapture,
validateInitialImportDiagnosticArtifacts,
} from './initial-import-diagnostic-artifacts';
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
const HEAP_META = {
edge_fields: ['type', 'name_or_index', 'to_node'],
edge_types: [['context'], 'string_or_number', 'node'],
node_fields: [
'type',
'name',
'id',
'self_size',
'edge_count',
'detachedness',
],
node_types: [
['hidden'],
'string',
'number',
'number',
'number',
'number',
],
};
const HEAP_SNAPSHOT = {
edges: [0, 0, 0],
nodes: [0, 0, 1, 0, 1, 0],
snapshot: { meta: HEAP_META },
strings: ['', 'root'],
};
const TRACE = {
traceEvents: [{ name: 'initial-import', ph: 'X', ts: 1 }],
};
function capture(directory: string): InitialImportDiagnosticCapture {
return {
main: {
cpuProfilePath: join(directory, 'main.cpuprofile'),
heapSnapshotPath: join(directory, 'main.heapsnapshot'),
workers: [
{
kind: 'database.worker',
ordinal: 4,
profilePath: join(
directory,
'database.worker-4.cpuprofile'
),
snapshotPath: join(
directory,
'database.worker-4.heapsnapshot'
),
},
],
},
renderer: {
cpuProfilePath: join(directory, 'renderer.cpuprofile'),
heapSnapshotPath: join(directory, 'renderer.heapsnapshot'),
tracePath: join(directory, 'renderer.trace.json'),
},
};
}
async function writeArtifacts(directory: string): Promise<void> {
const artifacts: Readonly<Record<string, unknown>> = {
'database.worker-4.cpuprofile': CPU_PROFILE,
'database.worker-4.heapsnapshot': HEAP_SNAPSHOT,
'main.cpuprofile': CPU_PROFILE,
'main.heapsnapshot': HEAP_SNAPSHOT,
'renderer.cpuprofile': CPU_PROFILE,
'renderer.heapsnapshot': HEAP_SNAPSHOT,
'renderer.trace.json': TRACE,
};
await Promise.all(
Object.entries(artifacts).map(([filename, value]) =>
writeFile(join(directory, filename), JSON.stringify(value))
)
);
}
async function withArtifacts(
run: (directory: string) => Promise<void>
): Promise<void> {
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-content-')
);
try {
await writeArtifacts(directory);
await run(directory);
} finally {
await rm(directory, { force: true, recursive: true });
}
}
test('accepts bounded signed CPU time deltas emitted by Chromium', async () => {
await withArtifacts(async (directory) => {
await writeFile(
join(directory, 'renderer.cpuprofile'),
JSON.stringify({
...CPU_PROFILE,
endTime: 5_000,
samples: [1, 1, 1],
timeDeltas: [1_000, -100, 1_000],
})
);
await assert.doesNotReject(() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
)
);
});
});
test('rejects CPU profiles without useful bounded samples and stack references', async () => {
await withArtifacts(async (directory) => {
const invalidProfiles = [
{ ...CPU_PROFILE, samples: [], timeDeltas: [] },
{ ...CPU_PROFILE, timeDeltas: undefined },
{ ...CPU_PROFILE, timeDeltas: [] },
{
...CPU_PROFILE,
endTime: CPU_PROFILE.startTime,
},
{
...CPU_PROFILE,
samples: [2],
},
{
...CPU_PROFILE,
timeDeltas: [-1],
},
{
...CPU_PROFILE,
timeDeltas: [2_001],
},
{
...CPU_PROFILE,
nodes: [
...CPU_PROFILE.nodes,
{ ...CPU_PROFILE.nodes[0], id: 1 },
],
},
{
...CPU_PROFILE,
nodes: [{ id: 1 }],
},
];
for (const profile of invalidProfiles) {
await writeFile(
join(directory, 'main.cpuprofile'),
JSON.stringify(profile)
);
await assert.rejects(
() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
),
/initial-import-diagnostic-artifact-invalid:main-cpu-profile/
);
}
});
});
test('rejects empty or malformed heap tables and invalid field strides', async () => {
await withArtifacts(async (directory) => {
const invalidSnapshots = [
{ snapshot: {}, nodes: [], edges: [], strings: [] },
{ ...HEAP_SNAPSHOT, nodes: [] },
{ ...HEAP_SNAPSHOT, edges: [] },
{ ...HEAP_SNAPSHOT, strings: [] },
{
...HEAP_SNAPSHOT,
snapshot: {
meta: { ...HEAP_META, node_types: undefined },
},
},
{ ...HEAP_SNAPSHOT, nodes: [...HEAP_SNAPSHOT.nodes, 0] },
{ ...HEAP_SNAPSHOT, edges: [...HEAP_SNAPSHOT.edges, 0] },
];
for (const snapshot of invalidSnapshots) {
await writeFile(
join(directory, 'main.heapsnapshot'),
JSON.stringify(snapshot)
);
await assert.rejects(
() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
),
/initial-import-diagnostic-artifact-invalid:main-heap-snapshot/
);
}
});
});
test('rejects empty traces and malformed trace events', async () => {
await withArtifacts(async (directory) => {
const invalidTraces = [
{ traceEvents: [] },
{ traceEvents: [null] },
{ traceEvents: [{ name: '', ph: 'X', ts: 1 }] },
{ traceEvents: [{ name: 'initial-import', ph: '', ts: 1 }] },
{
traceEvents: [
{ name: 'initial-import', ph: 'X', ts: 'invalid' },
],
},
];
for (const trace of invalidTraces) {
await writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(trace)
);
await assert.rejects(
() =>
validateInitialImportDiagnosticArtifacts(
capture(directory),
directory
),
/initial-import-diagnostic-artifact-invalid:renderer-trace/
);
}
});
});
@@ -0,0 +1,394 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { mkdtemp, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { CPU_PROFILE } from './initial-import-diagnostic-test-fixtures';
interface DiagnosticCapture {
readonly main: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly workers: readonly {
readonly kind: string;
readonly ordinal: number;
readonly profilePath: string | null;
readonly snapshotPath: string | null;
}[];
};
readonly renderer: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly tracePath: string | null;
};
}
interface DiagnosticArtifactsModule {
validateInitialImportDiagnosticArtifacts?: (
capture: DiagnosticCapture,
iterationDirectory: string
) => Promise<void>;
}
const diagnosticArtifactsModulePromise = import(
new URL('./initial-import-diagnostic-artifacts.ts', import.meta.url).href
)
.then((module) => module as DiagnosticArtifactsModule)
.catch(() => null);
const HEAP_SNAPSHOT = {
edges: [0, 0, 0],
nodes: [0, 0, 1, 0, 1, 0],
snapshot: {
meta: {
edge_fields: ['type', 'name_or_index', 'to_node'],
edge_types: [['context'], 'string_or_number', 'node'],
node_fields: [
'type',
'name',
'id',
'self_size',
'edge_count',
'detachedness',
],
node_types: [
['hidden'],
'string',
'number',
'number',
'number',
'number',
],
},
},
strings: ['', 'root'],
};
const RENDERER_TRACE = {
traceEvents: [{ name: 'initial-import', ph: 'X', ts: 1 }],
};
function capture(directory: string, databaseOrdinal = 4): DiagnosticCapture {
return {
main: {
cpuProfilePath: join(directory, 'main.cpuprofile'),
heapSnapshotPath: join(directory, 'main.heapsnapshot'),
workers: [
{
kind: 'database.worker',
ordinal: databaseOrdinal,
profilePath: join(
directory,
`database.worker-${databaseOrdinal}.cpuprofile`
),
snapshotPath: join(
directory,
`database.worker-${databaseOrdinal}.heapsnapshot`
),
},
],
},
renderer: {
cpuProfilePath: join(directory, 'renderer.cpuprofile'),
heapSnapshotPath: join(directory, 'renderer.heapsnapshot'),
tracePath: join(directory, 'renderer.trace.json'),
},
};
}
async function writeValidArtifacts(
directory: string,
databaseOrdinal = 4
): Promise<void> {
await Promise.all([
writeFile(
join(directory, 'main.cpuprofile'),
JSON.stringify(CPU_PROFILE)
),
writeFile(
join(directory, 'renderer.cpuprofile'),
JSON.stringify(CPU_PROFILE)
),
writeFile(
join(directory, `database.worker-${databaseOrdinal}.cpuprofile`),
JSON.stringify(CPU_PROFILE)
),
writeFile(
join(directory, `database.worker-${databaseOrdinal}.heapsnapshot`),
JSON.stringify(HEAP_SNAPSHOT)
),
writeFile(
join(directory, 'main.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
),
writeFile(
join(directory, 'renderer.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
),
writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(RENDERER_TRACE)
),
]);
}
test('accepts the complete parseable initial-import diagnostic artifact set', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module, 'initial-import diagnostic validator must exist');
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-diagnostic-')
);
try {
await writeValidArtifacts(directory);
await assert.doesNotReject(() =>
validate?.(capture(directory), directory)
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('fails closed for malformed or missing required process artifacts', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-invalid-')
);
try {
await writeValidArtifacts(directory);
await writeFile(join(directory, 'renderer.trace.json'), '{');
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:renderer-trace/
);
await writeFile(
join(directory, 'renderer.trace.json'),
JSON.stringify(RENDERER_TRACE)
);
const missingMainProfile: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
cpuProfilePath: null,
},
};
await assert.rejects(
() => validate?.(missingMainProfile, directory),
/initial-import-diagnostic-artifact-missing:main-cpu-profile/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('rejects escaped paths and symlinks instead of reading outside the iteration directory', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-contained-')
);
const outsideDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-outside-')
);
try {
await writeValidArtifacts(directory);
const escapedCapture: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
cpuProfilePath: join(outsideDirectory, 'main.cpuprofile'),
},
};
await writeFile(
join(outsideDirectory, 'main.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(escapedCapture, directory),
/initial-import-diagnostic-artifact-path-invalid:main-cpu-profile/
);
const rendererProfilePath = join(directory, 'renderer.cpuprofile');
await unlink(rendererProfilePath);
await symlink(
join(outsideDirectory, 'main.cpuprofile'),
rendererProfilePath
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:renderer-cpu-profile/
);
} finally {
await rm(directory, { force: true, recursive: true });
await rm(outsideDirectory, { force: true, recursive: true });
}
});
test('requires exactly one database profile and rejects every playlist-worker profile', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-workers-')
);
try {
await writeValidArtifacts(directory);
await writeFile(
join(directory, 'database.worker-9.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-database-profile-cardinality-invalid/
);
await unlink(join(directory, 'database.worker-9.cpuprofile'));
await writeFile(
join(directory, 'playlist-refresh.worker-2.cpuprofile'),
JSON.stringify(CPU_PROFILE)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-playlist-profile-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('requires a structurally valid database worker heap snapshot', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-')
);
try {
await writeValidArtifacts(directory);
const missingSnapshot: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
workers: [
{
...capture(directory).main.workers[0],
snapshotPath: null,
},
],
},
};
await assert.rejects(
() => validate?.(missingSnapshot, directory),
/initial-import-diagnostic-artifact-missing:database-heap-snapshot/
);
await writeFile(
join(directory, 'database.worker-4.heapsnapshot'),
JSON.stringify({ snapshot: {}, nodes: [], edges: [] })
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-invalid:database-heap-snapshot/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
test('rejects escaped and symlinked database worker heap snapshots', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-contained-')
);
const outsideDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-outside-')
);
try {
await writeValidArtifacts(directory);
const outsideSnapshotPath = join(
outsideDirectory,
'database.worker-4.heapsnapshot'
);
await writeFile(outsideSnapshotPath, JSON.stringify(HEAP_SNAPSHOT));
const escapedSnapshot: DiagnosticCapture = {
...capture(directory),
main: {
...capture(directory).main,
workers: [
{
...capture(directory).main.workers[0],
snapshotPath: outsideSnapshotPath,
},
],
},
};
await assert.rejects(
() => validate?.(escapedSnapshot, directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
const snapshotPath = join(directory, 'database.worker-4.heapsnapshot');
await unlink(snapshotPath);
await symlink(outsideSnapshotPath, snapshotPath);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-artifact-path-invalid:database-heap-snapshot/
);
} finally {
await rm(directory, { force: true, recursive: true });
await rm(outsideDirectory, { force: true, recursive: true });
}
});
test('rejects extra database snapshots and every playlist-worker snapshot', async () => {
const module = await diagnosticArtifactsModulePromise;
assert.ok(module);
const validate = module.validateInitialImportDiagnosticArtifacts;
assert.equal(typeof validate, 'function');
const directory = await mkdtemp(
join(tmpdir(), 'iptvnator-initial-import-worker-snapshot-count-')
);
try {
await writeValidArtifacts(directory);
await writeFile(
join(directory, 'database.worker-9.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-database-snapshot-cardinality-invalid/
);
await unlink(join(directory, 'database.worker-9.heapsnapshot'));
await writeFile(
join(directory, 'playlist-refresh.worker-2.heapsnapshot'),
JSON.stringify(HEAP_SNAPSHOT)
);
await assert.rejects(
() => validate?.(capture(directory), directory),
/initial-import-diagnostic-playlist-snapshot-unexpected/
);
} finally {
await rm(directory, { force: true, recursive: true });
}
});
@@ -0,0 +1,262 @@
import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import {
isCpuProfile,
isHeapSnapshot,
isRendererTrace,
} from './initial-import-diagnostic-artifact-content-validation';
interface DiagnosticWorkerArtifact {
readonly kind: string;
readonly ordinal: number;
readonly profilePath: string | null;
readonly snapshotPath: string | null;
}
export interface InitialImportDiagnosticCapture {
readonly main: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly workers: readonly DiagnosticWorkerArtifact[];
};
readonly renderer: {
readonly cpuProfilePath: string | null;
readonly heapSnapshotPath: string | null;
readonly tracePath: string | null;
};
}
type ArtifactLabel =
| 'database-cpu-profile'
| 'database-heap-snapshot'
| 'main-cpu-profile'
| 'main-heap-snapshot'
| 'renderer-cpu-profile'
| 'renderer-heap-snapshot'
| 'renderer-trace';
interface ArtifactDescription {
readonly filename: string;
readonly label: ArtifactLabel;
readonly path: string | null;
readonly validate: (value: unknown) => boolean;
}
export async function validateInitialImportDiagnosticArtifacts(
capture: InitialImportDiagnosticCapture,
iterationDirectory: string
): Promise<void> {
const directory = resolve(iterationDirectory);
let realDirectory: string;
try {
realDirectory = await realpath(directory);
} catch {
throw new Error(
'initial-import-diagnostic-artifact-path-invalid:iteration-directory'
);
}
const databaseWorker = requireExactDatabaseWorker(capture.main.workers);
await assertWorkerArtifactCardinality(directory);
const artifacts: readonly ArtifactDescription[] = [
{
filename: 'main.cpuprofile',
label: 'main-cpu-profile',
path: capture.main.cpuProfilePath,
validate: isCpuProfile,
},
{
filename: 'renderer.cpuprofile',
label: 'renderer-cpu-profile',
path: capture.renderer.cpuProfilePath,
validate: isCpuProfile,
},
{
filename: 'renderer.trace.json',
label: 'renderer-trace',
path: capture.renderer.tracePath,
validate: isRendererTrace,
},
{
filename: 'main.heapsnapshot',
label: 'main-heap-snapshot',
path: capture.main.heapSnapshotPath,
validate: isHeapSnapshot,
},
{
filename: 'renderer.heapsnapshot',
label: 'renderer-heap-snapshot',
path: capture.renderer.heapSnapshotPath,
validate: isHeapSnapshot,
},
{
filename: `database.worker-${databaseWorker.ordinal}.cpuprofile`,
label: 'database-cpu-profile',
path: databaseWorker.profilePath,
validate: isCpuProfile,
},
{
filename: `database.worker-${databaseWorker.ordinal}.heapsnapshot`,
label: 'database-heap-snapshot',
path: databaseWorker.snapshotPath,
validate: isHeapSnapshot,
},
];
for (const artifact of artifacts) {
await validateJsonArtifact(directory, realDirectory, artifact);
}
}
function requireExactDatabaseWorker(
workers: readonly DiagnosticWorkerArtifact[]
): DiagnosticWorkerArtifact {
if (
workers.some(
(worker) =>
worker.kind === 'playlist-refresh.worker' &&
worker.profilePath !== null
)
) {
throw new Error(
'initial-import-diagnostic-playlist-profile-unexpected'
);
}
if (
workers.some(
(worker) =>
worker.kind === 'playlist-refresh.worker' &&
worker.snapshotPath !== null
)
) {
throw new Error(
'initial-import-diagnostic-playlist-snapshot-unexpected'
);
}
const databaseWorkers = workers.filter(
(worker) => worker.kind === 'database.worker'
);
const worker = databaseWorkers[0];
if (
databaseWorkers.length !== 1 ||
!worker ||
!Number.isSafeInteger(worker.ordinal) ||
worker.ordinal < 0
) {
throw new Error(
'initial-import-diagnostic-database-profile-cardinality-invalid'
);
}
return worker;
}
async function assertWorkerArtifactCardinality(
directory: string
): Promise<void> {
let entries: readonly string[];
try {
entries = await readdir(directory);
} catch {
throw new Error(
'initial-import-diagnostic-artifact-path-invalid:iteration-directory'
);
}
if (
entries.some((entry) =>
/^playlist-refresh\.worker-\d+\.cpuprofile$/u.test(entry)
)
) {
throw new Error(
'initial-import-diagnostic-playlist-profile-unexpected'
);
}
if (
entries.filter((entry) =>
/^database\.worker-\d+\.cpuprofile$/u.test(entry)
).length !== 1
) {
throw new Error(
'initial-import-diagnostic-database-profile-cardinality-invalid'
);
}
if (
entries.some((entry) =>
/^playlist-refresh\.worker-\d+\.heapsnapshot$/u.test(entry)
)
) {
throw new Error(
'initial-import-diagnostic-playlist-snapshot-unexpected'
);
}
if (
entries.filter((entry) =>
/^database\.worker-\d+\.heapsnapshot$/u.test(entry)
).length !== 1
) {
throw new Error(
'initial-import-diagnostic-database-snapshot-cardinality-invalid'
);
}
}
async function validateJsonArtifact(
directory: string,
realDirectory: string,
artifact: ArtifactDescription
): Promise<void> {
if (artifact.path === null) {
throw new Error(
`initial-import-diagnostic-artifact-missing:${artifact.label}`
);
}
const expectedPath = join(directory, artifact.filename);
if (resolve(artifact.path) !== expectedPath) {
throw new Error(
`initial-import-diagnostic-artifact-path-invalid:${artifact.label}`
);
}
let value: unknown;
try {
const stats = await lstat(expectedPath);
if (!stats.isFile() || stats.isSymbolicLink()) {
throw new Error('unsafe-artifact-type');
}
const realArtifactPath = await realpath(expectedPath);
if (!isContained(realDirectory, realArtifactPath)) {
throw new Error('escaped-artifact');
}
value = JSON.parse(await readFile(realArtifactPath, 'utf8')) as unknown;
} catch (error) {
if (
error instanceof Error &&
(error.message === 'unsafe-artifact-type' ||
error.message === 'escaped-artifact')
) {
throw new Error(
`initial-import-diagnostic-artifact-path-invalid:${artifact.label}`
);
}
throw new Error(
`initial-import-diagnostic-artifact-invalid:${artifact.label}`
);
}
if (!artifact.validate(value)) {
throw new Error(
`initial-import-diagnostic-artifact-invalid:${artifact.label}`
);
}
}
function isContained(directory: string, artifactPath: string): boolean {
const relativePath = relative(directory, artifactPath);
return (
relativePath.length > 0 &&
!isAbsolute(relativePath) &&
relativePath !== '..' &&
!relativePath.startsWith(`..${sep}`)
);
}
@@ -0,0 +1,19 @@
export const CPU_PROFILE = Object.freeze({
endTime: 3_000,
nodes: Object.freeze([
Object.freeze({
callFrame: Object.freeze({
columnNumber: -1,
functionName: '(root)',
lineNumber: -1,
scriptId: '0',
url: '',
}),
hitCount: 1,
id: 1,
}),
]),
samples: Object.freeze([1]),
startTime: 1_000,
timeDeltas: Object.freeze([1_000]),
});
@@ -0,0 +1,115 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { PERFORMANCE_ITERATION_KIND } from './m3u-refresh-cancellation-contract';
import {
assertSyntheticM3uImportUrl,
createM3uImportIterationDefinitions,
listM3uImportScenarios,
M3U_IMPORT_SCENARIO_ID,
} from './m3u-import-benchmark-contract';
describe('formal M3U import benchmark contract', () => {
it('defines the exact 10k, 50k, and 100k synthetic scenarios', () => {
assert.deepEqual(listM3uImportScenarios(), [
{
channelCount: 10_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_10K,
},
{
channelCount: 50_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_50K,
},
{
channelCount: 100_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_100K,
},
]);
});
it('schedules one warm-up, five measured runs, and one diagnostic per size', () => {
const definitions = createM3uImportIterationDefinitions(false);
assert.equal(definitions.length, 21);
for (const scenario of listM3uImportScenarios()) {
const scenarioRuns = definitions.filter(
(definition) => definition.scenarioId === scenario.id
);
assert.deepEqual(
scenarioRuns.map(({ kind, runId }) => ({ kind, runId })),
[
{
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
runId: 'warmup-01',
},
...Array.from({ length: 5 }, (_, index) => ({
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
runId: `run-${String(index + 1).padStart(2, '0')}`,
})),
{
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
runId: 'diagnostic',
},
]
);
assert.ok(
scenarioRuns.every(
(definition) =>
definition.channelCount === scenario.channelCount
)
);
}
});
it('keeps fixture sizes unchanged while smoke mode reduces measured runs to one', () => {
const definitions = createM3uImportIterationDefinitions(true);
assert.equal(definitions.length, 9);
for (const scenario of listM3uImportScenarios()) {
assert.deepEqual(
definitions
.filter(
(definition) => definition.scenarioId === scenario.id
)
.map(({ kind, runId }) => ({ kind, runId })),
[
{
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
runId: 'warmup-01',
},
{
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
runId: 'run-01',
},
{
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
runId: 'diagnostic',
},
]
);
}
});
it('accepts only the exact credential-free loopback fixture URL', () => {
assert.doesNotThrow(() =>
assertSyntheticM3uImportUrl(
'http://127.0.0.1:43210/synthetic-performance.m3u'
)
);
for (const unsafe of [
'https://127.0.0.1:43210/synthetic-performance.m3u',
'http://localhost:43210/synthetic-performance.m3u',
'http://127.0.0.1/synthetic-performance.m3u',
'http://user:pass@127.0.0.1:43210/synthetic-performance.m3u',
'http://127.0.0.1:43210/other.m3u',
'http://127.0.0.1:43210/synthetic-performance.m3u?token=secret',
'http://127.0.0.1:43210/synthetic-performance.m3u#fragment',
]) {
assert.throws(
() => assertSyntheticM3uImportUrl(unsafe),
/exact synthetic HTTP loopback URL/
);
}
});
});
@@ -0,0 +1,118 @@
import {
PERFORMANCE_ITERATION_KIND,
type PerformanceIterationKind,
} from './m3u-refresh-cancellation-contract';
import { SYNTHETIC_M3U_RESOURCE_PATH } from './synthetic-m3u-server';
import type { SyntheticM3uChannelCount } from './synthetic-m3u';
export const M3U_IMPORT_SCENARIO_ID = {
IMPORT_10K: 'm3u-import-10k',
IMPORT_50K: 'm3u-import-50k',
IMPORT_100K: 'm3u-import-100k',
} as const;
export type M3uImportScenarioId =
(typeof M3U_IMPORT_SCENARIO_ID)[keyof typeof M3U_IMPORT_SCENARIO_ID];
export interface M3uImportScenario {
readonly channelCount: SyntheticM3uChannelCount;
readonly id: M3uImportScenarioId;
}
export interface M3uImportIterationDefinition {
readonly channelCount: SyntheticM3uChannelCount;
readonly kind: PerformanceIterationKind;
readonly runId: string;
readonly scenarioId: M3uImportScenarioId;
}
const SCENARIOS: readonly M3uImportScenario[] = Object.freeze([
Object.freeze({
channelCount: 10_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_10K,
}),
Object.freeze({
channelCount: 50_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_50K,
}),
Object.freeze({
channelCount: 100_000,
id: M3U_IMPORT_SCENARIO_ID.IMPORT_100K,
}),
]);
export function listM3uImportScenarios(): readonly M3uImportScenario[] {
return SCENARIOS;
}
export function createM3uImportIterationDefinitions(
smoke: boolean
): readonly M3uImportIterationDefinition[] {
const measuredRuns = smoke ? 1 : 5;
const definitions: M3uImportIterationDefinition[] = [];
for (const scenario of SCENARIOS) {
definitions.push(
iteration(scenario, PERFORMANCE_ITERATION_KIND.WARMUP, 'warmup-01')
);
for (let index = 1; index <= measuredRuns; index += 1) {
definitions.push(
iteration(
scenario,
PERFORMANCE_ITERATION_KIND.MEASURED,
`run-${String(index).padStart(2, '0')}`
)
);
}
definitions.push(
iteration(
scenario,
PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
'diagnostic'
)
);
}
return Object.freeze(definitions);
}
export function assertSyntheticM3uImportUrl(value: string): void {
let url: URL;
try {
url = new URL(value);
} catch {
throw invalidSyntheticUrl();
}
if (
url.protocol !== 'http:' ||
url.hostname !== '127.0.0.1' ||
url.port.length === 0 ||
url.username.length > 0 ||
url.password.length > 0 ||
url.pathname !== SYNTHETIC_M3U_RESOURCE_PATH ||
url.search.length > 0 ||
url.hash.length > 0
) {
throw invalidSyntheticUrl();
}
}
function iteration(
scenario: M3uImportScenario,
kind: PerformanceIterationKind,
runId: string
): M3uImportIterationDefinition {
return Object.freeze({
channelCount: scenario.channelCount,
kind,
runId,
scenarioId: scenario.id,
});
}
function invalidSyntheticUrl(): Error {
return new Error(
'M3U performance fixture must use the exact synthetic HTTP loopback URL'
);
}
@@ -0,0 +1,184 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { describe, it } from 'node:test';
import {
assertM3uImportOutputPathHasNoSymlinks,
assertM3uImportSourceState,
M3U_IMPORT_RENDERER_CDP_PORT,
resolveM3uImportRendererCdpPort,
resolveM3uImportOutputLayout,
} from './m3u-import-benchmark-support';
import { assertM3uImportRequestDelta } from './m3u-import.benchmark';
describe('initial M3U import benchmark lifecycle', () => {
it('keeps requested output and variant strictly below dist/performance', () => {
const workspaceRoot = resolve('/workspace/iptvnator');
const outputRoot = resolve(
workspaceRoot,
'dist/performance/20260727T040000Z-m3u-import'
);
assert.deepEqual(
resolveM3uImportOutputLayout(workspaceRoot, outputRoot, 'baseline'),
{
outputRoot,
variantDirectory: resolve(outputRoot, 'baseline'),
}
);
for (const requestedRoot of [
'dist/performance/run',
resolve(workspaceRoot, 'dist/performance'),
resolve(workspaceRoot, 'dist/performance-sibling/run'),
resolve(workspaceRoot, '../outside'),
]) {
assert.throws(
() =>
resolveM3uImportOutputLayout(
workspaceRoot,
requestedRoot,
'baseline'
),
/absolute path below dist\/performance/
);
}
assert.throws(
() =>
resolveM3uImportOutputLayout(
workspaceRoot,
outputRoot,
'../escape'
),
/variant is invalid/i
);
});
it('requires a clean source tree for formal runs but permits dirty smoke evidence', () => {
const dirtyState = {
commit: 'a'.repeat(40),
diffSha256: 'b'.repeat(64),
dirty: true,
};
assert.throws(
() => assertM3uImportSourceState(false, dirtyState),
/Formal performance runs require a clean Git worktree/
);
assert.doesNotThrow(() => assertM3uImportSourceState(true, dirtyState));
assert.doesNotThrow(() =>
assertM3uImportSourceState(false, {
...dirtyState,
dirty: false,
})
);
});
it('rejects an existing output ancestor symlink before artifact creation', async () => {
const fixtureRoot = await mkdtemp(
join(tmpdir(), 'iptvnator-m3u-output-path-')
);
const repositoryRoot = join(fixtureRoot, 'repository');
const performanceRoot = join(
repositoryRoot,
'dist',
'performance'
);
const outside = join(fixtureRoot, 'outside');
await mkdir(performanceRoot, { recursive: true });
await mkdir(outside);
await symlink(outside, join(performanceRoot, 'redirect'));
try {
await assert.rejects(
assertM3uImportOutputPathHasNoSymlinks(
repositoryRoot,
join(performanceRoot, 'redirect', 'run')
),
/Performance output path contains a symbolic link/
);
await assert.doesNotReject(
assertM3uImportOutputPathHasNoSymlinks(
repositoryRoot,
join(performanceRoot, 'ordinary', 'run')
)
);
} finally {
await rm(fixtureRoot, { force: true, recursive: true });
}
});
it('keeps formal CDP capture on 9222 while allowing an isolated smoke port', () => {
assert.equal(
resolveM3uImportRendererCdpPort(false, undefined),
M3U_IMPORT_RENDERER_CDP_PORT
);
assert.equal(
resolveM3uImportRendererCdpPort(false, '9222'),
M3U_IMPORT_RENDERER_CDP_PORT
);
assert.equal(resolveM3uImportRendererCdpPort(true, '9322'), 9322);
assert.throws(
() => resolveM3uImportRendererCdpPort(false, '9322'),
/Formal performance runs require CDP port 9222/
);
for (const invalid of ['0', '1023', '65536', '9222.5', 'not-a-port']) {
assert.throws(
() => resolveM3uImportRendererCdpPort(true, invalid),
/IPTVNATOR_PERF_CDP_PORT is invalid/
);
}
});
it('fails closed unless one and only one app GET occurs', () => {
assert.doesNotThrow(() => assertM3uImportRequestDelta(3, 4));
assert.throws(
() => assertM3uImportRequestDelta(3, 3),
/exactly one application GET/
);
assert.throws(
() => assertM3uImportRequestDelta(3, 5),
/exactly one application GET/
);
});
it('waits for the exact upsert plus route-reload GET sequence', () => {
const source = readFileSync(
resolve(
process.cwd(),
'src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
assert.match(source, /status\.databaseRequests\s*===\s*2/);
assert.match(source, /status\.databaseUpsertsCompleted\s*===\s*1/);
assert.match(source, /status\.databaseGetsCompleted\s*===\s*1/);
assert.match(source, /status\.databasePending\s*===\s*0/);
assert.match(source, /status\.preloadSuccessMarkers\s*===\s*2/);
});
it('always disposes renderer capture before closing Electron on iteration failure', () => {
const source = readFileSync(
resolve(
process.cwd(),
'src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
const runIteration = source.indexOf('async function runIteration');
const finallyBlock = source.indexOf('} finally {', runIteration);
const rendererDispose = source.indexOf(
'await renderer.dispose()',
finallyBlock
);
const appClose = source.indexOf('await closeElectronApp(app)', finallyBlock);
assert.ok(finallyBlock > runIteration);
assert.ok(rendererDispose > finallyBlock);
assert.ok(appClose > rendererDispose);
});
});
@@ -0,0 +1,300 @@
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { access, lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
import { createServer as createTcpServer } from 'node:net';
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import { expect, type Page } from '@playwright/test';
import {
type LaunchedElectronApp,
workspaceRoot,
} from '../electron-test-fixtures';
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
import type { RendererWindowIdentity } from './renderer-window-rss-session';
const OUTPUT_VARIANT_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
export const M3U_IMPORT_RENDERER_CDP_PORT = 9222;
export interface M3uImportGitSourceState {
readonly commit: string;
readonly diffSha256: string;
readonly dirty: boolean;
}
export interface M3uImportOutputLayout {
readonly outputRoot: string;
readonly variantDirectory: string;
}
export interface M3uImportBenchmarkConfiguration extends M3uImportOutputLayout {
readonly electronVersion: string;
readonly measuredRuns: number;
readonly rendererCdpPort: number;
readonly smoke: boolean;
readonly sourceState: M3uImportGitSourceState;
readonly variant: string;
}
export async function resolveM3uImportBenchmarkConfiguration(): Promise<M3uImportBenchmarkConfiguration> {
const requestedRoot = process.env['IPTVNATOR_PERF_OUTPUT_DIR'];
if (!requestedRoot) {
throw new Error('IPTVNATOR_PERF_OUTPUT_DIR is required');
}
const variant = process.env['IPTVNATOR_PERF_VARIANT'] ?? 'baseline';
const layout = resolveM3uImportOutputLayout(
workspaceRoot,
requestedRoot,
variant
);
const smoke = process.env['IPTVNATOR_PERF_SMOKE'] === '1';
const rendererCdpPort = resolveM3uImportRendererCdpPort(
smoke,
process.env['IPTVNATOR_PERF_CDP_PORT']
);
const sourceState = readGitSourceState(workspaceRoot);
assertM3uImportSourceState(smoke, sourceState);
const electronPackage = JSON.parse(
await readFile(
join(workspaceRoot, 'node_modules', 'electron', 'package.json'),
'utf8'
)
) as { version?: unknown };
if (typeof electronPackage.version !== 'string') {
throw new Error('Unable to resolve the Electron runtime version');
}
await assertM3uImportOutputPathHasNoSymlinks(
workspaceRoot,
layout.outputRoot
);
await assertMissing(layout.variantDirectory);
await assertPerformanceArtifactCapacity(layout.variantDirectory);
await mkdir(layout.variantDirectory, { recursive: true });
return Object.freeze({
...layout,
electronVersion: electronPackage.version,
measuredRuns: smoke ? 1 : 5,
rendererCdpPort,
smoke,
sourceState,
variant,
});
}
export function resolveM3uImportOutputLayout(
repositoryRoot: string,
requestedRoot: string,
variant: string
): M3uImportOutputLayout {
const performanceRoot = resolve(repositoryRoot, 'dist', 'performance');
const outputRoot = resolve(requestedRoot);
if (
!isAbsolute(requestedRoot) ||
!isStrictDescendant(performanceRoot, outputRoot)
) {
throw new Error(
'Performance output must be an absolute path below dist/performance'
);
}
if (!OUTPUT_VARIANT_PATTERN.test(variant)) {
throw new Error('IPTVNATOR_PERF_VARIANT is invalid');
}
const variantDirectory = resolve(outputRoot, variant);
if (!isStrictDescendant(outputRoot, variantDirectory)) {
throw new Error('IPTVNATOR_PERF_VARIANT is invalid');
}
return Object.freeze({ outputRoot, variantDirectory });
}
export async function assertM3uImportOutputPathHasNoSymlinks(
repositoryRoot: string,
outputRoot: string
): Promise<void> {
const root = resolve(repositoryRoot);
const output = resolve(outputRoot);
if (!isStrictDescendant(root, output)) {
throw new Error('Performance output path escapes the repository');
}
const components = relative(root, output).split(sep);
let current = root;
for (const component of ['', ...components]) {
current = component === '' ? current : join(current, component);
try {
const stats = await lstat(current);
if (stats.isSymbolicLink()) {
throw new Error(
`Performance output path contains a symbolic link: ${current}`
);
}
} catch (error) {
if (isMissingPath(error)) {
return;
}
throw error;
}
}
}
export function assertM3uImportSourceState(
smoke: boolean,
state: M3uImportGitSourceState
): void {
if (!smoke && state.dirty) {
throw new Error(
'Formal performance runs require a clean Git worktree; commit or stash changes first'
);
}
}
export function resolveM3uImportRendererCdpPort(
smoke: boolean,
requestedPort: string | undefined
): number {
const port =
requestedPort === undefined
? M3U_IMPORT_RENDERER_CDP_PORT
: Number(requestedPort);
if (
(requestedPort !== undefined && !/^[1-9]\d*$/.test(requestedPort)) ||
!Number.isSafeInteger(port) ||
port < 1024 ||
port > 65_535
) {
throw new Error('IPTVNATOR_PERF_CDP_PORT is invalid');
}
if (!smoke && port !== M3U_IMPORT_RENDERER_CDP_PORT) {
throw new Error(
`Formal performance runs require CDP port ${M3U_IMPORT_RENDERER_CDP_PORT}`
);
}
return port;
}
export async function resolveRendererWindowIdentity(
app: LaunchedElectronApp
): Promise<RendererWindowIdentity> {
const handle = await app.electronApp.browserWindow(app.mainWindow);
try {
return await handle.evaluate((browserWindow) => {
const exactWindow = browserWindow as unknown as {
readonly id: number;
readonly webContents: { readonly id: number };
};
return {
browserWindowId: exactWindow.id,
webContentsId: exactWindow.webContents.id,
};
});
} finally {
await handle.dispose();
}
}
export async function assertTcpPortAvailable(port: number): Promise<void> {
const server = createTcpServer();
await new Promise<void>((resolvePromise, rejectPromise) => {
server.once('error', rejectPromise);
server.listen(
{ exclusive: true, host: '127.0.0.1', port },
resolvePromise
);
});
await new Promise<void>((resolvePromise, rejectPromise) => {
server.close((error) =>
error ? rejectPromise(error) : resolvePromise()
);
});
}
export async function assertRendererCdpTarget(
page: Page,
port: number
): Promise<void> {
await expect
.poll(
async () => {
try {
const response = await fetch(
`http://127.0.0.1:${port}/json/list`
);
const targets = response.ok
? ((await response.json()) as unknown)
: null;
return (
Array.isArray(targets) &&
targets.some(
(target) =>
typeof target === 'object' &&
target !== null &&
(target as Record<string, unknown>)['type'] ===
'page' &&
(target as Record<string, unknown>)['url'] ===
page.url()
)
);
} catch {
return false;
}
},
{ timeout: 10_000 }
)
.toBe(true);
}
export async function writeM3uImportJson(
path: string,
value: unknown
): Promise<void> {
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function readGitSourceState(repositoryRoot: string): M3uImportGitSourceState {
const git = (args: readonly string[]): string =>
execFileSync('git', [...args], {
cwd: repositoryRoot,
encoding: 'utf8',
});
const commit = git(['rev-parse', 'HEAD']).trim();
const status = git(['status', '--porcelain=v1', '--untracked-files=all']);
const diffSha256 = createHash('sha256')
.update(status)
.update('\0')
.update(git(['diff', '--binary', 'HEAD']))
.update('\0')
.update(git(['diff', '--binary', '--cached', 'HEAD']))
.digest('hex');
return Object.freeze({
commit,
diffSha256,
dirty: status.trim().length > 0,
});
}
function isStrictDescendant(parent: string, candidate: string): boolean {
const pathFromParent = relative(resolve(parent), resolve(candidate));
return (
pathFromParent.length > 0 &&
pathFromParent !== '..' &&
!pathFromParent.startsWith(`..${sep}`) &&
!isAbsolute(pathFromParent)
);
}
async function assertMissing(path: string): Promise<void> {
try {
await access(path);
} catch {
return;
}
throw new Error(`Performance output already exists: ${path}`);
}
function isMissingPath(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
error.code === 'ENOENT'
);
}
@@ -0,0 +1,250 @@
import type {
MainTimelineRecord,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
type InitialImportDatabaseOperation =
| 'DB_GET_APP_PLAYLIST'
| 'DB_UPSERT_APP_PLAYLIST';
export interface M3uImportIpcAttribution {
readonly databaseWorkerToMainCloneProxyMs: number;
readonly ipcStructuredCloneProxyMs: number;
readonly mainToDatabaseWorkerCloneProxyMs: number;
readonly mainToRendererCloneProxyMs: number;
readonly rendererToMainCloneProxyMs: number;
}
interface RequestBoundaryAttribution {
readonly mainToDatabaseWorkerMs: number;
readonly mainToRendererMs: number;
readonly rendererToMainMs: number;
readonly successEpochMs: number;
readonly workerToMainMs: number;
}
export function deriveM3uImportIpcAttribution(input: {
readonly get: WorkerRequestPerformanceMetrics;
readonly getReadStartedEpochMs: number;
readonly importDispatchEndedEpochMs: number;
readonly importDispatchStartedEpochMs: number;
readonly normalizeEndedEpochMs: number;
readonly playlistId: string;
readonly publishChannelsStartedEpochMs: number;
readonly serializeStartedEpochMs: number;
readonly timeline: readonly MainTimelineRecord[];
readonly upsert: WorkerRequestPerformanceMetrics;
}): M3uImportIpcAttribution {
requireOrdered(
input.normalizeEndedEpochMs,
input.importDispatchStartedEpochMs
);
requireOrdered(
input.importDispatchStartedEpochMs,
input.importDispatchEndedEpochMs
);
const upsert = deriveRequestBoundaryAttribution({
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: input.playlistId,
request: input.upsert,
timeline: input.timeline,
workPhaseStartedEpochMs: input.serializeStartedEpochMs,
});
requireOrdered(input.importDispatchEndedEpochMs, requireSource(input.upsert));
const get = deriveRequestBoundaryAttribution({
operation: 'DB_GET_APP_PLAYLIST',
playlistId: input.playlistId,
request: input.get,
timeline: input.timeline,
workPhaseStartedEpochMs: input.getReadStartedEpochMs,
});
requireOrdered(upsert.successEpochMs, requireSource(input.get));
requireOrdered(
get.successEpochMs,
input.publishChannelsStartedEpochMs
);
const initialMainToRendererMs = difference(
input.importDispatchStartedEpochMs,
input.normalizeEndedEpochMs
);
const mainToRendererCloneProxyMs =
initialMainToRendererMs +
upsert.mainToRendererMs +
get.mainToRendererMs;
const rendererToMainCloneProxyMs =
upsert.rendererToMainMs + get.rendererToMainMs;
const mainToDatabaseWorkerCloneProxyMs =
upsert.mainToDatabaseWorkerMs + get.mainToDatabaseWorkerMs;
const databaseWorkerToMainCloneProxyMs =
upsert.workerToMainMs + get.workerToMainMs;
return Object.freeze({
databaseWorkerToMainCloneProxyMs,
ipcStructuredCloneProxyMs:
mainToRendererCloneProxyMs +
rendererToMainCloneProxyMs +
mainToDatabaseWorkerCloneProxyMs +
databaseWorkerToMainCloneProxyMs,
mainToDatabaseWorkerCloneProxyMs,
mainToRendererCloneProxyMs,
rendererToMainCloneProxyMs,
});
}
function deriveRequestBoundaryAttribution(input: {
readonly operation: InitialImportDatabaseOperation;
readonly playlistId: string;
readonly request: WorkerRequestPerformanceMetrics;
readonly timeline: readonly MainTimelineRecord[];
readonly workPhaseStartedEpochMs: number;
}): RequestBoundaryAttribution {
requireInitialImportPreloadIdentity(input.request, input.operation);
const requestTimeline = requireTimelineRecord(
input.timeline,
'db-request',
input.request,
input.operation,
input.playlistId
);
requireTimelineRecord(
input.timeline,
'db-response',
input.request,
input.operation,
input.playlistId
);
const successTimeline = requireTimelineRecord(
input.timeline,
'preload-performance-success',
input.request,
input.operation,
input.playlistId
);
const sourceEpochMs = requireSource(input.request);
const requestReceivedEpochMs = requireTimestamp(
input.request.requestReceivedEpochMs
);
const workStartedEpochMs = requireTimestamp(
input.request.workStartedEpochMs
);
const workEndedEpochMs = requireTimestamp(input.request.workEndedEpochMs);
const responsePostedEpochMs = requireTimestamp(
input.request.responsePostedEpochMs
);
const responseEpochMs = requireTimestamp(input.request.responseEpochMs);
const successEpochMs = requireTimestamp(successTimeline.sourceEpochMs);
requireOrdered(sourceEpochMs, requestTimeline.epochMs);
requireOrdered(requestTimeline.epochMs, requestReceivedEpochMs);
requireOrdered(requestReceivedEpochMs, workStartedEpochMs);
requireOrdered(workStartedEpochMs, input.workPhaseStartedEpochMs);
requireOrdered(input.workPhaseStartedEpochMs, workEndedEpochMs);
requireOrdered(workEndedEpochMs, responsePostedEpochMs);
requireOrdered(responsePostedEpochMs, responseEpochMs);
requireOrdered(responseEpochMs, successEpochMs);
return {
mainToDatabaseWorkerMs: difference(
requestReceivedEpochMs,
requestTimeline.epochMs
),
mainToRendererMs: difference(successEpochMs, responseEpochMs),
rendererToMainMs: difference(requestTimeline.epochMs, sourceEpochMs),
successEpochMs,
workerToMainMs: difference(responseEpochMs, responsePostedEpochMs),
};
}
function requireTimelineRecord(
timeline: readonly MainTimelineRecord[],
type: 'db-request' | 'db-response' | 'preload-performance-success',
request: WorkerRequestPerformanceMetrics,
operation: InitialImportDatabaseOperation,
playlistId: string
): MainTimelineRecord {
if (!request.requestId || !Number.isSafeInteger(request.ipcCallId)) {
throw new Error('database-request-identity-missing');
}
const matches = timeline.filter((record) => {
if (
record.type !== type ||
record.operation !== operation ||
record.playlistId !== playlistId
) {
return false;
}
if (type === 'preload-performance-success') {
return (
record.ipcCallId === request.ipcCallId &&
record.sourceEpochMs !== undefined &&
record.success === true
);
}
return (
record.requestId === request.requestId &&
(type !== 'db-response' || record.success === true)
);
});
if (matches.length !== 1 || !matches[0]) {
throw new Error(`database-${type}-timeline-correlation`);
}
const record = matches[0];
if (
type === 'db-request' &&
(record.ipcCallId !== request.ipcCallId ||
record.sourceEpochMs !== request.sourceEpochMs)
) {
throw new Error('database-request-timeline-identity');
}
return record;
}
function requireInitialImportPreloadIdentity(
request: WorkerRequestPerformanceMetrics,
operation: InitialImportDatabaseOperation
): void {
if (
request.operation !== operation ||
!request.success ||
request.performanceCaptureUnavailableReason !== null ||
!Number.isSafeInteger(request.ipcCallId) ||
Number(request.ipcCallId) < 1 ||
typeof request.sourceEpochMs !== 'number' ||
!Number.isFinite(request.sourceEpochMs) ||
request.sourceEpochMs < 0 ||
request.operationId !== null ||
request.operationIdUnavailableReason !==
'preload-performance-marker-uncorrelated'
) {
throw new Error('database-request-preload-identity');
}
}
function requireSource(request: WorkerRequestPerformanceMetrics): number {
return requireTimestamp(request.sourceEpochMs);
}
function requireTimestamp(value: number | null | undefined): number {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
throw new Error('database-request-timestamp-missing');
}
return value;
}
function requireOrdered(startEpochMs: number, endEpochMs: number): void {
if (
!Number.isFinite(startEpochMs) ||
!Number.isFinite(endEpochMs) ||
endEpochMs < startEpochMs
) {
throw new Error('phase-order');
}
}
function difference(endEpochMs: number, startEpochMs: number): number {
requireOrdered(startEpochMs, endEpochMs);
return endEpochMs - startEpochMs;
}
@@ -0,0 +1,181 @@
import { once } from 'node:events';
import { createWriteStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { CDPSession } from '@playwright/test';
interface TraceData {
readonly value: readonly unknown[];
}
export interface RendererArtifactCapture {
beginTraceOutput(): void;
readonly cpuProfilePath: string | null;
finishTraceOutput(): Promise<void>;
readonly heapSnapshotPath: string | null;
readonly onTraceComplete: () => void;
readonly onTraceData: (data: TraceData) => void;
readonly traceComplete: Promise<void>;
readonly tracePath: string | null;
}
export function createRendererArtifactCapture(options: {
readonly diagnostic: boolean;
readonly outputDirectory: string;
}): RendererArtifactCapture {
const tracePath = options.diagnostic
? join(options.outputDirectory, 'renderer.trace.json')
: null;
const traceOutput = tracePath ? createWriteStream(tracePath) : null;
let traceOutputOpened = false;
let traceOutputFinish: Promise<void> | null = null;
let traceFirstEvent = true;
let completeTrace: (() => void) | null = null;
const traceComplete = new Promise<void>((resolve) => {
completeTrace = resolve;
});
return {
beginTraceOutput: () => {
if (
traceOutput &&
!traceOutputOpened &&
traceOutputFinish === null
) {
traceOutput.write('{"traceEvents":[');
traceOutputOpened = true;
}
},
cpuProfilePath: options.diagnostic
? join(options.outputDirectory, 'renderer.cpuprofile')
: null,
finishTraceOutput: () => {
traceOutputFinish ??= (async () => {
if (!traceOutput) {
return;
}
if (!traceOutputOpened) {
traceOutput.write('{"traceEvents":[');
traceOutputOpened = true;
}
if (!traceOutput.writableEnded) {
traceOutput.write(']}');
traceOutput.end();
}
if (!traceOutput.writableFinished) {
await once(traceOutput, 'finish');
}
})();
return traceOutputFinish;
},
heapSnapshotPath: options.diagnostic
? join(options.outputDirectory, 'renderer.heapsnapshot')
: null,
onTraceComplete: () => {
completeTrace?.();
completeTrace = null;
},
onTraceData: (data) => {
if (!traceOutputOpened || traceOutput?.writableEnded) {
return;
}
for (const event of data.value) {
traceOutput?.write(
`${traceFirstEvent ? '' : ','}${JSON.stringify(event)}`
);
traceFirstEvent = false;
}
},
traceComplete,
tracePath,
};
}
export async function startRendererDiagnosticCapture(
session: CDPSession,
artifacts: RendererArtifactCapture
): Promise<void> {
await session.send('Profiler.enable');
await session.send('Profiler.start');
artifacts.beginTraceOutput();
session.on('Tracing.dataCollected', artifacts.onTraceData);
session.on('Tracing.tracingComplete', artifacts.onTraceComplete);
await session.send('Tracing.start', {
categories: [
'blink.user_timing',
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'disabled-by-default-v8.cpu_profiler',
'v8',
].join(','),
options: 'sampling-frequency=1000',
transferMode: 'ReportEvents',
});
}
export async function stopRendererDiagnosticCapture(
session: CDPSession,
artifacts: RendererArtifactCapture
): Promise<void> {
try {
const cpuProfile = await session.send('Profiler.stop');
await writeFile(
requirePath(artifacts.cpuProfilePath, 'renderer CPU profile'),
JSON.stringify(cpuProfile['profile']),
'utf8'
);
await session.send('Tracing.end');
await artifacts.traceComplete;
} finally {
disposeRendererDiagnosticListeners(session, artifacts);
await artifacts.finishTraceOutput();
}
}
export async function disposeRendererDiagnosticCapture(
session: CDPSession,
artifacts: RendererArtifactCapture
): Promise<void> {
disposeRendererDiagnosticListeners(session, artifacts);
await artifacts.finishTraceOutput();
}
export async function takeRendererHeapSnapshot(
session: CDPSession,
path: string | null
): Promise<void> {
const output = createWriteStream(
requirePath(path, 'renderer heap snapshot')
);
const onChunk = ({ chunk }: { readonly chunk: string }): void => {
output.write(chunk);
};
session.on('HeapProfiler.addHeapSnapshotChunk', onChunk);
try {
await session.send('HeapProfiler.takeHeapSnapshot', {
reportProgress: false,
});
} finally {
session.off('HeapProfiler.addHeapSnapshotChunk', onChunk);
output.end();
if (!output.writableFinished) {
await once(output, 'finish');
}
}
}
function disposeRendererDiagnosticListeners(
session: CDPSession,
artifacts: RendererArtifactCapture
): void {
session.off('Tracing.dataCollected', artifacts.onTraceData);
session.off('Tracing.tracingComplete', artifacts.onTraceComplete);
artifacts.onTraceComplete();
}
function requirePath(value: string | null, label: string): string {
if (value === null) {
throw new Error(`${label} path is unavailable`);
}
return value;
}
@@ -0,0 +1,313 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based lifecycle test. */
import { RENDERER_PERFORMANCE_PHASE_HOOK_KEY } from '@iptvnator/shared/logging';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import { startM3uImportRendererCapture } from './m3u-import-renderer-capture';
import { M3U_IMPORT_RENDERER_STATE_KEY } from './m3u-import-renderer-probe';
test('idempotently disposes failed-iteration timers, probe, trace stream, listeners, and CDP session', async () => {
const browser = installFakeProbeEnvironment();
const outputDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-renderer-capture-dispose-')
);
const session = new FakeCdpSession();
const control = {
click: async () => undefined,
fill: async () => undefined,
first: () => control,
getByRole: () => control,
isDisabled: async () => false,
last: () => control,
waitFor: async () => undefined,
};
const page = {
...browser.page,
context: () => ({
newCDPSession: async () => session,
}),
getByRole: () => control,
locator: () => control,
};
try {
const capture = await startM3uImportRendererCapture(
page as never,
{
diagnostic: true,
outputDirectory,
playlistTitle: 'Synthetic',
playlistUrl:
'http://127.0.0.1:43210/synthetic-performance.m3u',
}
);
assert.equal(session.listenerCount('Tracing.dataCollected'), 1);
assert.equal(session.listenerCount('Tracing.tracingComplete'), 1);
await capture.dispose();
await capture.dispose();
assert.equal(session.detachCount, 1);
assert.equal(session.listenerCount('Tracing.dataCollected'), 0);
assert.equal(session.listenerCount('Tracing.tracingComplete'), 0);
assert.equal(browser.readState(), undefined);
const heapSamplesAfterDispose = session.heapUsageCalls;
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(session.heapUsageCalls, heapSamplesAfterDispose);
assert.deepEqual(
JSON.parse(
await readFile(
join(outputDirectory, 'renderer.trace.json'),
'utf8'
)
),
{ traceEvents: [] }
);
} finally {
browser.cleanup();
await rm(outputDirectory, { force: true, recursive: true });
}
});
test('failed-iteration disposal does not await a wedged renderer or CDP detach', async () => {
const browser = installFakeProbeEnvironment();
const outputDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-renderer-capture-wedged-')
);
const session = new FakeCdpSession({ hangDetach: true });
let evaluateCalls = 0;
const page = createFakePage(browser, session, async (callback, argument) => {
evaluateCalls += 1;
if (evaluateCalls > 1) {
return new Promise<never>(() => undefined);
}
return browser.page.evaluate(callback, argument);
});
try {
const capture = await startM3uImportRendererCapture(page as never, {
diagnostic: false,
outputDirectory,
playlistTitle: 'Synthetic',
playlistUrl:
'http://127.0.0.1:43210/synthetic-performance.m3u',
});
const outcome = await Promise.race([
capture.dispose().then(() => 'disposed'),
new Promise<'timeout'>((resolve) =>
setTimeout(() => resolve('timeout'), 150)
),
]);
assert.equal(outcome, 'disposed');
assert.equal(session.detachCount, 1);
const heapSamplesAfterDispose = session.heapUsageCalls;
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(session.heapUsageCalls, heapSamplesAfterDispose);
} finally {
browser.cleanup();
await rm(outputDirectory, { force: true, recursive: true });
}
});
test('rolls back partially started capture when Chromium tracing rejects', async () => {
const browser = installFakeProbeEnvironment();
const outputDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-renderer-capture-startup-')
);
const session = new FakeCdpSession({ failMethod: 'Tracing.start' });
const intervalToken = Object.freeze({});
const setIntervalDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'setInterval'
);
const clearIntervalDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
'clearInterval'
);
let clearIntervalCalls = 0;
Object.defineProperty(globalThis, 'setInterval', {
configurable: true,
value: () => intervalToken,
writable: true,
});
Object.defineProperty(globalThis, 'clearInterval', {
configurable: true,
value: (token: unknown) => {
if (token === intervalToken) {
clearIntervalCalls += 1;
}
},
writable: true,
});
try {
await assert.rejects(
startM3uImportRendererCapture(
createFakePage(browser, session) as never,
{
diagnostic: true,
outputDirectory,
playlistTitle: 'Synthetic',
playlistUrl:
'http://127.0.0.1:43210/synthetic-performance.m3u',
}
),
/synthetic Tracing\.start failure/
);
assert.equal(clearIntervalCalls, 1);
assert.equal(session.detachCount, 1);
assert.equal(session.listenerCount('Tracing.dataCollected'), 0);
assert.equal(session.listenerCount('Tracing.tracingComplete'), 0);
assert.equal(browser.readState(), undefined);
assert.deepEqual(
JSON.parse(
await readFile(
join(outputDirectory, 'renderer.trace.json'),
'utf8'
)
),
{ traceEvents: [] }
);
} finally {
restoreGlobal('setInterval', setIntervalDescriptor);
restoreGlobal('clearInterval', clearIntervalDescriptor);
browser.cleanup();
await rm(outputDirectory, { force: true, recursive: true });
}
});
class FakeCdpSession extends EventEmitter {
detachCount = 0;
heapUsageCalls = 0;
constructor(
private readonly options: {
readonly failMethod?: string;
readonly hangDetach?: boolean;
} = {}
) {
super();
}
async detach(): Promise<void> {
this.detachCount += 1;
if (this.options.hangDetach) {
return new Promise<never>(() => undefined);
}
}
async send(method: string): Promise<Record<string, unknown>> {
if (method === this.options.failMethod) {
throw new Error(`synthetic ${method} failure`);
}
if (method === 'Runtime.getHeapUsage') {
this.heapUsageCalls += 1;
return { usedSize: 1_024 };
}
return {};
}
}
function installFakeProbeEnvironment() {
const descriptors = new Map<string, PropertyDescriptor | undefined>();
let nextFrameId = 1;
const replaceGlobal = (key: string, value: unknown): void => {
descriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
Object.defineProperty(globalThis, key, {
configurable: true,
value,
writable: true,
});
};
class FakePerformanceObserver {
static readonly supportedEntryTypes = ['longtask'];
disconnect(): undefined {
return undefined;
}
observe(): undefined {
return undefined;
}
takeRecords(): PerformanceEntry[] {
return [];
}
}
replaceGlobal('performance', { now: () => 0, timeOrigin: 0 });
replaceGlobal('PerformanceObserver', FakePerformanceObserver);
replaceGlobal('cancelAnimationFrame', () => undefined);
replaceGlobal('requestAnimationFrame', () => nextFrameId++);
replaceGlobal('location', { pathname: '/' });
replaceGlobal('document', { querySelector: () => null });
return {
cleanup: () => {
for (const [key, descriptor] of descriptors) {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
} else {
Reflect.deleteProperty(globalThis, key);
}
}
Reflect.deleteProperty(
globalThis,
Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY)
);
Reflect.deleteProperty(
globalThis,
M3U_IMPORT_RENDERER_STATE_KEY
);
},
page: {
evaluate: async (
callback: (argument: never) => unknown,
argument: never
) => callback(argument),
},
readState: () =>
(globalThis as Record<string, unknown>)[
M3U_IMPORT_RENDERER_STATE_KEY
],
};
}
function createFakePage(
browser: ReturnType<typeof installFakeProbeEnvironment>,
session: FakeCdpSession,
evaluate = browser.page.evaluate
) {
const control = {
click: async () => undefined,
fill: async () => undefined,
first: () => control,
getByRole: () => control,
isDisabled: async () => false,
last: () => control,
waitFor: async () => undefined,
};
return {
...browser.page,
context: () => ({
newCDPSession: async () => session,
}),
evaluate,
getByRole: () => control,
locator: () => control,
};
}
function restoreGlobal(
key: 'clearInterval' | 'setInterval',
descriptor: PropertyDescriptor | undefined
): void {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
} else {
Reflect.deleteProperty(globalThis, key);
}
}
@@ -0,0 +1,389 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import { RENDERER_PERFORMANCE_PHASE_HOOK_KEY } from '@iptvnator/shared/logging';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
interface ImportProbe {
firstChannelVisibleEpochMs: number | null;
frameGapsMs: readonly number[];
heartbeatDelaysMs: readonly number[];
longTasksMs: readonly number[];
operationStartEpochMs: number;
performancePhaseEvents: readonly unknown[];
playlistId: string | null;
routeReadyEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintedEpochMs: number | null;
}
interface RendererCaptureModule {
advanceM3uImportHeartbeatDeadline?: (
deadlineEpochMs: number,
nowEpochMs: number
) => number;
assertCompleteM3uImportRendererProbe?: (probe: ImportProbe) => void;
startM3uImportRendererCapture?: unknown;
}
interface RendererProbeModule {
M3U_IMPORT_RENDERER_STATE_KEY?: string;
installM3uImportRendererProbe?: (page: unknown) => Promise<void>;
recordM3uImportHeartbeat?: (
page: unknown,
deadlineEpochMs: number
) => Promise<void>;
stopM3uImportRendererProbe?: (page: unknown) => Promise<ImportProbe>;
}
interface FakeProbeState {
frameGapsMs: number[];
heartbeatDelaysMs: number[];
operationStartEpochMs: number;
operationStartMonotonicMs: number;
terminalEpochMs: number | null;
}
const captureUrl = new URL('./m3u-import-renderer-capture.ts', import.meta.url);
const artifactsUrl = new URL(
'./m3u-import-renderer-artifacts.ts',
import.meta.url
);
const probeUrl = new URL('./m3u-import-renderer-probe.ts', import.meta.url);
const captureModulePromise = import(captureUrl.href)
.then((module) => module as RendererCaptureModule)
.catch(() => null);
const probeModulePromise = import(probeUrl.href)
.then((module) => module as unknown as RendererProbeModule)
.catch(() => null);
function installFakeBrowserEnvironment() {
const descriptors = new Map<string, PropertyDescriptor | undefined>();
const frames = new Map<number, (timestamp: number) => void>();
let nextFrameId = 1;
let nowMs = 0;
let observerRecords: PerformanceEntry[] | null = null;
const replaceGlobal = (key: string, value: unknown): void => {
descriptors.set(key, Object.getOwnPropertyDescriptor(globalThis, key));
Object.defineProperty(globalThis, key, {
configurable: true,
value,
writable: true,
});
};
class FakePerformanceObserver {
static readonly supportedEntryTypes = ['longtask'];
constructor() {
observerRecords = [];
}
disconnect(): undefined {
return undefined;
}
observe(): undefined {
return undefined;
}
takeRecords(): PerformanceEntry[] {
return observerRecords?.splice(0) ?? [];
}
}
replaceGlobal('performance', { now: () => nowMs, timeOrigin: 0 });
replaceGlobal('PerformanceObserver', FakePerformanceObserver);
replaceGlobal('cancelAnimationFrame', (requestId: number) => {
frames.delete(requestId);
});
replaceGlobal(
'requestAnimationFrame',
(callback: (timestamp: number) => void) => {
const requestId = nextFrameId++;
frames.set(requestId, callback);
return requestId;
}
);
replaceGlobal('location', { pathname: '/' });
replaceGlobal('document', { querySelector: () => null });
return {
cleanup: () => {
for (const [key, descriptor] of descriptors) {
if (descriptor) {
Object.defineProperty(globalThis, key, descriptor);
} else {
Reflect.deleteProperty(globalThis, key);
}
}
Reflect.deleteProperty(
globalThis,
Symbol.for(RENDERER_PERFORMANCE_PHASE_HOOK_KEY)
);
},
getObserver: () => observerRecords,
page: {
evaluate: async (
callback: (argument: unknown) => unknown,
argument: unknown
) => callback(argument),
},
readState: (stateKey: string) =>
(globalThis as Record<string, unknown>)[stateKey] as FakeProbeState,
runFrame: (timestamp: number) => {
nowMs = timestamp;
for (const [requestId, callback] of [...frames]) {
if (frames.delete(requestId)) {
callback(timestamp);
}
}
},
setNow: (value: number) => {
nowMs = value;
},
showTerminal: () => {
(
globalThis as unknown as {
location: { pathname: string };
}
).location.pathname =
'/Users/test/iptvnator/dist/apps/web/workspace/playlists/synthetic-id/all';
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: { querySelector: () => ({}) },
writable: true,
});
},
};
}
function readSource(url: URL): string | null {
try {
return readFileSync(url, 'utf8');
} catch {
return null;
}
}
function completeProbe(): ImportProbe {
return {
firstChannelVisibleEpochMs: 130,
frameGapsMs: [16, 48],
heartbeatDelaysMs: [0, 12],
longTasksMs: [72],
operationStartEpochMs: 100,
performancePhaseEvents: [],
playlistId: 'synthetic-id',
routeReadyEpochMs: 120,
terminalEpochMs: 150,
uiPaintedEpochMs: 150,
};
}
test('exports the formal M3U import renderer collector', async () => {
const module = await captureModulePromise;
assert.ok(module, 'M3U import renderer capture module must exist');
assert.equal(typeof module.startM3uImportRendererCapture, 'function');
assert.equal(
typeof module.assertCompleteM3uImportRendererProbe,
'function'
);
});
test('prepares the URL dialog before starting any measured capture', () => {
const source = readSource(captureUrl);
assert.ok(source, 'M3U import renderer capture source must exist');
const dialogPreparation = source.indexOf(
'await prepareM3uImportDialog(page, options)'
);
const cdpSession = source.indexOf('newCDPSession(page)');
const initialHeapSample = source.indexOf('await sampleHeap()');
assert.ok(dialogPreparation >= 0);
assert.ok(cdpSession > dialogPreparation);
assert.ok(initialHeapSample > cdpSession);
});
test('captures heap samples in every run and raw CPU, trace, and heap artifacts only in diagnostics', () => {
const source = readSource(captureUrl);
const artifactSource = readSource(artifactsUrl);
assert.ok(source);
assert.ok(artifactSource);
assert.match(source, /setInterval\(\(\) => void sampleHeap\(\), 20\)/);
assert.match(source, /Runtime\.getHeapUsage/);
assert.match(source, /HeapProfiler\.collectGarbage/);
assert.match(
artifactSource,
/cpuProfilePath: options\.diagnostic[\s\S]*renderer\.cpuprofile/
);
assert.match(artifactSource, /Profiler\.start/);
assert.match(artifactSource, /Tracing\.start/);
assert.match(artifactSource, /HeapProfiler\.takeHeapSnapshot/);
});
test('clips drained long tasks and heartbeat samples to the operation window', async () => {
const module = await probeModulePromise;
assert.ok(module);
assert.equal(typeof module.installM3uImportRendererProbe, 'function');
assert.equal(typeof module.recordM3uImportHeartbeat, 'function');
assert.equal(typeof module.stopM3uImportRendererProbe, 'function');
assert.equal(typeof module.M3U_IMPORT_RENDERER_STATE_KEY, 'string');
const browser = installFakeBrowserEnvironment();
try {
await module.installM3uImportRendererProbe?.(browser.page);
const state = browser.readState(
module.M3U_IMPORT_RENDERER_STATE_KEY ?? ''
);
state.operationStartEpochMs = 100;
state.operationStartMonotonicMs = 100;
state.terminalEpochMs = 200;
const observerRecords = browser.getObserver();
assert.ok(observerRecords);
observerRecords.push(
{ duration: 40, startTime: 80 } as PerformanceEntry,
{ duration: 30, startTime: 190 } as PerformanceEntry,
{ duration: 30, startTime: 210 } as PerformanceEntry
);
browser.setNow(250);
await module.recordM3uImportHeartbeat?.(browser.page, 90);
await module.recordM3uImportHeartbeat?.(browser.page, 150);
await module.recordM3uImportHeartbeat?.(browser.page, 210);
const metrics = await module.stopM3uImportRendererProbe?.(browser.page);
assert.deepEqual(metrics?.heartbeatDelaysMs, [50]);
assert.deepEqual(metrics?.longTasksMs, [20, 10]);
} finally {
browser.cleanup();
}
});
test('records the terminal animation-frame gap exactly once', async () => {
const module = await probeModulePromise;
assert.ok(module?.installM3uImportRendererProbe);
assert.equal(typeof module.M3U_IMPORT_RENDERER_STATE_KEY, 'string');
const browser = installFakeBrowserEnvironment();
try {
await module.installM3uImportRendererProbe(browser.page);
browser.runFrame(80);
const state = browser.readState(
module.M3U_IMPORT_RENDERER_STATE_KEY ?? ''
);
state.operationStartEpochMs = 100;
state.operationStartMonotonicMs = 100;
browser.showTerminal();
browser.runFrame(120);
browser.runFrame(140);
browser.runFrame(160);
assert.equal(state.terminalEpochMs, 160);
assert.deepEqual(state.frameGapsMs, [20, 20, 20]);
await module.stopM3uImportRendererProbe?.(browser.page);
} finally {
browser.cleanup();
}
});
test('advances the external heartbeat on its fixed 50ms deadline grid', async () => {
const module = await captureModulePromise;
assert.ok(module);
const advance = module.advanceM3uImportHeartbeatDeadline;
assert.equal(typeof advance, 'function');
assert.equal(advance?.(1_000, 1_005), 1_050);
assert.equal(advance?.(1_000, 1_060), 1_100);
assert.equal(advance?.(1_000, 1_200), 1_250);
});
test('installs the production renderer phase hook and returns its raw events', () => {
const source = readSource(probeUrl);
assert.ok(source);
assert.match(source, /RENDERER_PERFORMANCE_PHASE_HOOK_KEY/);
assert.match(source, /Symbol\.for\(hookKey\)/);
assert.match(source, /performancePhaseEvents\.push\(event\)/);
assert.match(
source,
/performancePhaseEvents: \[\.\.\.state\.performancePhaseEvents\]/
);
assert.match(source, /delete target\[hookSymbol\]/);
});
test('timestamps the click immediately before final Add and requires route, first item, and two animation frames', () => {
const source = readSource(probeUrl);
assert.ok(source);
const operationStart = source.indexOf(
'state.operationStartMonotonicMs = performance.now()'
);
const click = source.indexOf('button.click()', operationStart);
assert.ok(operationStart >= 0);
assert.ok(click > operationStart);
assert.ok(source.includes('/\\/workspace\\/playlists\\/([^/]+)\\/all$/'));
assert.match(source, /\[data-test-id="channel-item"\]/);
const firstPaintFrame = source.indexOf(
'state.uiPaintFrameRequestId = requestAnimationFrame'
);
const secondPaintFrame = source.indexOf(
'state.uiPaintFrameRequestId = requestAnimationFrame',
firstPaintFrame + 1
);
assert.ok(firstPaintFrame >= 0);
assert.ok(secondPaintFrame > firstPaintFrame);
for (const phase of [
'operationStartEpochMs',
'routeReadyEpochMs',
'firstChannelVisibleEpochMs',
'uiPaintedEpochMs',
'terminalEpochMs',
]) {
assert.match(source, new RegExp(phase));
}
});
test('fails closed when any terminal proof or the probe itself is missing', async () => {
const module = await captureModulePromise;
assert.ok(module);
const assertComplete = module.assertCompleteM3uImportRendererProbe;
assert.equal(typeof assertComplete, 'function');
assert.doesNotThrow(() => assertComplete?.(completeProbe()));
for (const field of [
'playlistId',
'routeReadyEpochMs',
'firstChannelVisibleEpochMs',
'uiPaintedEpochMs',
'terminalEpochMs',
] as const) {
const incomplete = { ...completeProbe(), [field]: null };
assert.throws(
() => assertComplete?.(incomplete),
/m3u-import-renderer-probe-incomplete/
);
}
assert.throws(
() =>
assertComplete?.({
...completeProbe(),
operationStartEpochMs: 0,
}),
/m3u-import-renderer-probe-incomplete/
);
});
test('clears the harness heap sampler even when the renderer probe is missing', () => {
const source = readSource(captureUrl);
assert.ok(source);
const backgroundStop = source.indexOf(
'await stopBackgroundWork()',
source.indexOf('const stopCapture')
);
const probeStop = source.indexOf('probe = await stopProbe()', backgroundStop);
assert.match(source, /clearInterval\(heapSampleTimer\)/);
assert.ok(backgroundStop >= 0);
assert.ok(probeStop > backgroundStop);
});
@@ -0,0 +1,366 @@
import type { CDPSession, Page } from '@playwright/test';
import type { NumericDistribution } from './m3u-refresh-cancellation-contract';
import {
createRendererArtifactCapture,
disposeRendererDiagnosticCapture,
type RendererArtifactCapture,
startRendererDiagnosticCapture,
stopRendererDiagnosticCapture,
takeRendererHeapSnapshot,
} from './m3u-import-renderer-artifacts';
import {
installM3uImportRendererProbe,
type M3uImportRendererProbeMetrics,
prepareM3uImportDialog,
recordM3uImportHeartbeat,
stopM3uImportRendererProbe,
triggerM3uImport,
waitForM3uImportTerminal,
} from './m3u-import-renderer-probe';
import { summarizeNumbers } from './performance-statistics';
const HEARTBEAT_INTERVAL_MS = 50;
export interface M3uImportRendererCaptureStartOptions {
readonly diagnostic: boolean;
readonly outputDirectory: string;
readonly playlistTitle: string;
readonly playlistUrl: string;
}
export interface M3uImportRendererPhaseTimestamps {
readonly firstChannelVisibleEpochMs: number;
readonly operationStartEpochMs: number;
readonly routeReadyEpochMs: number;
readonly terminalEpochMs: number;
readonly uiPaintedEpochMs: number;
}
export interface M3uImportRendererCaptureMetrics {
readonly cpuProfilePath: string | null;
readonly frameGap: NumericDistribution;
readonly heapSnapshotPath: string | null;
readonly heartbeatDelay: NumericDistribution;
readonly longTask: NumericDistribution;
readonly peakHeapUsedBytes: number;
readonly phaseTimestamps: M3uImportRendererPhaseTimestamps;
readonly postGcHeapUsedBytes: number;
readonly probe: M3uImportRendererProbeMetrics;
readonly tracePath: string | null;
}
export interface RunningM3uImportRendererCapture {
readonly session: CDPSession;
dispose(): Promise<void>;
stop(): Promise<M3uImportRendererCaptureMetrics>;
triggerImport(): Promise<number>;
waitForTerminal(): Promise<void>;
}
interface HeapUsageResult {
readonly usedSize: number;
}
export async function startM3uImportRendererCapture(
page: Page,
options: M3uImportRendererCaptureStartOptions
): Promise<RunningM3uImportRendererCapture> {
await prepareM3uImportDialog(page, options);
const session = await page.context().newCDPSession(page);
let probeInstalled = false;
let heapSampleTimer: NodeJS.Timeout | null = null;
let artifacts: RendererArtifactCapture | null = null;
const heapSamples: number[] = [];
let heapSampleError: unknown = null;
let heapSamplePromise: Promise<void> | null = null;
const sampleHeap = (): Promise<void> => {
heapSamplePromise ??= (async () => {
try {
const sample = (await session.send(
'Runtime.getHeapUsage'
)) as HeapUsageResult;
if (Number.isFinite(sample.usedSize)) {
heapSamples.push(sample.usedSize);
}
} catch (error) {
heapSampleError ??= error;
} finally {
heapSamplePromise = null;
}
})();
return heapSamplePromise;
};
try {
await session.send('Runtime.enable');
await session.send('Performance.enable');
await installM3uImportRendererProbe(page);
probeInstalled = true;
await sampleHeap();
throwCaptureError(heapSampleError, 'Renderer heap sampling failed');
heapSampleTimer = setInterval(() => void sampleHeap(), 20);
artifacts = createRendererArtifactCapture(options);
if (options.diagnostic) {
await startRendererDiagnosticCapture(session, artifacts);
}
} catch (error) {
if (heapSampleTimer) {
clearInterval(heapSampleTimer);
heapSampleTimer = null;
}
if (artifacts && options.diagnostic) {
await disposeRendererDiagnosticCapture(
session,
artifacts
).catch(() => undefined);
}
if (probeInstalled) {
continueBestEffort(() => stopM3uImportRendererProbe(page));
}
continueBestEffort(() => session.detach());
throw error;
}
if (!artifacts || !heapSampleTimer) {
throw new Error('M3U import renderer capture initialization failed');
}
let heartbeatDeadlineEpochMs: number | null = null;
let heartbeatError: unknown = null;
let heartbeatInFlight: Promise<void> | null = null;
let heartbeatTimer: NodeJS.Timeout | null = null;
let stopped = false;
let backgroundStopPromise: Promise<void> | null = null;
let probeStopPromise: Promise<M3uImportRendererProbeMetrics> | null = null;
let sessionDetachPromise: Promise<void> | null = null;
let abortPromise: Promise<void> | null = null;
let stopPromise: Promise<M3uImportRendererCaptureMetrics> | null = null;
let stopRequested = false;
let disposeRequested = false;
const stopSchedulingBackgroundWork = (): void => {
stopped = true;
if (heartbeatTimer) {
clearTimeout(heartbeatTimer);
heartbeatTimer = null;
}
if (heapSampleTimer) {
clearInterval(heapSampleTimer);
heapSampleTimer = null;
}
};
const scheduleHeartbeat = (): void => {
if (stopped || heartbeatDeadlineEpochMs === null) {
return;
}
const deadline = heartbeatDeadlineEpochMs;
heartbeatTimer = setTimeout(
() => {
heartbeatTimer = null;
heartbeatInFlight = recordM3uImportHeartbeat(page, deadline)
.catch((error: unknown) => {
heartbeatError ??= error;
})
.finally(() => {
heartbeatInFlight = null;
if (stopped) {
return;
}
heartbeatDeadlineEpochMs =
advanceM3uImportHeartbeatDeadline(
deadline,
Date.now()
);
scheduleHeartbeat();
});
},
Math.max(0, deadline - Date.now())
);
};
const stopBackgroundWork = (): Promise<void> => {
backgroundStopPromise ??= (async () => {
stopSchedulingBackgroundWork();
await heartbeatInFlight;
await heapSamplePromise;
await sampleHeap();
})();
return backgroundStopPromise;
};
const stopProbe = (): Promise<M3uImportRendererProbeMetrics> => {
probeStopPromise ??= stopM3uImportRendererProbe(page);
return probeStopPromise;
};
const detachSession = (): Promise<void> => {
sessionDetachPromise ??= session.detach();
return sessionDetachPromise;
};
const abortResources = (): Promise<void> => {
abortPromise ??= (async () => {
stopSchedulingBackgroundWork();
continueBestEffort(() => heartbeatInFlight ?? Promise.resolve());
continueBestEffort(() => heapSamplePromise ?? Promise.resolve());
continueBestEffort(stopProbe);
if (options.diagnostic) {
await disposeRendererDiagnosticCapture(
session,
artifacts
).catch(() => undefined);
}
continueBestEffort(detachSession);
})();
return abortPromise;
};
const stopCapture =
async (): Promise<M3uImportRendererCaptureMetrics> => {
try {
await stopBackgroundWork();
let probe: M3uImportRendererProbeMetrics | null = null;
let probeError: unknown = null;
try {
probe = await stopProbe();
} catch (error) {
probeError = error;
}
if (options.diagnostic) {
await stopRendererDiagnosticCapture(session, artifacts);
}
await session.send('HeapProfiler.enable');
await session.send('HeapProfiler.collectGarbage');
const postGc = (await session.send(
'Runtime.getHeapUsage'
)) as HeapUsageResult;
if (options.diagnostic) {
await takeRendererHeapSnapshot(
session,
artifacts.heapSnapshotPath
);
}
await detachSession();
throwCaptureError(
heapSampleError,
'Renderer heap sampling failed'
);
throwCaptureError(heartbeatError, 'Renderer heartbeat failed');
throwCaptureError(probeError, 'Renderer probe stop failed');
assertCompleteM3uImportRendererProbe(probe);
return Object.freeze({
cpuProfilePath: artifacts.cpuProfilePath,
frameGap: summarizeNumbers(probe.frameGapsMs),
heapSnapshotPath: artifacts.heapSnapshotPath,
heartbeatDelay: summarizeNumbers(
probe.heartbeatDelaysMs
),
longTask: summarizeNumbers(probe.longTasksMs),
peakHeapUsedBytes: Math.max(0, ...heapSamples),
phaseTimestamps: Object.freeze({
firstChannelVisibleEpochMs:
probe.firstChannelVisibleEpochMs,
operationStartEpochMs: probe.operationStartEpochMs,
routeReadyEpochMs: probe.routeReadyEpochMs,
terminalEpochMs: probe.terminalEpochMs,
uiPaintedEpochMs: probe.uiPaintedEpochMs,
}),
postGcHeapUsedBytes: postGc.usedSize,
probe,
tracePath: artifacts.tracePath,
});
} catch (error) {
await abortResources();
throw error;
}
};
return Object.freeze({
dispose: async () => {
disposeRequested = true;
if (stopPromise) {
continueBestEffort(() => stopPromise as Promise<unknown>);
}
await abortResources();
},
session,
stop: () => {
if (stopRequested || disposeRequested) {
return Promise.reject(
new Error('M3U import renderer capture already stopped')
);
}
stopRequested = true;
stopPromise = stopCapture();
return stopPromise;
},
triggerImport: async () => {
const operationStartEpochMs = await triggerM3uImport(page);
heartbeatDeadlineEpochMs =
operationStartEpochMs + HEARTBEAT_INTERVAL_MS;
scheduleHeartbeat();
return operationStartEpochMs;
},
waitForTerminal: () => waitForM3uImportTerminal(page),
});
}
export function advanceM3uImportHeartbeatDeadline(
deadlineEpochMs: number,
nowEpochMs: number
): number {
const nextDeadline = deadlineEpochMs + HEARTBEAT_INTERVAL_MS;
if (nextDeadline > nowEpochMs) {
return nextDeadline;
}
const missedIntervals =
Math.floor((nowEpochMs - nextDeadline) / HEARTBEAT_INTERVAL_MS) + 1;
return nextDeadline + missedIntervals * HEARTBEAT_INTERVAL_MS;
}
export function assertCompleteM3uImportRendererProbe(
probe: M3uImportRendererProbeMetrics | null | undefined
): asserts probe is M3uImportRendererProbeMetrics & {
readonly firstChannelVisibleEpochMs: number;
readonly playlistId: string;
readonly routeReadyEpochMs: number;
readonly terminalEpochMs: number;
readonly uiPaintedEpochMs: number;
} {
if (
!probe ||
typeof probe.playlistId !== 'string' ||
probe.playlistId.length === 0 ||
!isPositiveFinite(probe.operationStartEpochMs) ||
!isPositiveFinite(probe.routeReadyEpochMs) ||
!isPositiveFinite(probe.firstChannelVisibleEpochMs) ||
!isPositiveFinite(probe.uiPaintedEpochMs) ||
!isPositiveFinite(probe.terminalEpochMs)
) {
throw new Error('m3u-import-renderer-probe-incomplete');
}
if (
probe.routeReadyEpochMs < probe.operationStartEpochMs ||
probe.firstChannelVisibleEpochMs < probe.routeReadyEpochMs ||
probe.uiPaintedEpochMs < probe.firstChannelVisibleEpochMs ||
probe.terminalEpochMs < probe.uiPaintedEpochMs
) {
throw new Error('m3u-import-renderer-probe-incomplete');
}
}
function throwCaptureError(error: unknown, label: string): void {
if (error instanceof Error) {
throw error;
}
if (error !== null) {
throw new Error(`${label}: ${String(error)}`);
}
}
function continueBestEffort(work: () => Promise<unknown>): void {
try {
void work().catch(() => undefined);
} catch {
// The owning Electron app is closed immediately after abort cleanup.
}
}
function isPositiveFinite(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value > 0;
}
@@ -0,0 +1,378 @@
import type { Page } from '@playwright/test';
import {
RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
type RendererPerformancePhaseEvent,
type RendererPerformancePhaseHook,
} from '@iptvnator/shared/logging';
export const M3U_IMPORT_RENDERER_STATE_KEY =
'__iptvnatorM3uImportRendererCapture';
export interface M3uImportDialogOptions {
readonly playlistTitle: string;
readonly playlistUrl: string;
}
export interface M3uImportRendererProbeMetrics {
readonly firstChannelVisibleEpochMs: number | null;
readonly frameGapsMs: readonly number[];
readonly heartbeatDelaysMs: readonly number[];
readonly longTasksMs: readonly number[];
readonly operationStartEpochMs: number;
readonly performancePhaseEvents: readonly RendererPerformancePhaseEvent[];
readonly playlistId: string | null;
readonly routeReadyEpochMs: number | null;
readonly terminalEpochMs: number | null;
readonly uiPaintedEpochMs: number | null;
}
export async function prepareM3uImportDialog(
page: Page,
options: M3uImportDialogOptions
): Promise<void> {
await page.getByRole('button', { name: 'Add playlist' }).first().click();
const dialog = page.locator('mat-dialog-container').last();
await dialog.waitFor({ state: 'visible' });
await dialog
.getByRole('textbox', { name: 'Playlist URL (m3u, m3u8)' })
.fill(options.playlistUrl);
await dialog
.getByRole('textbox', { name: 'Playlist title' })
.fill(options.playlistTitle);
const submit = dialog.getByRole('button', { name: 'Add playlist' }).last();
await submit.waitFor({ state: 'visible' });
if (await submit.isDisabled()) {
throw new Error('m3u-import-final-add-disabled');
}
}
export async function installM3uImportRendererProbe(page: Page): Promise<void> {
await page.evaluate(
({ hookKey, stateKey }) => {
interface ProbeState {
firstChannelVisibleEpochMs: number | null;
frameGapsMs: number[];
frameRequestId: number;
heartbeatDelaysMs: number[];
lastFrameAt: number | null;
longTaskObserver: PerformanceObserver | null;
longTasksMs: number[];
operationStartEpochMs: number;
operationStartMonotonicMs: number;
performancePhaseEvents: RendererPerformancePhaseEvent[];
playlistId: string | null;
recordFrameGap(timestamp: number): void;
recordLongTask(entry: PerformanceEntry): void;
routeReadyEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintFrameRequestId: number | null;
uiPaintedEpochMs: number | null;
}
const target = globalThis as unknown as Record<
PropertyKey,
unknown
>;
const hookSymbol = Symbol.for(hookKey);
if (
target[stateKey] !== undefined ||
target[hookSymbol] !== undefined
) {
throw new Error('m3u-import-renderer-probe-already-installed');
}
const epoch = (): number =>
performance.timeOrigin + performance.now();
const state: ProbeState = {
firstChannelVisibleEpochMs: null,
frameGapsMs: [],
frameRequestId: 0,
heartbeatDelaysMs: [],
lastFrameAt: null,
longTaskObserver: null,
longTasksMs: [],
operationStartEpochMs: 0,
operationStartMonotonicMs: 0,
performancePhaseEvents: [],
playlistId: null,
recordFrameGap: (timestamp) => {
const previousFrameAt = state.lastFrameAt;
state.lastFrameAt = timestamp;
if (
previousFrameAt === null ||
state.operationStartEpochMs <= 0 ||
state.terminalEpochMs !== null
) {
return;
}
const measuredFrameStart = Math.max(
previousFrameAt,
state.operationStartMonotonicMs
);
if (timestamp > measuredFrameStart) {
state.frameGapsMs.push(timestamp - measuredFrameStart);
}
},
recordLongTask: (entry) => {
const entryEpochMs =
performance.timeOrigin + entry.startTime;
const measuredStartEpochMs = Math.max(
entryEpochMs,
state.operationStartEpochMs
);
const measuredEndEpochMs = Math.min(
entryEpochMs + entry.duration,
state.terminalEpochMs ?? Number.POSITIVE_INFINITY
);
if (
state.operationStartEpochMs > 0 &&
measuredEndEpochMs > measuredStartEpochMs
) {
state.longTasksMs.push(
measuredEndEpochMs - measuredStartEpochMs
);
}
},
routeReadyEpochMs: null,
terminalEpochMs: null,
uiPaintFrameRequestId: null,
uiPaintedEpochMs: null,
};
const performancePhaseHook: RendererPerformancePhaseHook = (
event
) => {
state.performancePhaseEvents.push(event);
};
target[hookSymbol] = performancePhaseHook;
const terminalTarget = (): string | null => {
const route = location.pathname.match(
/\/workspace\/playlists\/([^/]+)\/all$/
);
if (
!route ||
!document.querySelector('[data-test-id="channel-item"]')
) {
return null;
}
return route[1] ?? null;
};
const inspectTerminal = (): void => {
if (
state.operationStartEpochMs <= 0 ||
state.terminalEpochMs !== null
) {
return;
}
const route = location.pathname.match(
/\/workspace\/playlists\/([^/]+)\/all$/
);
if (!route) {
return;
}
state.routeReadyEpochMs ??= epoch();
if (!document.querySelector('[data-test-id="channel-item"]')) {
return;
}
state.firstChannelVisibleEpochMs ??= epoch();
if (state.uiPaintFrameRequestId !== null) {
return;
}
state.uiPaintFrameRequestId = requestAnimationFrame(() => {
state.uiPaintFrameRequestId = requestAnimationFrame(
(terminalFrameAt) => {
state.uiPaintFrameRequestId = null;
const playlistId = terminalTarget();
if (playlistId === null) {
return;
}
state.recordFrameGap(terminalFrameAt);
state.playlistId = playlistId;
state.uiPaintedEpochMs = epoch();
state.terminalEpochMs = state.uiPaintedEpochMs;
cancelAnimationFrame(state.frameRequestId);
state.frameRequestId = 0;
for (const entry of state.longTaskObserver?.takeRecords() ??
[]) {
state.recordLongTask(entry);
}
state.longTaskObserver?.disconnect();
state.longTaskObserver = null;
}
);
});
};
const frame = (timestamp: number): void => {
state.recordFrameGap(timestamp);
if (state.operationStartEpochMs > 0) {
inspectTerminal();
}
state.frameRequestId =
state.terminalEpochMs === null
? requestAnimationFrame(frame)
: 0;
};
state.frameRequestId = requestAnimationFrame(frame);
if (
typeof PerformanceObserver === 'function' &&
PerformanceObserver.supportedEntryTypes.includes('longtask')
) {
state.longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
state.recordLongTask(entry);
}
});
state.longTaskObserver.observe({ entryTypes: ['longtask'] });
}
target[stateKey] = state;
},
{
hookKey: RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
stateKey: M3U_IMPORT_RENDERER_STATE_KEY,
}
);
}
export async function triggerM3uImport(page: Page): Promise<number> {
return page.evaluate((stateKey) => {
const target = globalThis as unknown as Record<string, unknown>;
const state = target[stateKey] as
| {
operationStartEpochMs: number;
operationStartMonotonicMs: number;
}
| undefined;
if (!state || state.operationStartEpochMs > 0) {
throw new Error('m3u-import-renderer-probe-unavailable');
}
const dialogs = Array.from(
document.querySelectorAll('mat-dialog-container')
);
const dialog = dialogs.at(-1);
const button = Array.from(dialog?.querySelectorAll('button') ?? [])
.reverse()
.find(
(candidate) =>
candidate.textContent?.trim() === 'Add playlist' &&
!candidate.disabled
);
if (!(button instanceof HTMLButtonElement)) {
throw new Error('m3u-import-final-add-unavailable');
}
state.operationStartMonotonicMs = performance.now();
state.operationStartEpochMs =
performance.timeOrigin + state.operationStartMonotonicMs;
button.click();
return state.operationStartEpochMs;
}, M3U_IMPORT_RENDERER_STATE_KEY);
}
export async function recordM3uImportHeartbeat(
page: Page,
deadlineEpochMs: number
): Promise<void> {
await page.evaluate(
({ deadlineEpochMs, stateKey }) => {
const target = globalThis as unknown as Record<string, unknown>;
const state = target[stateKey] as
| {
heartbeatDelaysMs: number[];
operationStartEpochMs: number;
terminalEpochMs: number | null;
}
| undefined;
if (!state || state.operationStartEpochMs <= 0) {
throw new Error('m3u-import-renderer-probe-unavailable');
}
if (
deadlineEpochMs >= state.operationStartEpochMs &&
(state.terminalEpochMs === null ||
deadlineEpochMs <= state.terminalEpochMs)
) {
const receivedEpochMs =
performance.timeOrigin + performance.now();
const measuredEpochMs =
state.terminalEpochMs === null
? receivedEpochMs
: Math.min(receivedEpochMs, state.terminalEpochMs);
state.heartbeatDelaysMs.push(
Math.max(0, measuredEpochMs - deadlineEpochMs)
);
}
},
{ deadlineEpochMs, stateKey: M3U_IMPORT_RENDERER_STATE_KEY }
);
}
export async function waitForM3uImportTerminal(page: Page): Promise<void> {
await page
.waitForFunction(
(stateKey) => {
const target = globalThis as unknown as Record<string, unknown>;
const state = target[stateKey] as
{ terminalEpochMs: number | null } | undefined;
return typeof state?.terminalEpochMs === 'number';
},
M3U_IMPORT_RENDERER_STATE_KEY,
{ timeout: 120_000 }
)
.then(() => undefined);
}
export async function stopM3uImportRendererProbe(
page: Page
): Promise<M3uImportRendererProbeMetrics> {
return page.evaluate(
({ hookKey, stateKey }) => {
const target = globalThis as unknown as Record<
PropertyKey,
unknown
>;
const hookSymbol = Symbol.for(hookKey);
const state = target[stateKey] as
| {
firstChannelVisibleEpochMs: number | null;
frameGapsMs: number[];
frameRequestId: number;
heartbeatDelaysMs: number[];
longTaskObserver: PerformanceObserver | null;
longTasksMs: number[];
operationStartEpochMs: number;
performancePhaseEvents: RendererPerformancePhaseEvent[];
playlistId: string | null;
recordLongTask(entry: PerformanceEntry): void;
routeReadyEpochMs: number | null;
terminalEpochMs: number | null;
uiPaintFrameRequestId: number | null;
uiPaintedEpochMs: number | null;
}
| undefined;
if (!state) {
throw new Error('m3u-import-renderer-probe-unavailable');
}
cancelAnimationFrame(state.frameRequestId);
if (state.uiPaintFrameRequestId !== null) {
cancelAnimationFrame(state.uiPaintFrameRequestId);
}
for (const entry of state.longTaskObserver?.takeRecords() ?? []) {
state.recordLongTask(entry);
}
state.longTaskObserver?.disconnect();
delete target[stateKey];
delete target[hookSymbol];
return {
firstChannelVisibleEpochMs: state.firstChannelVisibleEpochMs,
frameGapsMs: [...state.frameGapsMs],
heartbeatDelaysMs: [...state.heartbeatDelaysMs],
longTasksMs: [...state.longTasksMs],
operationStartEpochMs: state.operationStartEpochMs,
performancePhaseEvents: [...state.performancePhaseEvents],
playlistId: state.playlistId,
routeReadyEpochMs: state.routeReadyEpochMs,
terminalEpochMs: state.terminalEpochMs,
uiPaintedEpochMs: state.uiPaintedEpochMs,
};
},
{
hookKey: RENDERER_PERFORMANCE_PHASE_HOOK_KEY,
stateKey: M3U_IMPORT_RENDERER_STATE_KEY,
}
);
}
@@ -0,0 +1,338 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
import { deriveM3uImportPhaseMetrics } from './m3u-import-report';
import {
createTestMainCapture,
createTestRendererCapture,
TEST_M3U_CHANNEL_COUNT,
TEST_M3U_FIXTURE_BYTES,
} from './m3u-import-report.test-helpers';
describe('formal M3U import phase report', () => {
it('attributes every required phase without presenting indexes as zero work', () => {
const phases = deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: createTestMainCapture(),
renderer: createTestRendererCapture(),
});
assert.deepEqual(phases, {
angularRenderingMs: 10,
dataAcquireMs: 20,
databaseWorkerToMainCloneProxyMs: 12,
indexAndCommitMs: null,
indexAndCommitUnavailableReason:
'indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write',
ipcStructuredCloneProxyMs: 44,
mainToDatabaseWorkerCloneProxyMs: 2,
mainToRendererCloneProxyMs: 28,
m3uParsingMs: 40,
normalizationMs: 10,
playlistDeserializationMs: 12,
playlistSerializationMs: 20,
rendererToMainCloneProxyMs: 2,
sqliteReadMs: 5,
sqliteWriteMs: 30,
storeImportDispatchMs: 2,
storePublishChannelsMs: 10,
storeUpdateMs: 12,
totalMs: 215,
});
assert.equal(
phases.ipcStructuredCloneProxyMs,
phases.mainToRendererCloneProxyMs +
phases.rendererToMainCloneProxyMs +
phases.mainToDatabaseWorkerCloneProxyMs +
phases.databaseWorkerToMainCloneProxyMs
);
});
it('fails closed when any structured-clone boundary is missing, ambiguous, or out of order', () => {
const main = createTestMainCapture();
const databaseWorker = main.workers[0];
const upsert = databaseWorker?.requests[0];
assert.ok(databaseWorker);
assert.ok(upsert);
for (const invalidMain of [
{
...main,
timeline: main.timeline.filter(
(event) => event.type !== 'db-request'
),
},
{
...main,
timeline: [
...main.timeline,
main.timeline.find((event) => event.type === 'db-request'),
].filter(
(event): event is (typeof main.timeline)[number] =>
event !== undefined
),
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
sourceEpochMs: 1080,
},
...databaseWorker.requests.slice(1),
],
},
],
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
requestReceivedEpochMs: 1078,
},
...databaseWorker.requests.slice(1),
],
},
],
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
ipcCallId: null,
operationIdUnavailableReason:
'preload-performance-marker-missing',
sourceEpochMs: null,
},
...databaseWorker.requests.slice(1),
],
},
],
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
workStartedEpochMs: 1079,
},
...databaseWorker.requests.slice(1),
],
},
],
},
]) {
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: invalidMain,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
}
});
it('fails closed when a required phase, worker identity, or fixture count is ambiguous', () => {
const missingNormalize = createTestMainCapture();
const missingTimeline = missingNormalize.timeline.filter(
(event) => event.phase !== 'normalize'
);
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...missingNormalize,
timeline: missingTimeline,
},
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...createTestMainCapture(),
workers: [
...createTestMainCapture().workers,
{
...createTestMainCapture().workers[0],
ordinal: 2,
},
],
},
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
const wrongBytes = createTestMainCapture();
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES + 1,
main: wrongBytes,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
});
it('requires the route-reload GET and both worker-to-renderer return paths', () => {
const main = createTestMainCapture();
const databaseWorker = main.workers[0];
const upsert = databaseWorker?.requests[0];
const get = databaseWorker?.requests[1];
assert.ok(databaseWorker);
assert.ok(upsert);
assert.ok(get);
const invalidCaptures: MainCaptureMetrics[] = [
{
...main,
workers: [
{
...databaseWorker,
requests: databaseWorker.requests.slice(0, 1),
},
],
},
{
...main,
timeline: main.timeline.filter(
(event) =>
!(
event.type === 'preload-performance-success' &&
event.operation === 'DB_GET_APP_PLAYLIST'
)
),
},
{
...main,
timeline: main.timeline.filter(
(event) =>
!(
event.type === 'preload-performance-success' &&
event.operation === 'DB_UPSERT_APP_PLAYLIST'
)
),
},
{
...main,
timeline: main.timeline.map((event) =>
event.type === 'preload-performance-success' &&
event.operation === 'DB_GET_APP_PLAYLIST'
? {
...event,
sourceEpochMs: 1196,
}
: event
),
},
{
...main,
workers: [
{
...databaseWorker,
requests: [
upsert,
{
...get,
responsePostedEpochMs: 1175,
},
],
},
],
},
];
for (const invalidMain of invalidCaptures) {
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: invalidMain,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
}
});
it('rejects playlist workers and cross-playlist database writes', () => {
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...createTestMainCapture(),
workers: [
...createTestMainCapture().workers,
{
kind: 'playlist-refresh.worker',
requests: [],
},
],
} as MainCaptureMetrics,
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
const main = createTestMainCapture();
const databaseWorker = main.workers[0];
assert.ok(databaseWorker);
const upsert = databaseWorker.requests[0];
assert.ok(upsert);
assert.throws(
() =>
deriveM3uImportPhaseMetrics({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
main: {
...main,
workers: [
{
...databaseWorker,
requests: [
{
...upsert,
playlistId: 'other-playlist',
},
],
},
],
},
renderer: createTestRendererCapture(),
}),
/m3u-import-phase-attribution-invalid/
);
});
});
@@ -0,0 +1,458 @@
/* eslint-disable max-lines -- The deterministic fixture keeps the full production-shaped import sequence visible. */
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import type { MainCaptureMetrics } from './m3u-refresh-cancellation-contract';
import type { M3uImportBenchmarkManifest } from './m3u-import-summary';
export const TEST_M3U_CHANNEL_COUNT = 10_000;
export const TEST_M3U_FIXTURE_BYTES = 1_024_000;
export function createTestMainCapture(): MainCaptureMetrics {
return {
timeline: [
mainPhase('start', 'data.acquire', 1001, null),
mainPhase('end', 'data.acquire', 1021, 20, {
byteCount: TEST_M3U_FIXTURE_BYTES,
}),
mainPhase('start', 'parse.m3u', 1021, null),
mainPhase('end', 'parse.m3u', 1061, 40, {
itemCount: TEST_M3U_CHANNEL_COUNT,
}),
mainPhase('start', 'normalize', 1061, null),
mainPhase('end', 'normalize', 1071, 10, {
itemCount: TEST_M3U_CHANNEL_COUNT,
}),
{
epochMs: 1080,
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-1',
sourceEpochMs: 1079,
type: 'db-request',
},
{
epochMs: 1137,
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-1',
success: true,
type: 'db-response',
},
{
epochMs: 1140,
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
playlistId: 'playlist-1',
sourceEpochMs: 1140,
success: true,
type: 'preload-performance-success',
},
{
epochMs: 1142,
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-2',
sourceEpochMs: 1141,
type: 'db-request',
},
{
epochMs: 1174,
operation: 'DB_GET_APP_PLAYLIST',
playlistId: 'playlist-1',
requestId: 'db-request-2',
success: true,
type: 'db-response',
},
{
epochMs: 1194,
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
playlistId: 'playlist-1',
sourceEpochMs: 1194,
success: true,
type: 'preload-performance-success',
},
],
workers: [
{
kind: 'database.worker',
requests: [
{
ipcCallId: 7,
operation: 'DB_UPSERT_APP_PLAYLIST',
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-uncorrelated',
performanceCaptureUnavailableReason: null,
phaseEvents: [
workerPhase(
'start',
'serialize.playlist',
1082,
null,
'db-request-1'
),
workerPhase(
'end',
'serialize.playlist',
1102,
20,
'db-request-1',
{
itemCount: TEST_M3U_CHANNEL_COUNT,
}
),
workerPhase(
'start',
'sqlite.write',
1102,
null,
'db-request-1'
),
workerPhase(
'end',
'sqlite.write',
1132,
30,
'db-request-1',
{
itemCount: 1,
}
),
],
playlistId: 'playlist-1',
requestId: 'db-request-1',
requestReceivedEpochMs: 1081,
responseEpochMs: 1137,
responsePostedEpochMs: 1135,
sourceEpochMs: 1079,
success: true,
workEndedEpochMs: 1133,
workStartedEpochMs: 1082,
},
{
ipcCallId: 8,
operation: 'DB_GET_APP_PLAYLIST',
operationId: null,
operationIdUnavailableReason:
'preload-performance-marker-uncorrelated',
performanceCaptureUnavailableReason: null,
phaseEvents: [
workerPhase(
'start',
'sqlite.read',
1144,
null,
'db-request-2'
),
workerPhase(
'end',
'sqlite.read',
1149,
5,
'db-request-2',
{
itemCount: 1,
}
),
workerPhase(
'start',
'deserialize.playlist',
1149,
null,
'db-request-2'
),
workerPhase(
'end',
'deserialize.playlist',
1161,
12,
'db-request-2',
{
itemCount: TEST_M3U_CHANNEL_COUNT,
}
),
],
playlistId: 'playlist-1',
requestId: 'db-request-2',
requestReceivedEpochMs: 1143,
responseEpochMs: 1174,
responsePostedEpochMs: 1164,
sourceEpochMs: 1141,
success: true,
workEndedEpochMs: 1162,
workStartedEpochMs: 1144,
},
],
},
],
} as unknown as MainCaptureMetrics;
}
export function createTestRendererCapture(): M3uImportRendererCaptureMetrics {
return {
phaseTimestamps: {
firstChannelVisibleEpochMs: 1210,
operationStartEpochMs: 1000,
routeReadyEpochMs: 1140,
terminalEpochMs: 1215,
uiPaintedEpochMs: 1215,
},
probe: {
performancePhaseEvents: [
rendererPhase(
'start',
'store.m3u-import-dispatch',
'renderer-1',
1076,
76
),
rendererPhase(
'end',
'store.m3u-import-dispatch',
'renderer-1',
1078,
78
),
rendererPhase(
'start',
'store.m3u-publish-channels',
'renderer-2',
1195,
195,
TEST_M3U_CHANNEL_COUNT
),
rendererPhase(
'end',
'store.m3u-publish-channels',
'renderer-2',
1205,
205,
TEST_M3U_CHANNEL_COUNT
),
],
playlistId: 'playlist-1',
},
} as unknown as M3uImportRendererCaptureMetrics;
}
export function createCompleteTestMainCapture(
overrides: { mainRss?: number; rendererRss?: number } = {}
): MainCaptureMetrics {
const base = createTestMainCapture();
const databaseWorker = base.workers[0];
const upsert = databaseWorker?.requests[0];
const get = databaseWorker?.requests[1];
if (!databaseWorker || !upsert || !get) {
throw new Error('invalid-test-main-capture');
}
return {
...base,
cpuProfilePath: null,
cpuSystemMicros: 20,
cpuUserMicros: 30,
eventLoopDelay: { maxMs: 3, p95Ms: 1, p99Ms: 2 },
eventLoopUtilization: null,
eventLoopUtilizationUnavailableReason:
'electron-main-embedded-event-loop',
heapSnapshotPath: null,
memory: {
peakHeapUsedBytes: 2_000,
peakRssBytes: overrides.mainRss ?? 5_000,
postGcHeapUsedBytes: 1_000,
postGcRssBytes: 4_500,
},
rendererWindow: {
responsiveEvents: 0,
rss: {
identity: { creationTime: 100, pid: 42 },
missingSampleCount: 0,
peakRssBytes: overrides.rendererRss ?? 4_000,
unavailableReason: null,
validSampleCount: 5,
},
unresponsiveEvents: 0,
windowIdentity: {
browserWindowId: 1,
webContentsId: 2,
},
},
rssScope:
'electron-main-process-including-worker-threads-and-native-memory',
workers: [
{
...databaseWorker,
externalMemorySampleCount: 4,
heapUsedSampleCount: 4,
peakExternalBytes: 600,
peakExternalUnavailableReason: null,
peakHeapUsedBytes: 1_500,
peakHeapUsedUnavailableReason: null,
postGcHeapUnavailableReason: null,
postGcHeapUsedBytes: 900,
requests: [
{
...upsert,
eventLoopDelay: { maxMs: 6, p95Ms: 4, p99Ms: 5 },
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: 0.8,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1134,
invalidReason: null,
threadCpuSystemMicros: 40,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 50,
},
{
...get,
eventLoopDelay: { maxMs: 9, p95Ms: 7, p99Ms: 8 },
eventLoopDelayUnavailableReason: null,
eventLoopUtilization: 0.9,
eventLoopUtilizationUnavailableReason: null,
histogramFlushedEpochMs: 1163,
invalidReason: null,
threadCpuSystemMicros: 60,
threadCpuUnavailableReason: null,
threadCpuUserMicros: 70,
},
],
},
],
};
}
export function createCompleteTestRendererCapture(
overrides: { rendererHeap?: number } = {}
): M3uImportRendererCaptureMetrics {
const base = createTestRendererCapture();
return {
...base,
cpuProfilePath: null,
frameGap: distribution([16, 40]),
heapSnapshotPath: null,
heartbeatDelay: distribution([0, 5]),
longTask: distribution([60]),
peakHeapUsedBytes: overrides.rendererHeap ?? 3_000,
postGcHeapUsedBytes: 1_100,
probe: {
...base.probe,
firstChannelVisibleEpochMs: 1210,
frameGapsMs: [16, 40],
heartbeatDelaysMs: [0, 5],
longTasksMs: [60],
operationStartEpochMs: 1000,
routeReadyEpochMs: 1140,
terminalEpochMs: 1215,
uiPaintedEpochMs: 1215,
},
tracePath: null,
};
}
export function createTestM3uImportManifest(): M3uImportBenchmarkManifest {
return {
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
fixtureSha256: 'fixture-sha',
gitCommit: 'commit',
gitDiffSha256: 'diff',
gitDirty: false,
measuredRuns: 1,
memoryAccounting: {
databaseWorkerRss: 'unavailable-per-worker-thread',
mainRss:
'electron-main-process-including-worker-threads-and-native-memory',
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
rendererRss: 'separate-renderer-process',
workerJsHeap: 'separate-v8-isolate',
},
runtime: {
arch: 'arm64',
cdpPort: 9222,
electronVersion: '1',
harnessNodeVersion: '1',
platform: 'darwin',
},
scenario: 'm3u-import-10k',
syntheticSeed: 240_724,
variant: 'baseline',
warmupRuns: 1,
};
}
function mainPhase(
boundary: 'end' | 'start',
phase: string,
epochMs: number,
durationMs: number | null,
metadata: { byteCount?: number; itemCount?: number } = {}
) {
return {
boundary,
durationMs,
epochMs,
...metadata,
phase,
requestId: 'main-request-1',
type: 'm3u-import-phase',
};
}
function workerPhase(
boundary: 'end' | 'start',
phase:
| 'deserialize.playlist'
| 'serialize.playlist'
| 'sqlite.read'
| 'sqlite.write',
epochMs: number,
durationMs: number | null,
requestId: string,
metadata?: { byteCount?: number; itemCount?: number }
) {
return {
boundary,
durationMs,
epochMs,
...(metadata ? { metadata } : {}),
phase,
requestId,
};
}
function rendererPhase(
boundary: 'end' | 'start',
phase: 'store.m3u-import-dispatch' | 'store.m3u-publish-channels',
phaseId: string,
epochMs: number,
monotonicMs: number,
items?: number
) {
return {
boundary,
epochMs,
...(items === undefined ? {} : { metadata: { items } }),
monotonicMs,
...(boundary === 'end' ? { outcome: 'success' as const } : {}),
phase,
phaseId,
schemaVersion: 1 as const,
};
}
function distribution(values: readonly number[]) {
const min = Math.min(...values);
const max = Math.max(...values);
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
return {
count: values.length,
max,
mean,
median: mean,
min,
p95: values.length === 1 ? max : min + (max - min) * 0.95,
p99: values.length === 1 ? max : min + (max - min) * 0.99,
};
}
@@ -0,0 +1,367 @@
import {
APP_PLAYLIST_GET_PERFORMANCE_PHASE,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE,
M3U_IMPORT_PERFORMANCE_PHASE,
} from '@iptvnator/shared/interfaces';
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import type {
MainCaptureMetrics,
MainTimelineRecord,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import { deriveM3uImportIpcAttribution } from './m3u-import-ipc-attribution';
import {
pairPerformancePhaseEvents,
requirePerformancePhase,
type PairedPerformancePhase,
} from './performance-phase-events';
import {
pairRendererPerformancePhaseEvents,
requireRendererPerformancePhase,
} from './renderer-performance-phase-events';
export const M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON =
'indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write';
export interface M3uImportPhaseMetrics {
readonly angularRenderingMs: number;
readonly dataAcquireMs: number;
readonly databaseWorkerToMainCloneProxyMs: number;
readonly indexAndCommitMs: null;
readonly indexAndCommitUnavailableReason: typeof M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON;
readonly ipcStructuredCloneProxyMs: number;
readonly mainToDatabaseWorkerCloneProxyMs: number;
readonly mainToRendererCloneProxyMs: number;
readonly m3uParsingMs: number;
readonly normalizationMs: number;
readonly playlistDeserializationMs: number;
readonly playlistSerializationMs: number;
readonly rendererToMainCloneProxyMs: number;
readonly sqliteReadMs: number;
readonly sqliteWriteMs: number;
readonly storeImportDispatchMs: number;
readonly storePublishChannelsMs: number;
readonly storeUpdateMs: number;
readonly totalMs: number;
}
export interface M3uImportPhaseDerivationInput {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly main: MainCaptureMetrics;
readonly renderer: M3uImportRendererCaptureMetrics;
}
export function deriveM3uImportPhaseMetrics(
input: M3uImportPhaseDerivationInput
): M3uImportPhaseMetrics {
try {
return deriveM3uImportPhaseMetricsStrict(input);
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`m3u-import-phase-attribution-invalid:${detail}`);
}
}
function deriveM3uImportPhaseMetricsStrict(
input: M3uImportPhaseDerivationInput
): M3uImportPhaseMetrics {
requirePositiveSafeCount(input.channelCount, 'channel-count');
requirePositiveSafeCount(input.fixtureBytes, 'fixture-bytes');
const mainPhases = pairPerformancePhaseEvents(
input.main.timeline
.filter((event) => event.type === 'm3u-import-phase')
.map(toPerformancePhaseEvent)
);
if (mainPhases.length !== 3) {
throw new Error('main-phase-cardinality');
}
const mainRequestIds = new Set(mainPhases.map((phase) => phase.requestId));
if (mainRequestIds.size !== 1) {
throw new Error('main-request-correlation');
}
const mainRequestId = mainPhases[0]?.requestId;
if (!mainRequestId) {
throw new Error('main-request-missing');
}
const dataAcquire = requirePerformancePhase(
mainPhases,
mainRequestId,
M3U_IMPORT_PERFORMANCE_PHASE.DATA_ACQUIRE
);
const parseM3u = requirePerformancePhase(
mainPhases,
mainRequestId,
M3U_IMPORT_PERFORMANCE_PHASE.PARSE_M3U
);
const normalize = requirePerformancePhase(
mainPhases,
mainRequestId,
M3U_IMPORT_PERFORMANCE_PHASE.NORMALIZE
);
requireMetadataCount(
dataAcquire,
'byteCount',
input.fixtureBytes,
'data-acquire-bytes'
);
requireMetadataCount(
parseM3u,
'itemCount',
input.channelCount,
'parse-items'
);
requireMetadataCount(
normalize,
'itemCount',
input.channelCount,
'normalize-items'
);
requireOrdered(dataAcquire.endedEpochMs, parseM3u.startedEpochMs);
requireOrdered(parseM3u.endedEpochMs, normalize.startedEpochMs);
const rendererEvents = (
input.renderer.probe as typeof input.renderer.probe & {
readonly performancePhaseEvents?: readonly unknown[];
}
).performancePhaseEvents;
if (!rendererEvents) {
throw new Error('renderer-phase-events-missing');
}
const rendererPhases = pairRendererPerformancePhaseEvents(rendererEvents);
if (rendererPhases.length !== 2) {
throw new Error('renderer-phase-cardinality');
}
const importDispatch = requireRendererPerformancePhase(
rendererPhases,
'store.m3u-import-dispatch'
);
const publishChannels = requireRendererPerformancePhase(
rendererPhases,
'store.m3u-publish-channels'
);
requireRendererItems(
publishChannels.metadata?.items,
input.channelCount,
'publish-channels-items'
);
const playlistId = input.renderer.probe.playlistId;
if (typeof playlistId !== 'string' || playlistId.length === 0) {
throw new Error('renderer-playlist-id-missing');
}
const databaseWorkers = input.main.workers.filter(
(worker) => worker.kind === 'database.worker'
);
if (
databaseWorkers.length !== 1 ||
input.main.workers.some(
(worker) => worker.kind === 'playlist-refresh.worker'
)
) {
throw new Error('worker-cardinality');
}
const databaseWorker = databaseWorkers[0];
if (!databaseWorker) {
throw new Error('database-worker-missing');
}
const upserts = databaseWorker.requests.filter(
(request) =>
request.operation === 'DB_UPSERT_APP_PLAYLIST' &&
request.playlistId === playlistId &&
request.success
);
if (upserts.length !== 1 || !upserts[0]) {
throw new Error('database-upsert-correlation');
}
const upsert = upserts[0];
const gets = databaseWorker.requests.filter(
(request) =>
request.operation === 'DB_GET_APP_PLAYLIST' &&
request.playlistId === playlistId &&
request.success
);
if (
gets.length !== 1 ||
!gets[0] ||
databaseWorker.requests.length !== 2
) {
throw new Error('database-get-correlation');
}
const get = gets[0];
const databasePhases = pairPerformancePhaseEvents(upsert.phaseEvents);
if (
databasePhases.length !== 2 ||
!upsert.requestId ||
databasePhases.some((phase) => phase.requestId !== upsert.requestId)
) {
throw new Error('database-phase-cardinality');
}
const serializePlaylist = requireDatabasePhase(
databasePhases,
upsert,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SERIALIZE_PLAYLIST
);
const sqliteWrite = requireDatabasePhase(
databasePhases,
upsert,
APP_PLAYLIST_UPSERT_PERFORMANCE_PHASE.SQLITE_WRITE
);
requireMetadataCount(
serializePlaylist,
'itemCount',
input.channelCount,
'serialization-items'
);
requireMetadataCount(sqliteWrite, 'itemCount', 1, 'sqlite-row-count');
const getPhases = pairPerformancePhaseEvents(get.phaseEvents);
if (
getPhases.length !== 2 ||
!get.requestId ||
getPhases.some((phase) => phase.requestId !== get.requestId)
) {
throw new Error('database-get-phase-cardinality');
}
const sqliteRead = requireDatabasePhase(
getPhases,
get,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.SQLITE_READ
);
const deserializePlaylist = requireDatabasePhase(
getPhases,
get,
APP_PLAYLIST_GET_PERFORMANCE_PHASE.DESERIALIZE_PLAYLIST
);
requireMetadataCount(sqliteRead, 'itemCount', 1, 'sqlite-read-row-count');
requireMetadataCount(
deserializePlaylist,
'itemCount',
input.channelCount,
'deserialization-items'
);
const operationStart = input.renderer.phaseTimestamps.operationStartEpochMs;
const uiPainted = input.renderer.phaseTimestamps.uiPaintedEpochMs;
const terminal = input.renderer.phaseTimestamps.terminalEpochMs;
requireOrdered(operationStart, dataAcquire.startedEpochMs);
requireOrdered(serializePlaylist.endedEpochMs, sqliteWrite.startedEpochMs);
requireOrdered(sqliteWrite.endedEpochMs, sqliteRead.startedEpochMs);
requireOrdered(sqliteRead.endedEpochMs, deserializePlaylist.startedEpochMs);
requireOrdered(
deserializePlaylist.endedEpochMs,
publishChannels.startedEpochMs
);
requireOrdered(publishChannels.endedEpochMs, uiPainted);
requireOrdered(uiPainted, terminal);
const ipc = deriveM3uImportIpcAttribution({
get,
getReadStartedEpochMs: sqliteRead.startedEpochMs,
importDispatchEndedEpochMs: importDispatch.endedEpochMs,
importDispatchStartedEpochMs: importDispatch.startedEpochMs,
normalizeEndedEpochMs: normalize.endedEpochMs,
playlistId,
publishChannelsStartedEpochMs: publishChannels.startedEpochMs,
serializeStartedEpochMs: serializePlaylist.startedEpochMs,
timeline: input.main.timeline,
upsert,
});
const angularRenderingMs = difference(
uiPainted,
publishChannels.endedEpochMs
);
const totalMs = difference(terminal, operationStart);
return Object.freeze({
angularRenderingMs,
dataAcquireMs: dataAcquire.durationMs,
indexAndCommitMs: null,
indexAndCommitUnavailableReason:
M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
...ipc,
m3uParsingMs: parseM3u.durationMs,
normalizationMs: normalize.durationMs,
playlistDeserializationMs: deserializePlaylist.durationMs,
playlistSerializationMs: serializePlaylist.durationMs,
sqliteReadMs: sqliteRead.durationMs,
sqliteWriteMs: sqliteWrite.durationMs,
storeImportDispatchMs: importDispatch.durationMs,
storePublishChannelsMs: publishChannels.durationMs,
storeUpdateMs: importDispatch.durationMs + publishChannels.durationMs,
totalMs,
});
}
function toPerformancePhaseEvent(event: MainTimelineRecord): unknown {
const metadata = {
...(event.byteCount === undefined
? {}
: { byteCount: event.byteCount }),
...(event.itemCount === undefined
? {}
: { itemCount: event.itemCount }),
};
return {
boundary: event.boundary,
durationMs: event.durationMs,
epochMs: event.epochMs,
...(Object.keys(metadata).length === 0 ? {} : { metadata }),
phase: event.phase,
requestId: event.requestId,
};
}
function requireDatabasePhase(
phases: readonly PairedPerformancePhase[],
request: WorkerRequestPerformanceMetrics,
phase: string
): PairedPerformancePhase {
if (!request.requestId) {
throw new Error('database-request-id-missing');
}
return requirePerformancePhase(phases, request.requestId, phase);
}
function requireMetadataCount(
phase: PairedPerformancePhase,
key: 'byteCount' | 'itemCount',
expected: number,
label: string
): void {
if (phase.metadata?.[key] !== expected) {
throw new Error(label);
}
}
function requireRendererItems(
value: number | undefined,
expected: number,
label: string
): void {
if (value !== expected) {
throw new Error(label);
}
}
function requirePositiveSafeCount(value: number, label: string): void {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(label);
}
}
function requireOrdered(startEpochMs: number, endEpochMs: number): void {
if (
!Number.isFinite(startEpochMs) ||
!Number.isFinite(endEpochMs) ||
endEpochMs < startEpochMs
) {
throw new Error('phase-order');
}
}
function difference(endEpochMs: number, startEpochMs: number): number {
requireOrdered(startEpochMs, endEpochMs);
return endEpochMs - startEpochMs;
}
@@ -0,0 +1,131 @@
import type { DatabaseWorkerPeakMemoryValidity } from './database-worker-peak-memory-validity';
import type { DatabaseWorkerRequestMetricsValidity } from './database-worker-request-metrics-validity';
import type { M3uImportScenarioId } from './m3u-import-benchmark-contract';
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import {
M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
type M3uImportPhaseMetrics,
} from './m3u-import-report';
import type {
DatabaseWorkerPostGcValidity,
MainCaptureMetrics,
NumericDistribution,
PerformanceIterationKind,
} from './m3u-refresh-cancellation-contract';
import type { RendererRssValidity } from './renderer-rss-validity';
export interface M3uImportIterationResult {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly kind: PerformanceIterationKind;
readonly main: MainCaptureMetrics;
readonly phases: M3uImportPhaseMetrics;
readonly renderer: M3uImportRendererCaptureMetrics;
readonly runId: string;
readonly scenarioId: M3uImportScenarioId;
}
export interface M3uImportBenchmarkManifest {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly fixtureSha256: string;
readonly gitCommit: string;
readonly gitDiffSha256: string;
readonly gitDirty: boolean;
readonly measuredRuns: number;
readonly memoryAccounting: {
readonly databaseWorkerRss: 'unavailable-per-worker-thread';
readonly mainRss: 'electron-main-process-including-worker-threads-and-native-memory';
readonly nativeAndSqliteMemory: 'included-in-main-rss-not-separately-attributable';
readonly rendererRss: 'separate-renderer-process';
readonly workerJsHeap: 'separate-v8-isolate';
};
readonly runtime: {
readonly arch: string;
readonly cdpPort: number;
readonly electronVersion: string;
readonly harnessNodeVersion: string;
readonly platform: string;
};
readonly scenario: M3uImportScenarioId;
readonly syntheticSeed: number;
readonly variant: string;
readonly warmupRuns: number;
}
export interface M3uImportMeasuredSummary {
readonly angularRenderingMs: NumericDistribution;
readonly dataAcquireMs: NumericDistribution;
readonly databaseWorkerGetEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerGetEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerGetEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerGetEventLoopUtilization: NumericDistribution;
readonly databaseWorkerGetThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerGetThreadCpuUserMicros: NumericDistribution;
readonly databaseWorkerEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerEventLoopUtilization: NumericDistribution;
readonly databaseWorkerExternalPeakBytes: NumericDistribution;
readonly databaseWorkerHeapPeakBytes: NumericDistribution;
readonly databaseWorkerPostGcHeapBytes: NumericDistribution;
readonly databaseWorkerThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerThreadCpuUserMicros: NumericDistribution;
readonly databaseWorkerToMainCloneProxyMs: NumericDistribution;
readonly databaseWorkerUpsertEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerUpsertEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerUpsertEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerUpsertEventLoopUtilization: NumericDistribution;
readonly databaseWorkerUpsertThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerUpsertThreadCpuUserMicros: NumericDistribution;
readonly ipcStructuredCloneProxyMs: NumericDistribution;
readonly mainToDatabaseWorkerCloneProxyMs: NumericDistribution;
readonly mainToRendererCloneProxyMs: NumericDistribution;
readonly m3uParsingMs: NumericDistribution;
readonly mainEventLoopDelayMaxMs: NumericDistribution;
readonly mainEventLoopDelayP95Ms: NumericDistribution;
readonly mainEventLoopDelayP99Ms: NumericDistribution;
readonly mainEventLoopUtilization: NumericDistribution;
readonly mainHeapPeakBytes: NumericDistribution;
readonly mainHeapPostGcBytes: NumericDistribution;
readonly mainRssPeakBytes: NumericDistribution;
readonly mainRssPostGcBytes: NumericDistribution;
readonly normalizationMs: NumericDistribution;
readonly playlistDeserializationMs: NumericDistribution;
readonly playlistSerializationMs: NumericDistribution;
readonly rendererFrameGapMs: NumericDistribution;
readonly rendererHeartbeatDelayMs: NumericDistribution;
readonly rendererHeapPeakBytes: NumericDistribution;
readonly rendererHeapPostGcBytes: NumericDistribution;
readonly rendererLongTaskCount: number;
readonly rendererLongTaskMs: NumericDistribution;
readonly rendererRssPeakBytes: NumericDistribution;
readonly rendererToMainCloneProxyMs: NumericDistribution;
readonly responsiveEvents: number;
readonly sqliteReadMs: NumericDistribution;
readonly sqliteWriteMs: NumericDistribution;
readonly storeImportDispatchMs: NumericDistribution;
readonly storePublishChannelsMs: NumericDistribution;
readonly storeUpdateMs: NumericDistribution;
readonly totalMs: NumericDistribution;
readonly unresponsiveEvents: number;
}
export interface M3uImportBenchmarkSummary {
readonly iterations: readonly M3uImportIterationResult[];
readonly manifest: M3uImportBenchmarkManifest;
readonly measured: M3uImportMeasuredSummary;
readonly notApplicable: {
readonly cancelAcknowledgementLatencyMs: 'scenario-has-no-cancellation';
readonly databaseWorkerRss: 'unavailable-per-worker-thread';
readonly indexAndCommitMs: typeof M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON;
readonly nativeAndSqliteMemory: 'included-in-main-rss-not-separately-attributable';
readonly playlistRefreshWorker: 'not-used-by-initial-import';
};
readonly validity: {
readonly databaseWorkerPeakMemory: DatabaseWorkerPeakMemoryValidity;
readonly databaseWorkerPostGc: DatabaseWorkerPostGcValidity;
readonly databaseWorkerRequestMetrics: DatabaseWorkerRequestMetricsValidity;
readonly rendererRss: RendererRssValidity;
};
}
@@ -0,0 +1,247 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { it } from 'node:test';
import { PERFORMANCE_ITERATION_KIND } from './m3u-refresh-cancellation-contract';
import {
createM3uImportBenchmarkSummary,
createM3uImportIterationResult,
type M3uImportBenchmarkSummary,
} from './m3u-import-summary';
import { assertM3uImportComparisonValidity } from './m3u-import.benchmark';
import {
createCompleteTestMainCapture,
createCompleteTestRendererCapture,
createTestM3uImportManifest,
TEST_M3U_CHANNEL_COUNT,
TEST_M3U_FIXTURE_BYTES,
} from './m3u-import-report.test-helpers';
it('keeps summary aggregation below the hard line limit without a suppression', () => {
const source = readFileSync(
resolve(process.cwd(), 'src/performance/m3u-import-summary.ts'),
'utf8'
);
assert.doesNotMatch(source, /eslint-disable max-lines/);
assert.ok(source.split(/\r?\n/).length <= 400);
});
it('summarizes measured runs only while preserving per-process memory boundaries', () => {
const warmup = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'warmup-01',
scenarioId: 'm3u-import-10k',
});
const measured = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'run-01',
scenarioId: 'm3u-import-10k',
});
const diagnostic = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
main: createCompleteTestMainCapture({
mainRss: 9_999,
rendererRss: 8_888,
}),
renderer: createCompleteTestRendererCapture({
rendererHeap: 7_777,
}),
runId: 'diagnostic',
scenarioId: 'm3u-import-10k',
});
const summary = createM3uImportBenchmarkSummary(
createTestM3uImportManifest(),
[warmup, measured, diagnostic]
);
assert.equal(summary.measured.totalMs.count, 1);
assert.equal(summary.measured.totalMs.mean, 215);
assert.equal(summary.measured.ipcStructuredCloneProxyMs.mean, 44);
assert.equal(summary.measured.mainToDatabaseWorkerCloneProxyMs.mean, 2);
assert.equal(summary.measured.databaseWorkerToMainCloneProxyMs.mean, 12);
assert.equal(summary.measured.mainToRendererCloneProxyMs.mean, 28);
assert.equal(summary.measured.rendererToMainCloneProxyMs.mean, 2);
assert.equal(summary.measured.sqliteReadMs.mean, 5);
assert.equal(summary.measured.playlistDeserializationMs.mean, 12);
assert.equal(summary.measured.databaseWorkerThreadCpuSystemMicros.mean, 100);
assert.equal(summary.measured.databaseWorkerThreadCpuUserMicros.mean, 120);
assert.equal(
summary.measured.databaseWorkerUpsertEventLoopDelayP95Ms.mean,
4
);
assert.equal(
summary.measured.databaseWorkerGetEventLoopDelayP95Ms.mean,
7
);
assert.equal(summary.measured.mainRssPeakBytes.mean, 5_000);
assert.equal(summary.measured.mainHeapPeakBytes.mean, 2_000);
assert.equal(summary.measured.rendererRssPeakBytes.mean, 4_000);
assert.equal(summary.measured.rendererHeapPeakBytes.mean, 3_000);
assert.equal(summary.measured.databaseWorkerHeapPeakBytes.mean, 1_500);
assert.equal(summary.measured.databaseWorkerPostGcHeapBytes.mean, 900);
assert.equal(summary.measured.rendererLongTaskCount, 1);
assert.equal(summary.measured.unresponsiveEvents, 0);
assert.deepEqual(summary.notApplicable, {
cancelAcknowledgementLatencyMs: 'scenario-has-no-cancellation',
databaseWorkerRss: 'unavailable-per-worker-thread',
indexAndCommitMs:
'indexes-not-created-during-import;sqlite-autocommit-included-in-sqlite-write',
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
playlistRefreshWorker: 'not-used-by-initial-import',
});
assert.equal(summary.validity.rendererRss.validForComparison, true);
assert.equal(
summary.validity.databaseWorkerPeakMemory.validForComparison,
true
);
assert.equal(
summary.validity.databaseWorkerPostGc.validForComparison,
true
);
assert.doesNotThrow(() => assertM3uImportComparisonValidity(summary));
const databaseWorker = measured.main.workers[0];
assert.ok(databaseWorker);
const invalidMeasured = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: {
...measured.main,
workers: [
{
...databaseWorker,
externalMemorySampleCount: 0,
heapUsedSampleCount: 0,
peakExternalBytes: null,
peakExternalUnavailableReason:
'worker-external-memory-samples-missing',
peakHeapUsedBytes: null,
peakHeapUsedUnavailableReason:
'worker-heap-used-samples-missing',
},
],
},
renderer: measured.renderer,
runId: 'run-01',
scenarioId: 'm3u-import-10k',
});
const invalidPeakSummary = createM3uImportBenchmarkSummary(
createTestM3uImportManifest(),
[warmup, invalidMeasured, diagnostic]
);
assert.equal(
invalidPeakSummary.validity.databaseWorkerPeakMemory.validForComparison,
false
);
assert.equal(
invalidPeakSummary.measured.databaseWorkerHeapPeakBytes.count,
0
);
assert.throws(
() =>
assertM3uImportComparisonValidity(
invalidPeakSummary as M3uImportBenchmarkSummary
),
/Database worker peak-memory capture is invalid/
);
assert.throws(
() =>
createM3uImportBenchmarkSummary(createTestM3uImportManifest(), [
measured,
diagnostic,
]),
/m3u-import-summary-schedule-invalid/
);
});
it('fails comparison when any measured worker request omits mandatory event-loop or CPU metrics', () => {
const completeMain = createCompleteTestMainCapture();
const databaseWorker = completeMain.workers[0];
const get = databaseWorker?.requests[1];
assert.ok(databaseWorker);
assert.ok(get);
const invalidMain = {
...completeMain,
workers: [
{
...databaseWorker,
requests: [
databaseWorker.requests[0],
{
...get,
eventLoopDelay: null,
eventLoopDelayUnavailableReason:
'event-loop-delay-capture-unavailable',
histogramFlushedEpochMs: null,
},
],
},
],
};
const warmup = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.WARMUP,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'warmup-01',
scenarioId: 'm3u-import-10k',
});
const measured = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: invalidMain,
renderer: createCompleteTestRendererCapture(),
runId: 'run-01',
scenarioId: 'm3u-import-10k',
});
const diagnostic = createM3uImportIterationResult({
channelCount: TEST_M3U_CHANNEL_COUNT,
fixtureBytes: TEST_M3U_FIXTURE_BYTES,
kind: PERFORMANCE_ITERATION_KIND.DIAGNOSTIC,
main: createCompleteTestMainCapture(),
renderer: createCompleteTestRendererCapture(),
runId: 'diagnostic',
scenarioId: 'm3u-import-10k',
});
const summary = createM3uImportBenchmarkSummary(
createTestM3uImportManifest(),
[warmup, measured, diagnostic]
);
assert.equal(
summary.validity.databaseWorkerRequestMetrics.validForComparison,
false
);
assert.equal(
summary.validity.databaseWorkerRequestMetrics.expectedRequestCount,
2
);
assert.equal(
summary.validity.databaseWorkerRequestMetrics.validRequestCount,
1
);
assert.equal(
summary.measured.databaseWorkerGetEventLoopDelayP95Ms.count,
0
);
assert.throws(
() => assertM3uImportComparisonValidity(summary),
/Database worker request metrics are invalid/
);
});
@@ -0,0 +1,382 @@
import type { M3uImportRendererCaptureMetrics } from './m3u-import-renderer-capture';
import type { M3uImportScenarioId } from './m3u-import-benchmark-contract';
import type {
MainCaptureMetrics,
NumericDistribution,
PerformanceIterationKind,
WorkerCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
PERFORMANCE_ITERATION_KIND,
PERFORMANCE_WORKER_KIND,
} from './m3u-refresh-cancellation-contract';
import {
deriveM3uImportPhaseMetrics,
M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
} from './m3u-import-report';
import { summarizeNumbers } from './performance-statistics';
import { assessDatabaseWorkerPeakMemoryValidity } from './database-worker-peak-memory-validity';
import { assessDatabaseWorkerPostGcValidity } from './database-worker-post-gc-validity';
import { assessDatabaseWorkerRequestMetricsValidity } from './database-worker-request-metrics-validity';
import { assessRendererRssValidity } from './renderer-rss-validity';
import type {
M3uImportBenchmarkManifest,
M3uImportBenchmarkSummary,
M3uImportIterationResult,
} from './m3u-import-summary-contract';
export type {
M3uImportBenchmarkManifest,
M3uImportBenchmarkSummary,
M3uImportIterationResult,
M3uImportMeasuredSummary,
} from './m3u-import-summary-contract';
export function createM3uImportIterationResult(input: {
readonly channelCount: number;
readonly fixtureBytes: number;
readonly kind: PerformanceIterationKind;
readonly main: MainCaptureMetrics;
readonly renderer: M3uImportRendererCaptureMetrics;
readonly runId: string;
readonly scenarioId: M3uImportScenarioId;
}): M3uImportIterationResult {
return Object.freeze({
channelCount: input.channelCount,
fixtureBytes: input.fixtureBytes,
kind: input.kind,
main: input.main,
phases: deriveM3uImportPhaseMetrics(input),
renderer: input.renderer,
runId: input.runId,
scenarioId: input.scenarioId,
});
}
export function createM3uImportBenchmarkSummary(
manifest: M3uImportBenchmarkManifest,
iterations: readonly M3uImportIterationResult[]
): M3uImportBenchmarkSummary {
validateIterationSchedule(manifest, iterations);
const measured = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
);
validateMeasuredIterations(manifest, measured);
const values = (
select: (iteration: M3uImportIterationResult) => number | null
): NumericDistribution => summarizeNumbers(measured.map(select));
const rendererSamples = (
select: (
probe: M3uImportRendererCaptureMetrics['probe']
) => readonly number[]
): NumericDistribution =>
summarizeNumbers(
measured.flatMap((iteration) => [
...select(iteration.renderer.probe),
])
);
const databaseWorkers = measured.map(requireDatabaseWorker);
const databaseRequestPairs = databaseWorkers.map(
requireInitialImportRequests
);
const databaseUpserts = databaseRequestPairs.map((pair) => pair.upsert);
const databaseGets = databaseRequestPairs.map((pair) => pair.get);
const databaseRequests = databaseRequestPairs.flatMap((pair) => [
pair.upsert,
pair.get,
]);
return Object.freeze({
iterations: Object.freeze([...iterations]),
manifest,
measured: Object.freeze({
angularRenderingMs: values(
(iteration) => iteration.phases.angularRenderingMs
),
dataAcquireMs: values(
(iteration) => iteration.phases.dataAcquireMs
),
databaseWorkerGetEventLoopDelayMaxMs: summarizeNumbers(
databaseGets.map((request) => request.eventLoopDelay?.maxMs)
),
databaseWorkerGetEventLoopDelayP95Ms: summarizeNumbers(
databaseGets.map((request) => request.eventLoopDelay?.p95Ms)
),
databaseWorkerGetEventLoopDelayP99Ms: summarizeNumbers(
databaseGets.map((request) => request.eventLoopDelay?.p99Ms)
),
databaseWorkerGetEventLoopUtilization: summarizeNumbers(
databaseGets.map((request) => request.eventLoopUtilization)
),
databaseWorkerGetThreadCpuSystemMicros: summarizeNumbers(
databaseGets.map((request) => request.threadCpuSystemMicros)
),
databaseWorkerGetThreadCpuUserMicros: summarizeNumbers(
databaseGets.map((request) => request.threadCpuUserMicros)
),
databaseWorkerEventLoopDelayMaxMs: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopDelay?.maxMs)
),
databaseWorkerEventLoopDelayP95Ms: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopDelay?.p95Ms)
),
databaseWorkerEventLoopDelayP99Ms: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopDelay?.p99Ms)
),
databaseWorkerEventLoopUtilization: summarizeNumbers(
databaseRequests.map((request) => request.eventLoopUtilization)
),
databaseWorkerExternalPeakBytes: summarizeNumbers(
databaseWorkers.map((worker) => worker.peakExternalBytes)
),
databaseWorkerHeapPeakBytes: summarizeNumbers(
databaseWorkers.map((worker) => worker.peakHeapUsedBytes)
),
databaseWorkerPostGcHeapBytes: summarizeNumbers(
databaseWorkers.map((worker) => worker.postGcHeapUsedBytes)
),
databaseWorkerThreadCpuSystemMicros: summarizeNumbers(
databaseRequestPairs.map((pair) =>
sumRequestMetric(
pair.upsert.threadCpuSystemMicros,
pair.get.threadCpuSystemMicros
)
)
),
databaseWorkerThreadCpuUserMicros: summarizeNumbers(
databaseRequestPairs.map((pair) =>
sumRequestMetric(
pair.upsert.threadCpuUserMicros,
pair.get.threadCpuUserMicros
)
)
),
databaseWorkerToMainCloneProxyMs: values(
(iteration) =>
iteration.phases.databaseWorkerToMainCloneProxyMs
),
databaseWorkerUpsertEventLoopDelayMaxMs: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopDelay?.maxMs)
),
databaseWorkerUpsertEventLoopDelayP95Ms: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopDelay?.p95Ms)
),
databaseWorkerUpsertEventLoopDelayP99Ms: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopDelay?.p99Ms)
),
databaseWorkerUpsertEventLoopUtilization: summarizeNumbers(
databaseUpserts.map((request) => request.eventLoopUtilization)
),
databaseWorkerUpsertThreadCpuSystemMicros: summarizeNumbers(
databaseUpserts.map(
(request) => request.threadCpuSystemMicros
)
),
databaseWorkerUpsertThreadCpuUserMicros: summarizeNumbers(
databaseUpserts.map((request) => request.threadCpuUserMicros)
),
ipcStructuredCloneProxyMs: values(
(iteration) => iteration.phases.ipcStructuredCloneProxyMs
),
mainToDatabaseWorkerCloneProxyMs: values(
(iteration) => iteration.phases.mainToDatabaseWorkerCloneProxyMs
),
mainToRendererCloneProxyMs: values(
(iteration) => iteration.phases.mainToRendererCloneProxyMs
),
m3uParsingMs: values((iteration) => iteration.phases.m3uParsingMs),
mainEventLoopDelayMaxMs: values(
(iteration) => iteration.main.eventLoopDelay.maxMs
),
mainEventLoopDelayP95Ms: values(
(iteration) => iteration.main.eventLoopDelay.p95Ms
),
mainEventLoopDelayP99Ms: values(
(iteration) => iteration.main.eventLoopDelay.p99Ms
),
mainEventLoopUtilization: values(
(iteration) => iteration.main.eventLoopUtilization
),
mainHeapPeakBytes: values(
(iteration) => iteration.main.memory.peakHeapUsedBytes
),
mainHeapPostGcBytes: values(
(iteration) => iteration.main.memory.postGcHeapUsedBytes
),
mainRssPeakBytes: values(
(iteration) => iteration.main.memory.peakRssBytes
),
mainRssPostGcBytes: values(
(iteration) => iteration.main.memory.postGcRssBytes
),
normalizationMs: values(
(iteration) => iteration.phases.normalizationMs
),
playlistDeserializationMs: values(
(iteration) => iteration.phases.playlistDeserializationMs
),
playlistSerializationMs: values(
(iteration) => iteration.phases.playlistSerializationMs
),
rendererFrameGapMs: rendererSamples((probe) => probe.frameGapsMs),
rendererHeartbeatDelayMs: rendererSamples(
(probe) => probe.heartbeatDelaysMs
),
rendererHeapPeakBytes: values(
(iteration) => iteration.renderer.peakHeapUsedBytes
),
rendererHeapPostGcBytes: values(
(iteration) => iteration.renderer.postGcHeapUsedBytes
),
rendererLongTaskCount: measured.reduce(
(total, iteration) =>
total + iteration.renderer.probe.longTasksMs.length,
0
),
rendererLongTaskMs: rendererSamples((probe) => probe.longTasksMs),
rendererRssPeakBytes: values(
(iteration) => iteration.main.rendererWindow.rss.peakRssBytes
),
rendererToMainCloneProxyMs: values(
(iteration) => iteration.phases.rendererToMainCloneProxyMs
),
responsiveEvents: measured.reduce(
(total, iteration) =>
total + iteration.main.rendererWindow.responsiveEvents,
0
),
sqliteReadMs: values((iteration) => iteration.phases.sqliteReadMs),
sqliteWriteMs: values(
(iteration) => iteration.phases.sqliteWriteMs
),
storeImportDispatchMs: values(
(iteration) => iteration.phases.storeImportDispatchMs
),
storePublishChannelsMs: values(
(iteration) => iteration.phases.storePublishChannelsMs
),
storeUpdateMs: values(
(iteration) => iteration.phases.storeUpdateMs
),
totalMs: values((iteration) => iteration.phases.totalMs),
unresponsiveEvents: measured.reduce(
(total, iteration) =>
total + iteration.main.rendererWindow.unresponsiveEvents,
0
),
}),
notApplicable: Object.freeze({
cancelAcknowledgementLatencyMs: 'scenario-has-no-cancellation',
databaseWorkerRss: 'unavailable-per-worker-thread',
indexAndCommitMs: M3U_IMPORT_INDEX_COMMIT_UNAVAILABLE_REASON,
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
playlistRefreshWorker: 'not-used-by-initial-import',
}),
validity: Object.freeze({
databaseWorkerPeakMemory:
assessDatabaseWorkerPeakMemoryValidity(iterations),
databaseWorkerPostGc:
assessDatabaseWorkerPostGcValidity(iterations),
databaseWorkerRequestMetrics:
assessDatabaseWorkerRequestMetricsValidity(iterations),
rendererRss: assessRendererRssValidity(iterations),
}),
});
}
function validateIterationSchedule(
manifest: M3uImportBenchmarkManifest,
iterations: readonly M3uImportIterationResult[]
): void {
const warmupCount = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.WARMUP
).length;
const measuredCount = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.MEASURED
).length;
const diagnosticCount = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC
).length;
if (
warmupCount !== manifest.warmupRuns ||
measuredCount !== manifest.measuredRuns ||
diagnosticCount !== 1 ||
iterations.length !== warmupCount + measuredCount + diagnosticCount ||
new Set(iterations.map((iteration) => iteration.runId)).size !==
iterations.length ||
iterations.some(
(iteration) =>
iteration.channelCount !== manifest.channelCount ||
iteration.fixtureBytes !== manifest.fixtureBytes ||
iteration.scenarioId !== manifest.scenario
)
) {
throw new Error('m3u-import-summary-schedule-invalid');
}
}
function validateMeasuredIterations(
manifest: M3uImportBenchmarkManifest,
measured: readonly M3uImportIterationResult[]
): void {
if (
measured.length !== manifest.measuredRuns ||
new Set(measured.map((iteration) => iteration.runId)).size !==
measured.length ||
measured.some(
(iteration) =>
iteration.channelCount !== manifest.channelCount ||
iteration.fixtureBytes !== manifest.fixtureBytes ||
iteration.scenarioId !== manifest.scenario
)
) {
throw new Error('m3u-import-summary-measured-runs-invalid');
}
}
function requireDatabaseWorker(
iteration: M3uImportIterationResult
): WorkerCaptureMetrics {
const databaseWorkers = iteration.main.workers.filter(
(worker) => worker.kind === PERFORMANCE_WORKER_KIND.DATABASE
);
if (databaseWorkers.length !== 1 || !databaseWorkers[0]) {
throw new Error('m3u-import-summary-database-worker-invalid');
}
return databaseWorkers[0];
}
function requireInitialImportRequests(
worker: WorkerCaptureMetrics
): {
readonly get: WorkerRequestPerformanceMetrics;
readonly upsert: WorkerRequestPerformanceMetrics;
} {
const upserts = worker.requests.filter(
(request) =>
request.operation === 'DB_UPSERT_APP_PLAYLIST' && request.success
);
const gets = worker.requests.filter(
(request) =>
request.operation === 'DB_GET_APP_PLAYLIST' && request.success
);
if (
worker.requests.length !== 2 ||
upserts.length !== 1 ||
!upserts[0] ||
gets.length !== 1 ||
!gets[0]
) {
throw new Error('m3u-import-summary-database-request-invalid');
}
return { get: gets[0], upsert: upserts[0] };
}
function sumRequestMetric(
first: number | null,
second: number | null
): number | null {
return first === null || second === null ? null : first + second;
}
@@ -0,0 +1,341 @@
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
closeElectronApp,
expect,
launchElectronApp,
type LaunchedElectronApp,
} from '../electron-test-fixtures';
import { validateInitialImportDiagnosticArtifacts } from './initial-import-diagnostic-artifacts';
import {
assertRendererCdpTarget,
assertTcpPortAvailable,
type M3uImportBenchmarkConfiguration,
resolveM3uImportBenchmarkConfiguration,
resolveRendererWindowIdentity,
writeM3uImportJson as writeJson,
} from './m3u-import-benchmark-support';
import {
assertSyntheticM3uImportUrl,
createM3uImportIterationDefinitions,
listM3uImportScenarios,
type M3uImportIterationDefinition,
type M3uImportScenario,
} from './m3u-import-benchmark-contract';
import {
createM3uImportBenchmarkSummary,
createM3uImportIterationResult,
type M3uImportBenchmarkManifest,
type M3uImportBenchmarkSummary,
type M3uImportIterationResult,
} from './m3u-import-summary';
import {
startM3uImportRendererCapture,
type RunningM3uImportRendererCapture,
} from './m3u-import-renderer-capture';
import {
installMainCapture,
readMainCaptureStatus,
startMainCapture,
stopMainCapture,
} from './m3u-refresh-main-capture';
import { PERFORMANCE_ITERATION_KIND } from './m3u-refresh-cancellation-contract';
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
import {
startSyntheticM3uServer,
type SyntheticM3uServer,
} from './synthetic-m3u-server';
import {
createSyntheticM3uFixture,
SYNTHETIC_M3U_SEED,
type SyntheticM3uFixture,
} from './synthetic-m3u';
export async function runM3uImportBenchmark(): Promise<void> {
const config = await resolveM3uImportBenchmarkConfiguration();
const definitions = createM3uImportIterationDefinitions(config.smoke);
for (const scenario of listM3uImportScenarios()) {
await runScenario(
config,
scenario,
definitions.filter(
(definition) => definition.scenarioId === scenario.id
)
);
}
}
async function runScenario(
config: M3uImportBenchmarkConfiguration,
scenario: M3uImportScenario,
definitions: readonly M3uImportIterationDefinition[]
): Promise<void> {
const fixture = createSyntheticM3uFixture(scenario.channelCount);
const server = await startSyntheticM3uServer(fixture);
const scenarioDirectory = join(config.variantDirectory, scenario.id);
const iterations: M3uImportIterationResult[] = [];
try {
assertSyntheticM3uImportUrl(server.resourceUrl);
await server.prime();
await assertPerformanceArtifactCapacity(scenarioDirectory);
await mkdir(scenarioDirectory, { recursive: false });
for (const definition of definitions) {
console.log(
`[performance] ${config.variant}/${scenario.id}/${definition.runId} starting`
);
const result = await runIteration(
config,
definition,
fixture,
server,
scenarioDirectory
);
iterations.push(result);
console.log(
`[performance] ${definition.runId} total=${result.phases.totalMs.toFixed(3)}ms`
);
}
} finally {
await server.close();
}
if (server.requestCount() !== definitions.length + 1) {
throw new Error('Synthetic M3U server request total is invalid');
}
const manifest = createManifest(config, scenario, fixture);
const summary = createM3uImportBenchmarkSummary(manifest, iterations);
await writeJson(join(scenarioDirectory, 'manifest.json'), manifest);
await writeJson(join(scenarioDirectory, 'summary.json'), summary);
const diagnostic = requireDiagnosticIteration(iterations);
await validateInitialImportDiagnosticArtifacts(
{ main: diagnostic.main, renderer: diagnostic.renderer },
join(scenarioDirectory, diagnostic.runId)
);
assertM3uImportComparisonValidity(summary);
console.log(
`[performance] completed: ${join(scenarioDirectory, 'summary.json')}`
);
}
export function assertM3uImportComparisonValidity(
summary: M3uImportBenchmarkSummary
): void {
if (!summary.validity.rendererRss.validForComparison) {
throw new Error(
`Renderer RSS capture is invalid for comparison: ${JSON.stringify(
summary.validity.rendererRss.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerPostGc.validForComparison) {
throw new Error(
`Database worker post-GC capture is invalid for comparison: ${JSON.stringify(
summary.validity.databaseWorkerPostGc.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerPeakMemory.validForComparison) {
throw new Error(
`Database worker peak-memory capture is invalid for comparison: ${JSON.stringify(
summary.validity.databaseWorkerPeakMemory.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerRequestMetrics.validForComparison) {
throw new Error(
`Database worker request metrics are invalid for comparison: ${JSON.stringify(
summary.validity.databaseWorkerRequestMetrics.invalidRequests
)}`
);
}
}
async function runIteration(
config: M3uImportBenchmarkConfiguration,
definition: M3uImportIterationDefinition,
fixture: SyntheticM3uFixture,
server: SyntheticM3uServer,
scenarioDirectory: string
): Promise<M3uImportIterationResult> {
const iterationDirectory = join(scenarioDirectory, definition.runId);
await assertPerformanceArtifactCapacity(iterationDirectory);
await mkdir(iterationDirectory, { recursive: false });
const dataDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-m3u-import-performance-')
);
let app: LaunchedElectronApp | null = null;
let renderer: RunningM3uImportRendererCapture | null = null;
try {
await assertTcpPortAvailable(config.rendererCdpPort);
app = await launchElectronApp(dataDirectory, {
args: [
'--js-flags=--expose-gc',
'--remote-debugging-address=127.0.0.1',
`--remote-debugging-port=${config.rendererCdpPort}`,
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
],
env: {
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
IPTVNATOR_PERF_CAPTURE: '1',
IPTVNATOR_PERF_WORKER_PROFILING: '1',
IPTVNATOR_TRACE_RENDERER_CONSOLE:
process.env['IPTVNATOR_TRACE_RENDERER_CONSOLE'] ?? '0',
},
});
app.mainWindow.setDefaultTimeout(120_000);
await assertRendererCdpTarget(
app.mainWindow,
config.rendererCdpPort
);
await installMainCapture(app.electronApp);
const rendererWindowIdentity = await resolveRendererWindowIdentity(app);
const requestsBefore = server.requestCount();
const diagnostic =
definition.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC;
renderer = await startM3uImportRendererCapture(app.mainWindow, {
diagnostic,
outputDirectory: iterationDirectory,
playlistTitle: `Synthetic M3U import ${definition.channelCount}`,
playlistUrl: server.resourceUrl,
});
await startMainCapture(app.electronApp, {
diagnostic,
outputDirectory: iterationDirectory,
rendererWindowIdentity,
});
await renderer.triggerImport();
await renderer.waitForTerminal();
await waitForDatabaseSettlement(app);
assertM3uImportRequestDelta(requestsBefore, server.requestCount());
const [rendererMetrics, mainMetrics] = await Promise.all([
renderer.stop(),
stopMainCapture(app.electronApp),
]);
await Promise.all([
writeJson(
join(iterationDirectory, 'main-capture.json'),
mainMetrics
),
writeJson(
join(iterationDirectory, 'renderer-capture.json'),
rendererMetrics
),
]);
const result = createM3uImportIterationResult({
channelCount: definition.channelCount,
fixtureBytes: fixture.bytes,
kind: definition.kind,
main: mainMetrics,
renderer: rendererMetrics,
runId: definition.runId,
scenarioId: definition.scenarioId,
});
await writeJson(join(iterationDirectory, 'result.json'), result);
return result;
} finally {
if (renderer) {
await renderer.dispose().catch((error: unknown) => {
console.error(
'[performance] Renderer capture cleanup failed',
error
);
});
}
if (app) {
await closeElectronApp(app).catch((error: unknown) => {
console.error('[performance] Electron cleanup failed', error);
});
}
await rm(dataDirectory, { force: true, recursive: true });
}
}
async function waitForDatabaseSettlement(
app: LaunchedElectronApp
): Promise<void> {
await expect
.poll(
async () => {
const status = await readMainCaptureStatus(app.electronApp);
return (
status.databaseRequests === 2 &&
status.databaseUpsertsCompleted === 1 &&
status.databaseGetsCompleted === 1 &&
status.databasePending === 0 &&
status.preloadSuccessMarkers === 2
);
},
{ timeout: 120_000 }
)
.toBe(true);
}
function createManifest(
config: M3uImportBenchmarkConfiguration,
scenario: M3uImportScenario,
fixture: SyntheticM3uFixture
): M3uImportBenchmarkManifest {
return Object.freeze({
channelCount: fixture.channelCount,
fixtureBytes: fixture.bytes,
fixtureSha256: fixture.sha256,
gitCommit: config.sourceState.commit,
gitDiffSha256: config.sourceState.diffSha256,
gitDirty: config.sourceState.dirty,
measuredRuns: config.measuredRuns,
memoryAccounting: Object.freeze({
databaseWorkerRss: 'unavailable-per-worker-thread',
mainRss:
'electron-main-process-including-worker-threads-and-native-memory',
nativeAndSqliteMemory:
'included-in-main-rss-not-separately-attributable',
rendererRss: 'separate-renderer-process',
workerJsHeap: 'separate-v8-isolate',
}),
runtime: Object.freeze({
arch: process.arch,
cdpPort: config.rendererCdpPort,
electronVersion: config.electronVersion,
harnessNodeVersion: process.versions.node,
platform: process.platform,
}),
scenario: scenario.id,
syntheticSeed: SYNTHETIC_M3U_SEED,
variant: config.variant,
warmupRuns: 1,
});
}
function requireDiagnosticIteration(
iterations: readonly M3uImportIterationResult[]
): M3uImportIterationResult {
const diagnostic = iterations.filter(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC
);
if (diagnostic.length !== 1 || !diagnostic[0]) {
throw new Error('M3U import diagnostic iteration is missing');
}
return diagnostic[0];
}
export function assertM3uImportRequestDelta(
before: number,
after: number
): void {
const oneRequest =
Number.isSafeInteger(before) &&
Number.isSafeInteger(after) &&
after === before + 1;
if (!oneRequest) {
throw new Error(
'Synthetic M3U import must issue exactly one application GET'
);
}
}
@@ -1,3 +1,10 @@
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
import type { RendererRssValidity } from './renderer-rss-validity';
import type {
AppPlaylistPerformancePhase,
PerformancePhaseEvent,
} from '@iptvnator/shared/interfaces';
export const PERFORMANCE_ITERATION_KIND = {
DIAGNOSTIC: 'diagnostic',
MEASURED: 'measured',
@@ -15,6 +22,46 @@ export const PERFORMANCE_WORKER_KIND = {
export type PerformanceWorkerKind =
(typeof PERFORMANCE_WORKER_KIND)[keyof typeof PERFORMANCE_WORKER_KIND];
export const WORKER_POST_GC_HEAP_UNAVAILABLE_REASON = {
CAPTURE_FAILED: 'capture-failed',
DATABASE_WORKER_ACTIVITY_AFTER_CUTOFF:
'database-worker-activity-after-cutoff',
DATABASE_WORKER_MISSING: 'database-worker-missing',
DATABASE_WORKER_NOT_IDLE: 'database-worker-not-idle',
DATABASE_WORKER_UNEXPECTED_ACTIVITY: 'database-worker-unexpected-activity',
GC_UNAVAILABLE: 'gc-unavailable',
INVALID_CAPTURE: 'post-gc-capture-invalid',
MULTIPLE_DATABASE_WORKERS: 'multiple-database-workers',
PROBE_INVALID_RESPONSE: 'post-gc-probe-invalid-response',
PROBE_MESSAGE_ERROR: 'post-gc-probe-message-error',
PROBE_NOT_RUN: 'post-gc-probe-not-run',
PROBE_PORT_CLOSED: 'post-gc-probe-port-closed',
PROBE_POST_FAILED: 'post-gc-probe-post-failed',
PROBE_TIMEOUT: 'post-gc-probe-timeout',
PROFILING_DISABLED: 'profiling-disabled',
WORKER_BUSY: 'worker-busy',
WORKER_FORCE_TERMINATED: 'worker-force-terminated-before-gc',
} as const;
export type WorkerPostGcHeapUnavailableReason =
(typeof WORKER_POST_GC_HEAP_UNAVAILABLE_REASON)[keyof typeof WORKER_POST_GC_HEAP_UNAVAILABLE_REASON];
export type WorkerPostGcHeapCapture =
| {
readonly postGcHeapUnavailableReason: null;
readonly postGcHeapUsedBytes: number;
}
| {
readonly postGcHeapUnavailableReason: WorkerPostGcHeapUnavailableReason;
readonly postGcHeapUsedBytes: null;
};
export const WORKER_PEAK_MEMORY_UNAVAILABLE_REASON = {
EXTERNAL_MEMORY_SAMPLES_MISSING:
'worker-external-memory-samples-missing',
HEAP_USED_SAMPLES_MISSING: 'worker-heap-used-samples-missing',
} as const;
export interface NumericDistribution {
readonly count: number;
readonly max: number | null;
@@ -39,33 +86,76 @@ export interface ProcessMemoryMetrics {
}
export interface MainTimelineRecord {
readonly boundary?: 'end' | 'start';
readonly byteCount?: number;
readonly durationMs?: number | null;
readonly epochMs: number;
readonly ipcCallId?: number;
readonly itemCount?: number;
readonly operation?: string;
readonly operationId?: string;
readonly phase?: string;
readonly playlistId?: string;
readonly requestId?: string;
readonly success?: boolean;
readonly sourceEpochMs?: number;
readonly type: string;
}
export interface WorkerCaptureMetrics {
export interface WorkerRequestPerformanceMetrics {
readonly eventLoopDelay: EventLoopDelayMetrics | null;
readonly eventLoopDelayUnavailableReason: string | null;
readonly eventLoopUtilization: number | null;
readonly eventLoopUtilizationUnavailableReason: string | null;
readonly histogramFlushedEpochMs: number | null;
readonly ipcCallId: number | null;
readonly invalidReason: string | null;
readonly operation: string | null;
readonly operationId: string | null;
readonly operationIdUnavailableReason: string | null;
readonly performanceCaptureUnavailableReason: string | null;
readonly phaseEvents: readonly PerformancePhaseEvent<AppPlaylistPerformancePhase>[];
readonly playlistId: string | null;
readonly requestId: string | null;
readonly requestReceivedEpochMs: number | null;
readonly responseEpochMs: number;
readonly responsePostedEpochMs: number | null;
readonly sourceEpochMs: number | null;
readonly success: boolean;
readonly threadCpuSystemMicros: number | null;
readonly threadCpuUnavailableReason: string | null;
readonly threadCpuUserMicros: number | null;
readonly workEndedEpochMs: number | null;
readonly workStartedEpochMs: number | null;
}
export type WorkerCaptureMetrics = {
readonly cancelPostedEpochMs: number | null;
readonly cpuSystemMicros: number | null;
readonly cpuUserMicros: number | null;
readonly eventLoopDelay: EventLoopDelayMetrics | null;
readonly eventLoopDelayUnavailableReason: string | null;
readonly eventLoopUtilization: number | null;
readonly externalMemorySampleCount: number;
readonly heapUsedSampleCount: number;
readonly kind: PerformanceWorkerKind;
readonly operationId: string | null;
readonly peakExternalBytes: number;
readonly peakHeapUsedBytes: number;
readonly ordinal: number;
readonly peakExternalBytes: number | null;
readonly peakExternalUnavailableReason:
| typeof WORKER_PEAK_MEMORY_UNAVAILABLE_REASON.EXTERNAL_MEMORY_SAMPLES_MISSING
| null;
readonly peakHeapUsedBytes: number | null;
readonly peakHeapUsedUnavailableReason:
| typeof WORKER_PEAK_MEMORY_UNAVAILABLE_REASON.HEAP_USED_SAMPLES_MISSING
| null;
readonly playlistId: string | null;
readonly postGcHeapUsedBytes: number | null;
readonly profilePath: string | null;
readonly requests: readonly WorkerRequestPerformanceMetrics[];
readonly responseEpochMs: number | null;
readonly snapshotPath: string | null;
readonly terminatedEpochMs: number | null;
}
} & WorkerPostGcHeapCapture;
export interface MainCaptureMetrics {
readonly cpuProfilePath: string | null;
@@ -76,11 +166,17 @@ export interface MainCaptureMetrics {
readonly eventLoopUtilizationUnavailableReason: string | null;
readonly heapSnapshotPath: string | null;
readonly memory: ProcessMemoryMetrics;
readonly rendererPeakRssBytes: number;
readonly rendererWindow: {
readonly responsiveEvents: number;
readonly rss: RendererProcessRssCapture;
readonly unresponsiveEvents: number;
readonly windowIdentity: {
readonly browserWindowId: number;
readonly webContentsId: number;
};
};
readonly rssScope: 'electron-main-process-including-worker-threads-and-native-memory';
readonly timeline: readonly MainTimelineRecord[];
readonly unresponsiveEvents: number;
readonly responsiveEvents: number;
readonly workers: readonly WorkerCaptureMetrics[];
}
@@ -167,6 +263,27 @@ export interface CancellationBenchmarkManifest {
readonly warmupRuns: number;
}
export interface InvalidDatabaseWorkerPostGcMeasuredRun {
readonly databaseWorkerCount: number;
readonly reason: string;
readonly runId: string;
}
export interface NotApplicableDatabaseWorkerPostGcMeasuredRun {
readonly reason: 'operation-cancelled-before-database-phase';
readonly runId: string;
}
export interface DatabaseWorkerPostGcValidity {
readonly applicableMeasuredRunCount: number;
readonly invalidMeasuredRuns: readonly InvalidDatabaseWorkerPostGcMeasuredRun[];
readonly measuredRunCount: number;
readonly notApplicableMeasuredRuns: readonly NotApplicableDatabaseWorkerPostGcMeasuredRun[];
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
export interface CancellationBenchmarkSummary {
readonly cancellationEffectRate: number;
readonly iterations: readonly CancellationIterationResult[];
@@ -176,13 +293,15 @@ export interface CancellationBenchmarkSummary {
readonly cancelToDurableTerminalMs: NumericDistribution;
readonly cancelToWorkerTerminatedMs: NumericDistribution;
readonly cancelTransportLatencyMs: NumericDistribution;
readonly databaseWorkerEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerEventLoopUtilization: NumericDistribution;
readonly databaseWorkerExternalPeakBytes: NumericDistribution;
readonly databaseWorkerHeapPeakBytes: NumericDistribution;
readonly databaseWorkerPostGcHeapBytes: NumericDistribution;
readonly databaseWorkerRequestEventLoopDelayMaxMs: NumericDistribution;
readonly databaseWorkerRequestEventLoopDelayP95Ms: NumericDistribution;
readonly databaseWorkerRequestEventLoopDelayP99Ms: NumericDistribution;
readonly databaseWorkerRequestEventLoopUtilization: NumericDistribution;
readonly databaseWorkerRequestThreadCpuSystemMicros: NumericDistribution;
readonly databaseWorkerRequestThreadCpuUserMicros: NumericDistribution;
readonly dataFetchMs: NumericDistribution;
readonly dbCommitProxyMs: NumericDistribution;
readonly ipcStoreDispatchProxyMs: NumericDistribution;
@@ -196,13 +315,15 @@ export interface CancellationBenchmarkSummary {
readonly mainRssPostGcBytes: NumericDistribution;
readonly parseNormalizeCloneProxyMs: NumericDistribution;
readonly persistencePreparationProxyMs: NumericDistribution;
readonly playlistWorkerEventLoopUtilization: NumericDistribution;
readonly playlistWorkerEventLoopDelayMaxMs: NumericDistribution;
readonly playlistWorkerEventLoopDelayP95Ms: NumericDistribution;
readonly playlistWorkerEventLoopDelayP99Ms: NumericDistribution;
readonly playlistWorkerExternalPeakBytes: NumericDistribution;
readonly playlistWorkerHeapPeakBytes: NumericDistribution;
readonly playlistWorkerPostGcHeapBytes: NumericDistribution;
readonly playlistWorkerRequestEventLoopDelayMaxMs: NumericDistribution;
readonly playlistWorkerRequestEventLoopDelayP95Ms: NumericDistribution;
readonly playlistWorkerRequestEventLoopDelayP99Ms: NumericDistribution;
readonly playlistWorkerRequestEventLoopUtilization: NumericDistribution;
readonly playlistWorkerRequestThreadCpuSystemMicros: NumericDistribution;
readonly playlistWorkerRequestThreadCpuUserMicros: NumericDistribution;
readonly rendererFrameGapMs: NumericDistribution;
readonly rendererHeartbeatDelayMs: NumericDistribution;
readonly rendererHeapPeakBytes: NumericDistribution;
@@ -216,4 +337,8 @@ export interface CancellationBenchmarkSummary {
readonly unresponsiveEvents: number;
readonly visibleTotalMs: NumericDistribution;
};
readonly validity: {
readonly databaseWorkerPostGc: DatabaseWorkerPostGcValidity;
readonly rendererRss: RendererRssValidity;
};
}
@@ -9,6 +9,7 @@ import type {
NumericDistribution,
PerformanceWorkerKind,
RendererCaptureMetrics,
WorkerRequestPerformanceMetrics,
} from './m3u-refresh-cancellation-contract';
import {
PERFORMANCE_ITERATION_KIND,
@@ -18,6 +19,8 @@ import {
nonNegativeDifference,
summarizeNumbers,
} from './performance-statistics';
import { assessDatabaseWorkerPostGcValidity } from './database-worker-post-gc-validity';
import { assessRendererRssValidity } from './renderer-rss-validity';
export function createCancellationIterationResult(input: {
readonly kind: CancellationIterationResult['kind'];
@@ -56,22 +59,34 @@ export function createCancellationBenchmarkSummary(
select: (worker: MainCaptureMetrics['workers'][number]) => number | null
): NumericDistribution =>
summarizeNumbers(
measured.map((iteration) => {
const worker = iteration.main.workers.find(
(candidate) => candidate.kind === kind
);
return worker ? select(worker) : null;
})
measured.flatMap((iteration) =>
iteration.main.workers
.filter((worker) => worker.kind === kind)
.map((worker) => select(worker))
)
);
const workerEventLoopDelayMetric = (
const workerRequestMetric = (
kind: PerformanceWorkerKind,
select: (request: WorkerRequestPerformanceMetrics) => number | null
): NumericDistribution =>
summarizeNumbers(
measured.flatMap((iteration) =>
iteration.main.workers
.filter((worker) => worker.kind === kind)
.flatMap((worker) =>
worker.requests.map((request) => select(request))
)
)
);
const workerRequestEventLoopDelayMetric = (
kind: PerformanceWorkerKind,
percentile: keyof NonNullable<
MainCaptureMetrics['workers'][number]['eventLoopDelay']
WorkerRequestPerformanceMetrics['eventLoopDelay']
>
): NumericDistribution =>
workerMetric(
workerRequestMetric(
kind,
(worker) => worker.eventLoopDelay?.[percentile] ?? null
(request) => request.eventLoopDelay?.[percentile] ?? null
);
return Object.freeze({
@@ -105,22 +120,6 @@ export function createCancellationBenchmarkSummary(
(iteration) => iteration.phases.cancelTransportLatencyMs
)
),
databaseWorkerEventLoopDelayMaxMs: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'maxMs'
),
databaseWorkerEventLoopDelayP95Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p95Ms'
),
databaseWorkerEventLoopDelayP99Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p99Ms'
),
databaseWorkerEventLoopUtilization: workerMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(worker) => worker.eventLoopUtilization
),
databaseWorkerExternalPeakBytes: workerMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(worker) => worker.peakExternalBytes
@@ -133,6 +132,33 @@ export function createCancellationBenchmarkSummary(
PERFORMANCE_WORKER_KIND.DATABASE,
(worker) => worker.postGcHeapUsedBytes
),
databaseWorkerRequestEventLoopDelayMaxMs:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'maxMs'
),
databaseWorkerRequestEventLoopDelayP95Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p95Ms'
),
databaseWorkerRequestEventLoopDelayP99Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
'p99Ms'
),
databaseWorkerRequestEventLoopUtilization: workerRequestMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(request) => request.eventLoopUtilization
),
databaseWorkerRequestThreadCpuSystemMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(request) => request.threadCpuSystemMicros
),
databaseWorkerRequestThreadCpuUserMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.DATABASE,
(request) => request.threadCpuUserMicros
),
dataFetchMs: summarizeNumbers(
measured.map((iteration) => iteration.phases.dataFetchMs)
),
@@ -185,22 +211,6 @@ export function createCancellationBenchmarkSummary(
iteration.phases.persistencePreparationProxyMs
)
),
playlistWorkerEventLoopUtilization: workerMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(worker) => worker.eventLoopUtilization
),
playlistWorkerEventLoopDelayMaxMs: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'maxMs'
),
playlistWorkerEventLoopDelayP95Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p95Ms'
),
playlistWorkerEventLoopDelayP99Ms: workerEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p99Ms'
),
playlistWorkerExternalPeakBytes: workerMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(worker) => worker.peakExternalBytes
@@ -213,6 +223,33 @@ export function createCancellationBenchmarkSummary(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(worker) => worker.postGcHeapUsedBytes
),
playlistWorkerRequestEventLoopDelayMaxMs:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'maxMs'
),
playlistWorkerRequestEventLoopDelayP95Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p95Ms'
),
playlistWorkerRequestEventLoopDelayP99Ms:
workerRequestEventLoopDelayMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
'p99Ms'
),
playlistWorkerRequestEventLoopUtilization: workerRequestMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(request) => request.eventLoopUtilization
),
playlistWorkerRequestThreadCpuSystemMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(request) => request.threadCpuSystemMicros
),
playlistWorkerRequestThreadCpuUserMicros: workerRequestMetric(
PERFORMANCE_WORKER_KIND.PLAYLIST_REFRESH,
(request) => request.threadCpuUserMicros
),
rendererFrameGapMs: summarizeNumbers(
flattenRenderer(
(iteration) => iteration.renderer.probe.frameGapsMs
@@ -244,10 +281,14 @@ export function createCancellationBenchmarkSummary(
)
),
rendererRssPeakBytes: summarizeNumbers(
measured.map((iteration) => iteration.main.rendererPeakRssBytes)
measured.map(
(iteration) =>
iteration.main.rendererWindow.rss.peakRssBytes
)
),
responsiveEvents: measured.reduce(
(sum, iteration) => sum + iteration.main.responsiveEvents,
(sum, iteration) =>
sum + iteration.main.rendererWindow.responsiveEvents,
0
),
totalMs: summarizeNumbers(
@@ -259,13 +300,19 @@ export function createCancellationBenchmarkSummary(
)
),
unresponsiveEvents: measured.reduce(
(sum, iteration) => sum + iteration.main.unresponsiveEvents,
(sum, iteration) =>
sum + iteration.main.rendererWindow.unresponsiveEvents,
0
),
visibleTotalMs: summarizeNumbers(
measured.map((iteration) => iteration.phases.visibleTotalMs)
),
}),
validity: Object.freeze({
databaseWorkerPostGc:
assessDatabaseWorkerPostGcValidity(iterations),
rendererRss: assessRendererRssValidity(iterations),
}),
});
}
@@ -35,13 +35,17 @@ import {
createCancellationBenchmarkSummary,
createCancellationIterationResult,
} from './m3u-refresh-cancellation-report';
import { validateDiagnosticPlaylistWorkerCapture } from './diagnostic-playlist-worker-capture';
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
import {
installMainCapture,
readMainCaptureStatus,
rolloverMainCapture,
startMainCapture,
stopMainCapture,
} from './m3u-refresh-main-capture';
import { startRendererCapture } from './m3u-refresh-renderer-capture';
import type { RendererWindowIdentity } from './renderer-window-rss-session';
import {
createSyntheticM3uFixture,
SYNTHETIC_M3U_CHANNEL_COUNT,
@@ -133,6 +137,40 @@ export async function runM3uRefreshCancellationBenchmark(): Promise<void> {
const summary = createCancellationBenchmarkSummary(manifest, iterations);
await writeJson(join(config.outputDirectory, 'manifest.json'), manifest);
await writeJson(join(config.outputDirectory, 'summary.json'), summary);
const diagnosticIteration = iterations.find(
(iteration) => iteration.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC
);
if (!diagnosticIteration) {
throw new Error('Diagnostic iteration is missing');
}
if (!diagnosticIteration.cancellationEffectObserved) {
throw new Error(
'Cancellation effect was not observed in the diagnostic run'
);
}
await validateDiagnosticPlaylistWorkerCapture(
diagnosticIteration.main,
join(config.outputDirectory, diagnosticIteration.runId)
);
if (summary.cancellationEffectRate !== 1) {
throw new Error(
`Cancellation effect was not observed in every measured run: ${summary.cancellationEffectRate}`
);
}
if (!summary.validity.rendererRss.validForBenchmark) {
throw new Error(
`Renderer RSS capture is invalid: ${JSON.stringify(
summary.validity.rendererRss.invalidMeasuredRuns
)}`
);
}
if (!summary.validity.databaseWorkerPostGc.validForBenchmark) {
throw new Error(
`Database worker post-GC capture is invalid: ${JSON.stringify(
summary.validity.databaseWorkerPostGc.invalidMeasuredRuns
)}`
);
}
console.log(
`[performance] completed: ${join(
config.outputDirectory,
@@ -147,10 +185,11 @@ async function runIteration(
server: SyntheticServer
): Promise<CancellationIterationResult> {
const iterationDirectory = join(config.outputDirectory, definition.runId);
await assertPerformanceArtifactCapacity(iterationDirectory);
await mkdir(iterationDirectory, { recursive: false });
const dataDirectory = await mkdtemp(
join(tmpdir(), 'iptvnator-m3u-performance-')
);
await mkdir(iterationDirectory, { recursive: false });
let app: LaunchedElectronApp | null = null;
let rendererConsoleListener: ((message: ConsoleMessage) => void) | null =
null;
@@ -160,12 +199,14 @@ async function runIteration(
server.serveSeedFixture();
app = await launchElectronApp(dataDirectory, {
args: [
'--js-flags=--expose-gc',
'--remote-debugging-address=127.0.0.1',
`--remote-debugging-port=${RENDERER_CDP_PORT}`,
`--user-data-dir=${join(dataDirectory, 'user-data')}`,
],
env: {
IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: '0',
IPTVNATOR_PERF_CAPTURE: '1',
IPTVNATOR_PERF_WORKER_PROFILING: '1',
IPTVNATOR_TRACE_RENDERER_CONSOLE:
process.env['IPTVNATOR_TRACE_RENDERER_CONSOLE'] ?? '0',
@@ -174,6 +215,12 @@ async function runIteration(
await assertRendererCdpTarget(app.mainWindow);
app.mainWindow.setDefaultTimeout(120_000);
await installMainCapture(app.electronApp);
const rendererWindowIdentity = await resolveRendererWindowIdentity(app);
await startMainCapture(app.electronApp, {
diagnostic: false,
outputDirectory: iterationDirectory,
rendererWindowIdentity,
});
await seedPlaylist(app.mainWindow, server.resourceUrl);
await waitForSeedSettlement(app);
server.serveLargeFixture();
@@ -190,10 +237,20 @@ async function runIteration(
const diagnostic =
definition.kind === PERFORMANCE_ITERATION_KIND.DIAGNOSTIC;
await startMainCapture(app.electronApp, {
const seedRollover = await rolloverMainCapture(app.electronApp, {
diagnostic,
outputDirectory: iterationDirectory,
rendererWindowIdentity,
});
await writeJson(
join(iterationDirectory, 'seed-main-capture.json'),
seedRollover.completedCapture
);
if (!seedRollover.nextCaptureStarted) {
throw new Error(
`Seed capture could not atomically arm the measured generation: ${seedRollover.nextCaptureUnavailableReason}`
);
}
const renderer = await startRendererCapture(app.mainWindow, {
diagnostic,
outputDirectory: iterationDirectory,
@@ -235,6 +292,28 @@ async function runIteration(
}
}
async function resolveRendererWindowIdentity(
app: LaunchedElectronApp
): Promise<RendererWindowIdentity> {
const browserWindowHandle = await app.electronApp.browserWindow(
app.mainWindow
);
try {
return await browserWindowHandle.evaluate((browserWindow) => {
const exactWindow = browserWindow as unknown as {
readonly id: number;
readonly webContents: { readonly id: number };
};
return {
browserWindowId: exactWindow.id,
webContentsId: exactWindow.webContents.id,
};
});
} finally {
await browserWindowHandle.dispose();
}
}
async function seedPlaylist(page: Page, playlistUrl: string): Promise<void> {
await page.getByRole('button', { name: 'Add playlist' }).first().click();
const dialog = page.locator('mat-dialog-container').last();
@@ -258,11 +337,17 @@ async function seedPlaylist(page: Page, playlistUrl: string): Promise<void> {
async function waitForSeedSettlement(app: LaunchedElectronApp): Promise<void> {
await expect
.poll(
async () =>
(await readMainCaptureStatus(app.electronApp)).databasePending,
async () => {
const status = await readMainCaptureStatus(app.electronApp);
return (
status.databaseRequests > 0 &&
status.databaseUpsertsCompleted > 0 &&
status.databasePending === 0
);
},
{ timeout: 120_000 }
)
.toBe(0);
.toBe(true);
await waitForTwoAnimationFrames(app.mainWindow);
}
@@ -362,6 +447,7 @@ async function resolveConfiguration(): Promise<BenchmarkConfiguration> {
}
const outputDirectory = join(outputRoot, variant);
await assertMissing(outputDirectory);
await assertPerformanceArtifactCapacity(outputDirectory);
await mkdir(outputDirectory, { recursive: true });
return Object.freeze({
electronVersion: electronPackage.version,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,124 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import { dirname, resolve } from 'node:path';
import {
assertPerformanceArtifactCapacity,
MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
} from './performance-artifact-preflight';
test('checks the nearest existing ancestor using available blocks', async () => {
const requestedPath = resolve(
'/workspace/dist/performance/2026-07-26/baseline'
);
const checkedPaths: string[] = [];
const existingAncestor = dirname(dirname(requestedPath));
const result = await assertPerformanceArtifactCapacity(requestedPath, {
statfs: async (path) => {
checkedPaths.push(path);
if (path !== existingAncestor) {
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
}
return {
bavail: 4_194_304n,
bsize: 1_024n,
};
},
});
assert.deepEqual(checkedPaths, [
requestedPath,
dirname(requestedPath),
existingAncestor,
]);
assert.deepEqual(result, {
availableBytes: 4_294_967_296n,
checkedPath: existingAncestor,
minimumBytes: MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
});
});
test('accepts exactly two GiB of available capacity', async () => {
const result = await assertPerformanceArtifactCapacity('/existing', {
statfs: async () => ({
bavail: 2_097_152n,
bsize: 1_024n,
}),
});
assert.equal(result.availableBytes, MIN_PERFORMANCE_ARTIFACT_FREE_BYTES);
});
test('fails before artifact creation when available capacity is below two GiB', async () => {
await assert.rejects(
assertPerformanceArtifactCapacity('/existing', {
statfs: async () => ({
bavail: 2_097_151n,
bsize: 1_024n,
}),
}),
/requires at least 2147483648 available bytes.*2147482624/
);
});
test('does not substitute free blocks that are unavailable to the process', async () => {
await assert.rejects(
assertPerformanceArtifactCapacity('/existing', {
statfs: async () => ({
bavail: 1n,
bfree: 9_999_999_999n,
bsize: 1_024n,
}),
}),
/performance-artifact-space-insufficient/
);
});
test('does not hide filesystem errors other than a missing path', async () => {
const denied = Object.assign(new Error('denied'), { code: 'EACCES' });
let calls = 0;
await assert.rejects(
assertPerformanceArtifactCapacity('/denied/child', {
statfs: async () => {
calls += 1;
throw denied;
},
}),
denied
);
assert.equal(calls, 1);
});
test('the benchmark preflights capacity before creating output directories', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const runIteration = source.slice(
source.indexOf('async function runIteration('),
source.indexOf('async function seedPlaylist(')
);
const resolveConfiguration = source.slice(
source.indexOf('async function resolveConfiguration('),
source.indexOf('function readGitSourceState(')
);
for (const [name, section] of [
['iteration', runIteration],
['variant', resolveConfiguration],
] as const) {
const preflight = section.indexOf(
'await assertPerformanceArtifactCapacity('
);
const createDirectory = section.indexOf('await mkdir(');
assert.ok(preflight >= 0, `${name} preflight is missing`);
assert.ok(
preflight < createDirectory,
`${name} preflight must run before mkdir`
);
}
});
@@ -0,0 +1,76 @@
import { statfs } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
export const MIN_PERFORMANCE_ARTIFACT_FREE_BYTES =
2n * 1_024n * 1_024n * 1_024n;
interface FileSystemCapacity {
readonly bavail: bigint;
readonly bsize: bigint;
}
interface PerformanceArtifactPreflightDependencies {
readonly statfs?: (path: string) => Promise<FileSystemCapacity>;
}
export interface PerformanceArtifactCapacity {
readonly availableBytes: bigint;
readonly checkedPath: string;
readonly minimumBytes: bigint;
}
async function readFileSystemCapacity(
path: string
): Promise<FileSystemCapacity> {
const capacity = await statfs(path, { bigint: true });
return {
bavail: capacity.bavail,
bsize: capacity.bsize,
};
}
function isMissingPath(error: unknown): boolean {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
error.code === 'ENOENT'
);
}
export async function assertPerformanceArtifactCapacity(
targetPath: string,
dependencies: PerformanceArtifactPreflightDependencies = {}
): Promise<PerformanceArtifactCapacity> {
const readCapacity = dependencies.statfs ?? readFileSystemCapacity;
let checkedPath = resolve(targetPath);
while (true) {
try {
const capacity = await readCapacity(checkedPath);
const availableBytes = capacity.bavail * capacity.bsize;
if (availableBytes < MIN_PERFORMANCE_ARTIFACT_FREE_BYTES) {
throw new Error(
`performance-artifact-space-insufficient: benchmark requires at least ${MIN_PERFORMANCE_ARTIFACT_FREE_BYTES} available bytes; ${availableBytes} available at ${checkedPath}`
);
}
return Object.freeze({
availableBytes,
checkedPath,
minimumBytes: MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
});
} catch (error) {
if (!isMissingPath(error)) {
throw error;
}
const parent = dirname(checkedPath);
if (parent === checkedPath) {
throw new Error(
`performance-artifact-space-unavailable: no existing ancestor for ${targetPath}`,
{ cause: error }
);
}
checkedPath = parent;
}
}
}
@@ -0,0 +1,303 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based configuration contract tests. */
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import test from 'node:test';
import { join } from 'node:path';
interface TargetConfiguration {
configurations?: Record<string, Record<string, unknown>>;
dependsOn?: unknown;
executor?: unknown;
options?: Record<string, unknown>;
}
interface ProjectConfiguration {
targets: Record<string, TargetConfiguration>;
}
interface NxGraphTask {
outputs?: string[];
target: {
project: string;
target: string;
};
}
interface NxGraph {
tasks: {
tasks: Record<string, NxGraphTask>;
};
}
const workspaceRoot = fileURLToPath(new URL('../../../../', import.meta.url));
function readProject(relativePath: string): ProjectConfiguration {
return JSON.parse(
readFileSync(join(workspaceRoot, relativePath), 'utf8')
) as ProjectConfiguration;
}
function readResolvedWebBuildTask(): NxGraphTask | undefined {
const environment = {
...process.env,
FORCE_COLOR: '0',
NX_DAEMON: 'false',
};
delete environment['NO_COLOR'];
const graph = JSON.parse(
execFileSync(
process.execPath,
[
join(workspaceRoot, 'node_modules/nx/dist/bin/nx.js'),
'run',
'web:build',
'--graph=stdout',
],
{
cwd: workspaceRoot,
encoding: 'utf8',
env: environment,
}
)
) as NxGraph;
return Object.values(graph.tasks.tasks).find(
(task) =>
task.target.project === 'web' && task.target.target === 'build'
);
}
const e2eProject = readProject('apps/electron-backend-e2e/project.json');
const electronProject = readProject('apps/electron-backend/project.json');
const remoteControlProject = readProject(
'apps/remote-control-web/project.json'
);
const webProject = readProject('apps/web/project.json');
test('the performance harness target runs only Node performance specs', () => {
const target = e2eProject.targets['test-performance-harness'];
assert.ok(
target,
'electron-backend-e2e must define test-performance-harness'
);
assert.equal(target.executor, 'nx:run-commands');
assert.equal(target.options?.['cwd'], 'apps/electron-backend-e2e');
assert.equal(
target.options?.['command'],
'pnpm exec tsx --test src/performance/*.spec.ts'
);
});
test('the web performance build keeps production renderer behavior with profiling source maps', () => {
const build = webProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'web:build must define production');
assert.ok(performance, 'web:build must define electron-performance');
assert.equal(performance['baseHref'], './');
assert.equal(performance['serviceWorker'], false);
assert.deepEqual(performance['optimization'], production['optimization']);
assert.equal(performance['outputHashing'], production['outputHashing']);
assert.deepEqual(
performance['fileReplacements'],
production['fileReplacements']
);
assert.equal(performance['sourceMap'], true);
});
test('the resolved web build cache output is the renderer directory', () => {
const task = readResolvedWebBuildTask();
assert.ok(task, 'the Nx graph must include web:build');
assert.deepEqual(task.outputs, ['dist/apps/web']);
});
test('the remote-control performance build keeps optimized hashed output with profiling source maps', () => {
const build = remoteControlProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'remote-control-web:build must define production');
assert.ok(
performance,
'remote-control-web:build must define electron-performance'
);
assert.equal(performance['optimization'], true);
assert.equal(performance['outputHashing'], production['outputHashing']);
assert.equal(performance['sourceMap'], true);
});
test('the Electron performance build keeps production main-process behavior with profiling source maps', () => {
const build = electronProject.targets['build'];
const production = build.configurations?.['production'];
const performance = build.configurations?.['electron-performance'];
assert.ok(production, 'electron-backend:build must define production');
assert.ok(
performance,
'electron-backend:build must define electron-performance'
);
assert.equal(performance['optimization'], production['optimization']);
assert.equal(performance['inspect'], production['inspect']);
assert.deepEqual(
performance['fileReplacements'],
production['fileReplacements']
);
assert.equal(performance['sourceMap'], true);
});
test('the regular Electron build keeps its existing renderer dependency contract', () => {
const build = electronProject.targets['build'];
const dependencies = build.dependsOn as Array<
Record<string, unknown> | string
>;
const rendererBuild = dependencies.find(
(dependency): dependency is Record<string, unknown> =>
typeof dependency === 'object' &&
dependency !== null &&
dependency['target'] === 'build'
);
assert.deepEqual(rendererBuild, {
projects: ['web', 'remote-control-web'],
target: 'build',
});
});
test('the web performance wrapper selects the profiling configuration', () => {
const target = webProject.targets['build-performance'];
assert.ok(target, 'web must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'pnpm nx run web:build:electron-performance'
);
});
test('the remote-control performance wrapper selects the profiling configuration', () => {
const target = remoteControlProject.targets['build-performance'];
assert.ok(target, 'remote-control-web must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'pnpm nx run remote-control-web:build:electron-performance'
);
});
test('the Electron performance wrapper owns build dependencies before its profiled build', () => {
const target = electronProject.targets['build-performance'];
assert.ok(target, 'electron-backend must define build-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.deepEqual(target.dependsOn, [
'electron-backend:build-worker-performance',
'electron-backend:build-embedded-mpv',
{
projects: ['web', 'remote-control-web'],
target: 'build-performance',
},
]);
assert.equal(
target.options?.['command'],
'pnpm nx run electron-backend:build:electron-performance --excludeTaskDependencies'
);
});
test('the Electron performance wrapper builds optimized source-mapped workers', () => {
const target = electronProject.targets['build-worker-performance'];
const source = readFileSync(
join(workspaceRoot, 'apps/electron-backend/build-worker.js'),
'utf8'
);
assert.ok(target, 'electron-backend must define build-worker-performance');
assert.equal(target.executor, 'nx:run-commands');
assert.equal(
target.options?.['command'],
'node apps/electron-backend/build-worker.js --performance'
);
assert.match(
source,
/const isPerformance = process\.argv\.includes\('--performance'\)/
);
assert.match(source, /minify: isProduction \|\| isPerformance/);
assert.match(source, /sourcemap: isPerformance \|\| !isProduction/);
});
test('the cancellation benchmark uses the production-equivalent performance build', () => {
const target = e2eProject.targets['benchmark-m3u-refresh-cancellation'];
assert.deepEqual(target.dependsOn, ['electron-backend:build-performance']);
});
test('the cancellation benchmark command is pinned to its Playwright test file', () => {
const target = e2eProject.targets['benchmark-m3u-refresh-cancellation'];
assert.equal(
target.options?.['command'],
'pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-refresh-cancellation.performance.ts'
);
});
test('the cancellation benchmark enables preload performance capture', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
),
'utf8'
);
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
});
test('the cancellation benchmark exposes GC to Electron worker isolates', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-refresh-cancellation.benchmark.ts'
),
'utf8'
);
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});
test('the initial M3U import benchmark uses the production-equivalent performance build', () => {
const target = e2eProject.targets['benchmark-m3u-import'];
assert.ok(target, 'electron-backend-e2e must define benchmark-m3u-import');
assert.deepEqual(target.dependsOn, ['electron-backend:build-performance']);
});
test('the initial M3U import benchmark command is pinned to its Playwright test file', () => {
const target = e2eProject.targets['benchmark-m3u-import'];
assert.equal(
target.options?.['command'],
'pnpm exec playwright test --config=playwright.performance.config.ts src/m3u-import.performance.ts'
);
});
test('the initial M3U import benchmark enables opt-in capture and worker GC', () => {
const source = readFileSync(
join(
workspaceRoot,
'apps/electron-backend-e2e/src/performance/m3u-import.benchmark.ts'
),
'utf8'
);
assert.match(source, /IPTVNATOR_PERF_CAPTURE:\s*'1'/);
assert.match(source, /IPTVNATOR_PERF_WORKER_PROFILING:\s*'1'/);
assert.match(source, /'--js-flags=--expose-gc'/);
assert.doesNotMatch(source, /execArgv/);
});
@@ -0,0 +1,158 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
pairPerformancePhaseEvents,
requirePerformancePhase,
} from './performance-phase-events';
describe('performance phase event pairing', () => {
it('pairs request-scoped boundaries and preserves end metadata', () => {
const phases = pairPerformancePhaseEvents([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'data.acquire',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 12.5,
epochMs: 113,
metadata: { byteCount: 2048, itemCount: 10 },
phase: 'data.acquire',
requestId: 'request-1',
},
]);
assert.deepEqual(phases, [
{
durationMs: 12.5,
endedEpochMs: 113,
metadata: { byteCount: 2048, itemCount: 10 },
phase: 'data.acquire',
requestId: 'request-1',
startedEpochMs: 100,
},
]);
assert.deepEqual(
requirePerformancePhase(phases, 'request-1', 'data.acquire'),
phases[0]
);
});
it('pairs interleaved requests without cross-correlating them', () => {
const phases = pairPerformancePhaseEvents([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'serialize.playlist',
requestId: 'request-a',
},
{
boundary: 'start',
durationMs: null,
epochMs: 101,
phase: 'serialize.playlist',
requestId: 'request-b',
},
{
boundary: 'end',
durationMs: 3,
epochMs: 104,
phase: 'serialize.playlist',
requestId: 'request-b',
},
{
boundary: 'end',
durationMs: 8,
epochMs: 108,
phase: 'serialize.playlist',
requestId: 'request-a',
},
]);
assert.deepEqual(
phases.map(({ durationMs, requestId }) => ({
durationMs,
requestId,
})),
[
{ durationMs: 8, requestId: 'request-a' },
{ durationMs: 3, requestId: 'request-b' },
]
);
});
it('fails closed for orphaned, duplicate, malformed, or reversed boundaries', () => {
const validStart = {
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'normalize',
requestId: 'request-1',
} as const;
const validEnd = {
boundary: 'end',
durationMs: 2,
epochMs: 102,
phase: 'normalize',
requestId: 'request-1',
} as const;
for (const invalid of [
[validStart],
[validStart, validStart, validEnd],
[validStart, { ...validEnd, durationMs: null }],
[validStart, { ...validEnd, epochMs: 99 }],
[{ ...validStart, durationMs: 0 }, validEnd],
[
validStart,
{
...validEnd,
metadata: { byteCount: -1 },
},
],
]) {
assert.throws(
() => pairPerformancePhaseEvents(invalid),
/performance-phase-events-invalid/
);
}
});
it('requires one exact request and phase match', () => {
const phases = pairPerformancePhaseEvents([
{
boundary: 'start',
durationMs: null,
epochMs: 100,
phase: 'normalize',
requestId: 'request-1',
},
{
boundary: 'end',
durationMs: 2,
epochMs: 102,
phase: 'normalize',
requestId: 'request-1',
},
]);
assert.throws(
() => requirePerformancePhase(phases, 'request-2', 'normalize'),
/performance-phase-missing:request-2:normalize/
);
assert.throws(
() =>
requirePerformancePhase(
[...phases, ...phases],
'request-1',
'normalize'
),
/performance-phase-ambiguous:request-1:normalize/
);
});
});
@@ -0,0 +1,166 @@
import type { PerformancePhaseMetadata } from '@iptvnator/shared/interfaces';
export interface PairedPerformancePhase {
readonly durationMs: number;
readonly endedEpochMs: number;
readonly metadata: PerformancePhaseMetadata | null;
readonly phase: string;
readonly requestId: string;
readonly startedEpochMs: number;
}
interface ParsedPerformancePhaseEvent {
readonly boundary: 'end' | 'start';
readonly durationMs: number | null;
readonly epochMs: number;
readonly metadata: PerformancePhaseMetadata | null;
readonly phase: string;
readonly requestId: string;
}
export function pairPerformancePhaseEvents(
input: readonly unknown[]
): readonly PairedPerformancePhase[] {
const grouped = new Map<string, ParsedPerformancePhaseEvent[]>();
for (const value of input) {
const event = parsePerformancePhaseEvent(value);
const key = `${event.requestId}\u0000${event.phase}`;
const events = grouped.get(key) ?? [];
events.push(event);
if (events.length > 2) {
throw invalidPhaseEvents();
}
grouped.set(key, events);
}
const paired: PairedPerformancePhase[] = [];
for (const events of grouped.values()) {
const start = events.find((event) => event.boundary === 'start');
const end = events.find((event) => event.boundary === 'end');
if (
events.length !== 2 ||
!start ||
!end ||
start.durationMs !== null ||
start.metadata !== null ||
end.durationMs === null ||
end.epochMs < start.epochMs
) {
throw invalidPhaseEvents();
}
paired.push(
Object.freeze({
durationMs: end.durationMs,
endedEpochMs: end.epochMs,
metadata: end.metadata,
phase: start.phase,
requestId: start.requestId,
startedEpochMs: start.epochMs,
})
);
}
paired.sort(
(left, right) =>
left.startedEpochMs - right.startedEpochMs ||
left.endedEpochMs - right.endedEpochMs ||
left.requestId.localeCompare(right.requestId) ||
left.phase.localeCompare(right.phase)
);
return Object.freeze(paired);
}
export function requirePerformancePhase(
phases: readonly PairedPerformancePhase[],
requestId: string,
phase: string
): PairedPerformancePhase {
const matches = phases.filter(
(candidate) =>
candidate.requestId === requestId && candidate.phase === phase
);
if (matches.length === 0) {
throw new Error(`performance-phase-missing:${requestId}:${phase}`);
}
if (matches.length !== 1 || !matches[0]) {
throw new Error(`performance-phase-ambiguous:${requestId}:${phase}`);
}
return matches[0];
}
function parsePerformancePhaseEvent(
value: unknown
): ParsedPerformancePhaseEvent {
if (!isRecord(value)) {
throw invalidPhaseEvents();
}
const boundary = value['boundary'];
const durationMs = value['durationMs'];
const epochMs = value['epochMs'];
const phase = value['phase'];
const requestId = value['requestId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
(durationMs !== null && !isFiniteNonNegativeNumber(durationMs)) ||
!isFiniteNonNegativeNumber(epochMs) ||
typeof phase !== 'string' ||
phase.length === 0 ||
typeof requestId !== 'string' ||
requestId.length === 0
) {
throw invalidPhaseEvents();
}
return {
boundary,
durationMs,
epochMs,
metadata: parseMetadata(value['metadata']),
phase,
requestId,
};
}
function parseMetadata(value: unknown): PerformancePhaseMetadata | null {
if (value === undefined) {
return null;
}
if (!isRecord(value)) {
throw invalidPhaseEvents();
}
const keys = Object.keys(value);
if (
keys.some((key) => key !== 'byteCount' && key !== 'itemCount') ||
keys.some((key) => !isSafeCount(value[key]))
) {
throw invalidPhaseEvents();
}
return Object.freeze({
...(value['byteCount'] === undefined
? {}
: { byteCount: Number(value['byteCount']) }),
...(value['itemCount'] === undefined
? {}
: { itemCount: Number(value['itemCount']) }),
});
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function isSafeCount(value: unknown): boolean {
return (
value === undefined ||
(Number.isSafeInteger(value) && Number(value) >= 0)
);
}
function invalidPhaseEvents(): Error {
return new Error('performance-phase-events-invalid');
}
@@ -0,0 +1,133 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
pairRendererPerformancePhaseEvents,
requireRendererPerformancePhase,
} from './renderer-performance-phase-events';
describe('renderer performance phase event pairing', () => {
it('pairs a successful renderer phase using monotonic duration', () => {
const phases = pairRendererPerformancePhaseEvents([
{
boundary: 'start',
epochMs: 100,
metadata: { items: 10_000 },
monotonicMs: 20,
phase: 'store.m3u-publish-channels',
phaseId: 'phase-1',
schemaVersion: 1,
},
{
boundary: 'end',
epochMs: 106,
metadata: { items: 10_000 },
monotonicMs: 25.5,
outcome: 'success',
phase: 'store.m3u-publish-channels',
phaseId: 'phase-1',
schemaVersion: 1,
},
]);
assert.deepEqual(phases, [
{
durationMs: 5.5,
endedEpochMs: 106,
metadata: { items: 10_000 },
outcome: 'success',
phase: 'store.m3u-publish-channels',
phaseId: 'phase-1',
startedEpochMs: 100,
},
]);
assert.deepEqual(
requireRendererPerformancePhase(
phases,
'store.m3u-publish-channels'
),
phases[0]
);
});
it('fails closed for inconsistent identity, metadata, outcome, and clocks', () => {
const start = {
boundary: 'start',
epochMs: 100,
metadata: { items: 50_000 },
monotonicMs: 20,
phase: 'store.m3u-import-dispatch',
phaseId: 'phase-1',
schemaVersion: 1,
} as const;
const end = {
boundary: 'end',
epochMs: 102,
metadata: { items: 50_000 },
monotonicMs: 22,
outcome: 'success',
phase: 'store.m3u-import-dispatch',
phaseId: 'phase-1',
schemaVersion: 1,
} as const;
for (const invalid of [
[start],
[start, start, end],
[start, { ...end, phase: 'store.m3u-publish-channels' }],
[start, { ...end, metadata: { items: 10_000 } }],
[{ ...start, outcome: 'success' }, end],
[start, { ...end, outcome: undefined }],
[start, { ...end, epochMs: 99 }],
[start, { ...end, monotonicMs: 19 }],
[start, { ...end, metadata: { items: -1 } }],
]) {
assert.throws(
() => pairRendererPerformancePhaseEvents(invalid),
/renderer-performance-phase-events-invalid/
);
}
});
it('requires exactly one successful event for a named phase', () => {
const successful = {
durationMs: 2,
endedEpochMs: 102,
metadata: { items: 10_000 },
outcome: 'success',
phase: 'store.m3u-import-dispatch',
phaseId: 'phase-1',
startedEpochMs: 100,
} as const;
const failed = {
...successful,
outcome: 'error',
phaseId: 'phase-2',
} as const;
assert.throws(
() =>
requireRendererPerformancePhase(
[successful],
'store.m3u-publish-channels'
),
/renderer-performance-phase-missing:store.m3u-publish-channels/
);
assert.throws(
() =>
requireRendererPerformancePhase(
[successful, successful],
'store.m3u-import-dispatch'
),
/renderer-performance-phase-ambiguous:store.m3u-import-dispatch/
);
assert.throws(
() =>
requireRendererPerformancePhase(
[failed],
'store.m3u-import-dispatch'
),
/renderer-performance-phase-failed:store.m3u-import-dispatch/
);
});
});
@@ -0,0 +1,163 @@
export interface PairedRendererPerformancePhase {
readonly durationMs: number;
readonly endedEpochMs: number;
readonly metadata: Readonly<{ items: number }> | null;
readonly outcome: 'error' | 'success';
readonly phase: string;
readonly phaseId: string;
readonly startedEpochMs: number;
}
interface ParsedRendererPerformancePhaseEvent {
readonly boundary: 'end' | 'start';
readonly epochMs: number;
readonly metadata: Readonly<{ items: number }> | null;
readonly monotonicMs: number;
readonly outcome: 'error' | 'success' | null;
readonly phase: string;
readonly phaseId: string;
}
export function pairRendererPerformancePhaseEvents(
input: readonly unknown[]
): readonly PairedRendererPerformancePhase[] {
const grouped = new Map<string, ParsedRendererPerformancePhaseEvent[]>();
for (const value of input) {
const event = parseRendererPerformancePhaseEvent(value);
const events = grouped.get(event.phaseId) ?? [];
events.push(event);
if (events.length > 2) {
throw invalidRendererEvents();
}
grouped.set(event.phaseId, events);
}
const paired: PairedRendererPerformancePhase[] = [];
for (const events of grouped.values()) {
const start = events.find((event) => event.boundary === 'start');
const end = events.find((event) => event.boundary === 'end');
if (
events.length !== 2 ||
!start ||
!end ||
start.outcome !== null ||
end.outcome === null ||
start.phase !== end.phase ||
!sameMetadata(start.metadata, end.metadata) ||
end.epochMs < start.epochMs ||
end.monotonicMs < start.monotonicMs
) {
throw invalidRendererEvents();
}
paired.push(
Object.freeze({
durationMs: end.monotonicMs - start.monotonicMs,
endedEpochMs: end.epochMs,
metadata: end.metadata,
outcome: end.outcome,
phase: end.phase,
phaseId: end.phaseId,
startedEpochMs: start.epochMs,
})
);
}
paired.sort(
(left, right) =>
left.startedEpochMs - right.startedEpochMs ||
left.endedEpochMs - right.endedEpochMs ||
left.phaseId.localeCompare(right.phaseId)
);
return Object.freeze(paired);
}
export function requireRendererPerformancePhase(
phases: readonly PairedRendererPerformancePhase[],
phase: string
): PairedRendererPerformancePhase {
const matches = phases.filter((candidate) => candidate.phase === phase);
if (matches.length === 0) {
throw new Error(`renderer-performance-phase-missing:${phase}`);
}
if (matches.length !== 1 || !matches[0]) {
throw new Error(`renderer-performance-phase-ambiguous:${phase}`);
}
if (matches[0].outcome !== 'success') {
throw new Error(`renderer-performance-phase-failed:${phase}`);
}
return matches[0];
}
function parseRendererPerformancePhaseEvent(
value: unknown
): ParsedRendererPerformancePhaseEvent {
if (!isRecord(value) || value['schemaVersion'] !== 1) {
throw invalidRendererEvents();
}
const boundary = value['boundary'];
const epochMs = value['epochMs'];
const monotonicMs = value['monotonicMs'];
const outcome = value['outcome'];
const phase = value['phase'];
const phaseId = value['phaseId'];
if (
(boundary !== 'start' && boundary !== 'end') ||
!isFiniteNonNegativeNumber(epochMs) ||
!isFiniteNonNegativeNumber(monotonicMs) ||
typeof phase !== 'string' ||
phase.length === 0 ||
typeof phaseId !== 'string' ||
phaseId.length === 0 ||
(boundary === 'start' && outcome !== undefined) ||
(boundary === 'end' && outcome !== 'success' && outcome !== 'error')
) {
throw invalidRendererEvents();
}
return {
boundary,
epochMs,
metadata: parseMetadata(value['metadata']),
monotonicMs,
outcome: outcome === 'success' || outcome === 'error' ? outcome : null,
phase,
phaseId,
};
}
function parseMetadata(value: unknown): Readonly<{ items: number }> | null {
if (value === undefined) {
return null;
}
if (
!isRecord(value) ||
Object.keys(value).length !== 1 ||
!Number.isSafeInteger(value['items']) ||
Number(value['items']) < 0
) {
throw invalidRendererEvents();
}
return Object.freeze({ items: Number(value['items']) });
}
function sameMetadata(
left: Readonly<{ items: number }> | null,
right: Readonly<{ items: number }> | null
): boolean {
return left === null || right === null
? left === right
: left.items === right.items;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function isFiniteNonNegativeNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function invalidRendererEvents(): Error {
return new Error('renderer-performance-phase-events-invalid');
}
@@ -0,0 +1,309 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import test from 'node:test';
import * as rendererProcessRssCaptureModule from './renderer-process-rss-capture';
import {
captureRendererProcessRssSample,
createRendererProcessRssCapture,
type RendererProcessRssCapture,
type RendererProcessRssUnavailableReason,
} from './renderer-process-rss-capture';
const RENDERER_PID = 42;
const RENDERER_CREATION_TIME = 1_721_234_567_890;
test('exposes a self-contained factory for the Electron main-process injection boundary', () => {
const factory = (
rendererProcessRssCaptureModule as unknown as Record<string, unknown>
)['createRendererProcessRssCaptureApi'];
assert.equal(typeof factory, 'function');
const source = (factory as () => unknown).toString();
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => {
create(
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture;
};
assert.deepEqual(restoredFactory().create(RENDERER_PID, [metric()]), {
identity: {
creationTime: RENDERER_CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 1_263_616,
unavailableReason: null,
validSampleCount: 1,
});
});
interface MetricOverrides {
readonly creationTime?: unknown;
readonly pid?: unknown;
readonly type?: unknown;
readonly workingSetSize?: unknown;
}
function metric(overrides: MetricOverrides = {}): unknown {
return {
creationTime:
overrides.creationTime === undefined
? RENDERER_CREATION_TIME
: overrides.creationTime,
memory: {
workingSetSize:
overrides.workingSetSize === undefined
? 1_234
: overrides.workingSetSize,
},
pid: overrides.pid === undefined ? RENDERER_PID : overrides.pid,
type: overrides.type === undefined ? 'Tab' : overrides.type,
};
}
function assertUnavailable(
capture: RendererProcessRssCapture,
unavailableReason: RendererProcessRssUnavailableReason,
expected: {
readonly identity: RendererProcessRssCapture['identity'];
readonly missingSampleCount?: number;
readonly validSampleCount?: number;
}
): void {
assert.deepEqual(capture, {
identity: expected.identity,
missingSampleCount: expected.missingSampleCount ?? 0,
peakRssBytes: null,
unavailableReason,
validSampleCount: expected.validSampleCount ?? 0,
});
}
test('binds only the exact BrowserWindow renderer identity and converts KiB to bytes once', () => {
const capture = createRendererProcessRssCapture(RENDERER_PID, [
metric({
creationTime: RENDERER_CREATION_TIME + 1,
pid: 900,
type: 'Renderer',
workingSetSize: 9_999_999,
}),
metric(),
]);
assert.deepEqual(capture, {
identity: {
creationTime: RENDERER_CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 1_263_616,
unavailableReason: null,
validSampleCount: 1,
});
});
test('accepts only exact Tab or Renderer process types and tracks the peak without mutating prior state', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [
metric({ type: 'Tab', workingSetSize: 2_000 }),
]);
const lower = captureRendererProcessRssSample(initial, RENDERER_PID, [
metric({ type: 'Renderer', workingSetSize: 1_500 }),
metric({
pid: 777,
type: 'Renderer',
workingSetSize: 8_000_000,
}),
]);
assert.equal(initial.validSampleCount, 1);
assert.equal(initial.peakRssBytes, 2_048_000);
assert.deepEqual(lower, {
identity: {
creationTime: RENDERER_CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 2_048_000,
unavailableReason: null,
validSampleCount: 2,
});
const higher = captureRendererProcessRssSample(lower, RENDERER_PID, [
metric({ workingSetSize: 2_500 }),
]);
assert.equal(higher.peakRssBytes, 2_560_000);
assert.equal(higher.validSampleCount, 3);
});
test('fails closed when the BrowserWindow OS process id is invalid', () => {
for (const invalidPid of [0, -1, 1.5, Number.NaN, '42'] as const) {
assertUnavailable(
createRendererProcessRssCapture(invalidPid, [metric()]),
'renderer-os-pid-invalid',
{ identity: null }
);
}
});
test('fails closed when the initial target metric is missing or ambiguous', () => {
assertUnavailable(
createRendererProcessRssCapture(RENDERER_PID, [
metric({ pid: 99, workingSetSize: 99_999 }),
]),
'renderer-process-metric-missing-at-start',
{
identity: null,
missingSampleCount: 1,
}
);
assertUnavailable(
createRendererProcessRssCapture(RENDERER_PID, [
metric(),
metric({ type: 'Renderer', workingSetSize: 2_000 }),
]),
'renderer-process-metric-ambiguous',
{ identity: null }
);
});
test('fails closed for invalid initial type, creation time, or working set', () => {
const cases: ReadonlyArray<{
readonly metric: unknown;
readonly reason: RendererProcessRssUnavailableReason;
}> = [
{
metric: metric({ type: 'RendererHelper' }),
reason: 'renderer-process-metric-type-invalid',
},
{
metric: metric({ creationTime: '1721234567890' }),
reason: 'renderer-process-metric-creation-time-invalid',
},
{
metric: metric({ workingSetSize: Number.POSITIVE_INFINITY }),
reason: 'renderer-process-metric-working-set-invalid',
},
{
metric: metric({ workingSetSize: 0 }),
reason: 'renderer-process-metric-working-set-invalid',
},
];
for (const testCase of cases) {
assertUnavailable(
createRendererProcessRssCapture(RENDERER_PID, [testCase.metric]),
testCase.reason,
{ identity: null }
);
}
});
test('invalidates the capture when the bound renderer is missing and never recovers', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
const missing = captureRendererProcessRssSample(initial, RENDERER_PID, [
metric({ pid: 700, workingSetSize: 9_999_999 }),
]);
assertUnavailable(
missing,
'renderer-process-metric-missing-during-capture',
{
identity: initial.identity,
missingSampleCount: 1,
validSampleCount: 1,
}
);
const laterValid = captureRendererProcessRssSample(missing, RENDERER_PID, [
metric({ workingSetSize: 5_000 }),
]);
assert.deepEqual(laterValid, missing);
});
test('invalidates the capture when the BrowserWindow or process metric identity changes', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
assertUnavailable(
captureRendererProcessRssSample(initial, 43, [
metric({
creationTime: RENDERER_CREATION_TIME + 1,
pid: 43,
}),
]),
'renderer-process-identity-changed',
{
identity: initial.identity,
validSampleCount: 1,
}
);
assertUnavailable(
captureRendererProcessRssSample(initial, RENDERER_PID, [
metric({ creationTime: RENDERER_CREATION_TIME + 1 }),
]),
'renderer-process-identity-changed',
{
identity: initial.identity,
validSampleCount: 1,
}
);
});
test('fails closed for invalid or ambiguous metrics after binding', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
const cases: ReadonlyArray<{
readonly metrics: readonly unknown[];
readonly reason: RendererProcessRssUnavailableReason;
}> = [
{
metrics: [metric(), metric({ type: 'Renderer' })],
reason: 'renderer-process-metric-ambiguous',
},
{
metrics: [metric({ type: 'RendererHelper' })],
reason: 'renderer-process-metric-type-invalid',
},
{
metrics: [metric({ creationTime: Number.NaN })],
reason: 'renderer-process-metric-creation-time-invalid',
},
{
metrics: [metric({ workingSetSize: -1 })],
reason: 'renderer-process-metric-working-set-invalid',
},
];
for (const testCase of cases) {
assertUnavailable(
captureRendererProcessRssSample(
initial,
RENDERER_PID,
testCase.metrics
),
testCase.reason,
{
identity: initial.identity,
validSampleCount: 1,
}
);
}
});
test('keeps invalid renderer PIDs sticky after binding', () => {
const initial = createRendererProcessRssCapture(RENDERER_PID, [metric()]);
const invalid = captureRendererProcessRssSample(initial, 0, [metric()]);
assertUnavailable(invalid, 'renderer-os-pid-invalid', {
identity: initial.identity,
validSampleCount: 1,
});
assert.deepEqual(
captureRendererProcessRssSample(invalid, RENDERER_PID, [metric()]),
invalid
);
});
@@ -0,0 +1,267 @@
export type RendererProcessRssUnavailableReason =
| 'renderer-os-pid-invalid'
| 'renderer-process-identity-changed'
| 'renderer-process-metric-ambiguous'
| 'renderer-process-metric-creation-time-invalid'
| 'renderer-process-metric-missing-at-start'
| 'renderer-process-metric-missing-during-capture'
| 'renderer-process-metric-type-invalid'
| 'renderer-process-metric-working-set-invalid';
export interface RendererProcessIdentity {
readonly creationTime: number;
readonly pid: number;
}
export interface RendererProcessRssCapture {
readonly identity: RendererProcessIdentity | null;
readonly missingSampleCount: number;
readonly peakRssBytes: number | null;
readonly unavailableReason: RendererProcessRssUnavailableReason | null;
readonly validSampleCount: number;
}
export interface RendererProcessRssCaptureApi {
readonly create: (
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
) => RendererProcessRssCapture;
readonly sample: (
capture: RendererProcessRssCapture,
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
) => RendererProcessRssCapture;
}
export function createRendererProcessRssCaptureApi(): RendererProcessRssCaptureApi {
type JsonRecord = Record<string, unknown>;
const KIBIBYTE_BYTES = 1_024;
const MAX_WORKING_SET_KIB = Math.floor(
Number.MAX_SAFE_INTEGER / KIBIBYTE_BYTES
);
const helpers = {
readRecord(value: unknown): JsonRecord | null {
return typeof value === 'object' && value !== null
? (value as JsonRecord)
: null;
},
isValidOsPid(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
},
findPidCandidates(
processMetrics: readonly unknown[],
pid: number
): JsonRecord[] {
return processMetrics
.map((metric) => helpers.readRecord(metric))
.filter(
(metric): metric is JsonRecord =>
metric !== null && metric['pid'] === pid
);
},
readMetric(metric: JsonRecord):
| {
readonly creationTime: number;
readonly rssBytes: number;
}
| RendererProcessRssUnavailableReason {
if (metric['type'] !== 'Tab' && metric['type'] !== 'Renderer') {
return 'renderer-process-metric-type-invalid';
}
const creationTime = metric['creationTime'];
if (
typeof creationTime !== 'number' ||
!Number.isFinite(creationTime) ||
creationTime < 0
) {
return 'renderer-process-metric-creation-time-invalid';
}
const memory = helpers.readRecord(metric['memory']);
const workingSetSize = memory?.['workingSetSize'];
if (
!Number.isSafeInteger(workingSetSize) ||
Number(workingSetSize) <= 0 ||
Number(workingSetSize) > MAX_WORKING_SET_KIB
) {
return 'renderer-process-metric-working-set-invalid';
}
return {
creationTime,
rssBytes: Number(workingSetSize) * KIBIBYTE_BYTES,
};
},
unavailableCapture(
unavailableReason: RendererProcessRssUnavailableReason,
options: {
readonly identity?: RendererProcessIdentity | null;
readonly missingSampleCount?: number;
readonly validSampleCount?: number;
} = {}
): RendererProcessRssCapture {
return Object.freeze({
identity: options.identity ?? null,
missingSampleCount: options.missingSampleCount ?? 0,
peakRssBytes: null,
unavailableReason,
validSampleCount: options.validSampleCount ?? 0,
});
},
invalidateCapture(
capture: RendererProcessRssCapture,
unavailableReason: RendererProcessRssUnavailableReason,
missingSampleCount = capture.missingSampleCount
): RendererProcessRssCapture {
return helpers.unavailableCapture(unavailableReason, {
identity: capture.identity,
missingSampleCount,
validSampleCount: capture.validSampleCount,
});
},
create(
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
if (!helpers.isValidOsPid(webContentsOsPid)) {
return helpers.unavailableCapture('renderer-os-pid-invalid');
}
const candidates = helpers.findPidCandidates(
processMetrics,
webContentsOsPid
);
if (candidates.length === 0) {
return helpers.unavailableCapture(
'renderer-process-metric-missing-at-start',
{
missingSampleCount: 1,
}
);
}
if (candidates.length !== 1) {
return helpers.unavailableCapture(
'renderer-process-metric-ambiguous'
);
}
const metric = helpers.readMetric(candidates[0] as JsonRecord);
if (typeof metric === 'string') {
return helpers.unavailableCapture(metric);
}
return Object.freeze({
identity: Object.freeze({
creationTime: metric.creationTime,
pid: webContentsOsPid,
}),
missingSampleCount: 0,
peakRssBytes: metric.rssBytes,
unavailableReason: null,
validSampleCount: 1,
});
},
sample(
capture: RendererProcessRssCapture,
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
if (capture.unavailableReason !== null) {
return capture;
}
if (!helpers.isValidOsPid(webContentsOsPid)) {
return helpers.invalidateCapture(
capture,
'renderer-os-pid-invalid'
);
}
const identity = capture.identity;
if (identity === null || webContentsOsPid !== identity.pid) {
return helpers.invalidateCapture(
capture,
'renderer-process-identity-changed'
);
}
const candidates = helpers.findPidCandidates(
processMetrics,
webContentsOsPid
);
if (candidates.length === 0) {
return helpers.invalidateCapture(
capture,
'renderer-process-metric-missing-during-capture',
capture.missingSampleCount + 1
);
}
if (candidates.length !== 1) {
return helpers.invalidateCapture(
capture,
'renderer-process-metric-ambiguous'
);
}
const metric = helpers.readMetric(candidates[0] as JsonRecord);
if (typeof metric === 'string') {
return helpers.invalidateCapture(capture, metric);
}
if (metric.creationTime !== identity.creationTime) {
return helpers.invalidateCapture(
capture,
'renderer-process-identity-changed'
);
}
return Object.freeze({
identity,
missingSampleCount: capture.missingSampleCount,
peakRssBytes: Math.max(
capture.peakRssBytes ?? metric.rssBytes,
metric.rssBytes
),
unavailableReason: null,
validSampleCount: capture.validSampleCount + 1,
});
},
};
return Object.freeze({
create: helpers.create,
sample: helpers.sample,
});
}
const rendererProcessRssCaptureApi = createRendererProcessRssCaptureApi();
export function createRendererProcessRssCapture(
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
return rendererProcessRssCaptureApi.create(
webContentsOsPid,
processMetrics
);
}
export function captureRendererProcessRssSample(
capture: RendererProcessRssCapture,
webContentsOsPid: unknown,
processMetrics: readonly unknown[]
): RendererProcessRssCapture {
return rendererProcessRssCaptureApi.sample(
capture,
webContentsOsPid,
processMetrics
);
}
@@ -0,0 +1,172 @@
/* eslint-disable playwright/expect-expect -- This is a Node assertion-based performance contract test. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
interface TestIteration {
readonly kind: 'diagnostic' | 'measured' | 'warmup';
readonly main: {
readonly rendererWindow: {
readonly rss: {
readonly identity: {
readonly creationTime: number;
readonly pid: number;
} | null;
readonly missingSampleCount: number;
readonly peakRssBytes: number | null;
readonly unavailableReason: string | null;
readonly validSampleCount: number;
};
};
};
readonly runId: string;
}
interface RendererRssValidity {
readonly invalidMeasuredRuns: readonly {
readonly missingSampleCount: number;
readonly reason: string;
readonly runId: string;
readonly validSampleCount: number;
}[];
readonly measuredRunCount: number;
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
interface RendererRssValidityModule {
assessRendererRssValidity?: (
iterations: readonly TestIteration[]
) => RendererRssValidity;
}
const validityModulePromise = import(
new URL('./renderer-rss-validity.ts', import.meta.url).href
)
.then((module) => module as RendererRssValidityModule)
.catch(() => null);
function iteration(
runId: string,
kind: TestIteration['kind'],
input: Partial<TestIteration['main']['rendererWindow']['rss']> = {}
): TestIteration {
return {
kind,
main: {
rendererWindow: {
rss: {
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 4_096,
unavailableReason: null,
validSampleCount: 3,
...input,
},
},
},
runId,
};
}
test('validates only measured renderer RSS runs and preserves the exact summary shape', async () => {
const module = await validityModulePromise;
assert.ok(module, 'generic renderer RSS validity helper must exist');
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
const result = assess([
iteration('warmup-invalid', 'warmup', { identity: null }),
iteration('run-01', 'measured'),
iteration('diagnostic-invalid', 'diagnostic', {
unavailableReason: 'renderer-process-gone',
}),
]);
assert.deepEqual(result, {
invalidMeasuredRuns: [],
measuredRunCount: 1,
validForBenchmark: true,
validForComparison: true,
validMeasuredRunCount: 1,
});
assert.ok(Object.isFrozen(result));
assert.ok(Object.isFrozen(result.invalidMeasuredRuns));
});
test('fails every incomplete measured capture without losing its raw reason and counts', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
const result = assess([
iteration('run-no-identity', 'measured', { identity: null }),
iteration('run-missing-sample', 'measured', {
missingSampleCount: 1,
peakRssBytes: null,
unavailableReason: 'renderer-process-metric-missing-during-capture',
validSampleCount: 2,
}),
iteration('run-zero-peak', 'measured', { peakRssBytes: 0 }),
]);
assert.deepEqual(result, {
invalidMeasuredRuns: [
{
missingSampleCount: 0,
reason: 'renderer-rss-capture-invalid',
runId: 'run-no-identity',
validSampleCount: 3,
},
{
missingSampleCount: 1,
reason: 'renderer-process-metric-missing-during-capture',
runId: 'run-missing-sample',
validSampleCount: 2,
},
{
missingSampleCount: 0,
reason: 'renderer-rss-capture-invalid',
runId: 'run-zero-peak',
validSampleCount: 3,
},
],
measuredRunCount: 3,
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
});
test('requires at least one measured run', async () => {
const module = await validityModulePromise;
assert.ok(module);
const assess = module.assessRendererRssValidity;
assert.equal(typeof assess, 'function');
assert.deepEqual(assess([iteration('warmup-01', 'warmup')]), {
invalidMeasuredRuns: [],
measuredRunCount: 0,
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 0,
});
});
test('the cancellation report delegates renderer RSS validity to the generic helper', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation-report.ts', import.meta.url),
'utf8'
);
assert.match(
source,
/import \{ assessRendererRssValidity \} from '\.\/renderer-rss-validity';/
);
assert.doesNotMatch(source, /function assessRendererRssValidity\(/);
});
@@ -0,0 +1,72 @@
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
export interface RendererRssValidityIteration {
readonly kind: string;
readonly main: {
readonly rendererWindow: {
readonly rss: RendererProcessRssCapture;
};
};
readonly runId: string;
}
export interface InvalidRendererRssMeasuredRun {
readonly missingSampleCount: number;
readonly reason:
| Exclude<RendererProcessRssCapture['unavailableReason'], null>
| 'renderer-rss-capture-invalid';
readonly runId: string;
readonly validSampleCount: number;
}
export interface RendererRssValidity {
readonly invalidMeasuredRuns: readonly InvalidRendererRssMeasuredRun[];
readonly measuredRunCount: number;
readonly validForBenchmark: boolean;
readonly validForComparison: boolean;
readonly validMeasuredRunCount: number;
}
export function assessRendererRssValidity(
iterations: readonly RendererRssValidityIteration[]
): RendererRssValidity {
const measured = iterations.filter(
(iteration) => iteration.kind === 'measured'
);
const invalidMeasuredRuns: RendererRssValidity['invalidMeasuredRuns'][number][] =
[];
let validMeasuredRunCount = 0;
for (const iteration of measured) {
const rss = iteration.main.rendererWindow.rss;
if (
rss.identity !== null &&
Number.isSafeInteger(rss.peakRssBytes) &&
Number(rss.peakRssBytes) > 0 &&
rss.unavailableReason === null &&
rss.missingSampleCount === 0 &&
Number.isSafeInteger(rss.validSampleCount) &&
rss.validSampleCount > 0
) {
validMeasuredRunCount += 1;
continue;
}
invalidMeasuredRuns.push({
missingSampleCount: rss.missingSampleCount,
reason: rss.unavailableReason ?? 'renderer-rss-capture-invalid',
runId: iteration.runId,
validSampleCount: rss.validSampleCount,
});
}
const valid =
measured.length > 0 && validMeasuredRunCount === measured.length;
return Object.freeze({
invalidMeasuredRuns: Object.freeze(invalidMeasuredRuns),
measuredRunCount: measured.length,
validForBenchmark: valid,
validForComparison: valid,
validMeasuredRunCount,
});
}
@@ -0,0 +1,153 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
type CancellationBenchmarkManifest,
type CancellationIterationResult,
PERFORMANCE_ITERATION_KIND,
} from './m3u-refresh-cancellation-contract';
import { createCancellationBenchmarkSummary } from './m3u-refresh-cancellation-report';
import type { RendererProcessRssCapture } from './renderer-process-rss-capture';
function measuredIteration(
rss: RendererProcessRssCapture,
responsiveEvents: number,
unresponsiveEvents: number,
runId: string
): CancellationIterationResult {
return {
cancellationEffectObserved: true,
kind: PERFORMANCE_ITERATION_KIND.MEASURED,
main: {
eventLoopDelay: { maxMs: 0, p95Ms: 0, p99Ms: 0 },
eventLoopUtilization: null,
memory: {
peakHeapUsedBytes: 0,
peakRssBytes: 0,
postGcHeapUsedBytes: null,
postGcRssBytes: null,
},
rendererPeakRssBytes: 999_999_999,
rendererWindow: {
responsiveEvents,
rss,
unresponsiveEvents,
windowIdentity: {
browserWindowId: 7,
webContentsId: 11,
},
},
responsiveEvents: 100,
unresponsiveEvents: 100,
workers: [],
},
phases: {},
renderer: {
peakHeapUsedBytes: 0,
postGcHeapUsedBytes: null,
probe: {
frameGapsMs: [],
heartbeatDelaysMs: [],
longTasksMs: [],
},
},
runId,
} as unknown as CancellationIterationResult;
}
test('summary uses only exact target-window RSS and scoped responsiveness events', () => {
const summary = createCancellationBenchmarkSummary(
{} as CancellationBenchmarkManifest,
[
measuredIteration(
{
identity: {
creationTime: 1_721_234_567_890,
pid: 42,
},
missingSampleCount: 0,
peakRssBytes: 2_048,
unavailableReason: null,
validSampleCount: 3,
},
1,
2,
'run-01'
),
measuredIteration(
{
identity: {
creationTime: 1_721_234_567_890,
pid: 43,
},
missingSampleCount: 1,
peakRssBytes: null,
unavailableReason:
'renderer-process-metric-missing-during-capture',
validSampleCount: 2,
},
3,
4,
'run-02'
),
]
);
assert.deepEqual(summary.measured.rendererRssPeakBytes, {
count: 1,
max: 2_048,
mean: 2_048,
median: 2_048,
min: 2_048,
p95: 2_048,
p99: 2_048,
});
assert.equal(summary.measured.responsiveEvents, 4);
assert.equal(summary.measured.unresponsiveEvents, 6);
assert.deepEqual(
(
summary as unknown as {
readonly validity: {
readonly rendererRss: unknown;
};
}
).validity.rendererRss,
{
invalidMeasuredRuns: [
{
missingSampleCount: 1,
reason: 'renderer-process-metric-missing-during-capture',
runId: 'run-02',
validSampleCount: 2,
},
],
measuredRunCount: 2,
validForBenchmark: false,
validForComparison: false,
validMeasuredRunCount: 1,
}
);
});
test('benchmark preserves raw artifacts before rejecting incomplete renderer RSS', () => {
const source = readFileSync(
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
'utf8'
);
const summaryWrite = source.indexOf(
"writeJson(join(config.outputDirectory, 'summary.json'), summary)"
);
const validityGuard = source.indexOf(
'summary.validity.rendererRss.validForBenchmark'
);
assert.ok(summaryWrite >= 0, 'summary artifact write must exist');
assert.ok(validityGuard >= 0, 'renderer RSS validity guard must exist');
assert.ok(
summaryWrite < validityGuard,
'raw summary must be durable before the renderer RSS guard fails'
);
assert.match(source, /Renderer RSS capture is invalid/);
});
@@ -0,0 +1,323 @@
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import test from 'node:test';
import {
createRendererProcessRssCaptureApi,
type RendererProcessRssCapture,
type RendererProcessRssCaptureApi,
} from './renderer-process-rss-capture';
interface RendererWindowIdentity {
readonly browserWindowId: number;
readonly webContentsId: number;
}
interface RendererWindowRssSessionMetrics {
readonly responsiveEvents: number;
readonly rss: RendererProcessRssCapture;
readonly unresponsiveEvents: number;
readonly windowIdentity: RendererWindowIdentity;
}
interface RendererWindowRssSession {
detach(): void;
sample(): RendererWindowRssSessionMetrics;
snapshot(): RendererWindowRssSessionMetrics;
}
interface RendererWindowRssSessionFactory {
create(input: {
readonly browserWindowFromId: (
browserWindowId: number
) => FakeBrowserWindow | null;
readonly browserWindowId: number;
readonly getAppMetrics: () => readonly unknown[];
readonly rendererRssApi: RendererProcessRssCaptureApi;
readonly webContentsId: number;
}): RendererWindowRssSession;
}
interface RendererWindowRssSessionModule {
createRendererWindowRssSessionApi?: () => RendererWindowRssSessionFactory;
}
class FakeWebContents {
pidReads = 0;
throwOnPidRead = false;
constructor(
readonly id: number,
public pid: number
) {}
getOSProcessId(): number {
this.pidReads += 1;
if (this.throwOnPidRead) {
throw new Error('renderer unavailable');
}
return this.pid;
}
}
class FakeBrowserWindow extends EventEmitter {
constructor(
readonly id: number,
readonly webContents: FakeWebContents
) {
super();
}
}
const BROWSER_WINDOW_ID = 7;
const WEB_CONTENTS_ID = 70;
const RENDERER_PID = 700;
const CREATION_TIME = 1_721_234_567_890;
const sessionModulePromise = import(
new URL('./renderer-window-rss-session.ts', import.meta.url).href
)
.then((module) => module as RendererWindowRssSessionModule)
.catch(() => null);
function processMetric(
pid: number,
workingSetSize: number,
creationTime = CREATION_TIME
): unknown {
return {
creationTime,
memory: { workingSetSize },
pid,
type: 'Tab',
};
}
async function restoreSerializableFactory(): Promise<RendererWindowRssSessionFactory> {
const module = await sessionModulePromise;
assert.ok(module, 'renderer window RSS session module must exist');
const factory = module.createRendererWindowRssSessionApi;
assert.equal(typeof factory, 'function');
const source = factory.toString();
const restoredFactory = Function(
`"use strict"; return (${source});`
)() as () => RendererWindowRssSessionFactory;
return restoredFactory();
}
test('scopes renderer RSS and window events to the exact BrowserWindow', async () => {
const api = await restoreSerializableFactory();
const targetWebContents = new FakeWebContents(
WEB_CONTENTS_ID,
RENDERER_PID
);
const targetWindow = new FakeBrowserWindow(
BROWSER_WINDOW_ID,
targetWebContents
);
const decoyWebContents = new FakeWebContents(71, 701);
const decoyWindow = new FakeBrowserWindow(8, decoyWebContents);
let targetExternalEvents = 0;
let decoyExternalEvents = 0;
targetWindow.on('unresponsive', () => {
targetExternalEvents += 1;
});
decoyWindow.on('unresponsive', () => {
decoyExternalEvents += 1;
});
let appMetricReads = 0;
const targetWorkingSets = [1_000, 1_500];
const session = api.create({
browserWindowFromId: (id) =>
id === BROWSER_WINDOW_ID ? targetWindow : decoyWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => {
const targetWorkingSet = targetWorkingSets[
Math.min(appMetricReads, 1)
] as number;
appMetricReads += 1;
return [
processMetric(701, 9_999_999, CREATION_TIME + 1),
processMetric(RENDERER_PID, targetWorkingSet),
];
},
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
decoyWindow.emit('unresponsive');
decoyWindow.emit('responsive');
targetWindow.emit('unresponsive');
targetWindow.emit('responsive');
const sampled = session.sample();
assert.deepEqual(sampled, {
rss: {
identity: {
creationTime: CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: 1_536_000,
unavailableReason: null,
validSampleCount: 2,
},
responsiveEvents: 1,
unresponsiveEvents: 1,
windowIdentity: {
browserWindowId: BROWSER_WINDOW_ID,
webContentsId: WEB_CONTENTS_ID,
},
});
assert.equal(targetWebContents.pidReads, 2);
assert.equal(decoyWebContents.pidReads, 0);
assert.equal(appMetricReads, 2);
assert.equal(targetExternalEvents, 1);
assert.equal(decoyExternalEvents, 1);
assert.equal(targetWindow.listenerCount('unresponsive'), 2);
assert.equal(decoyWindow.listenerCount('unresponsive'), 1);
session.detach();
assert.equal(targetWindow.listenerCount('unresponsive'), 1);
assert.equal(targetWindow.listenerCount('responsive'), 0);
assert.equal(decoyWindow.listenerCount('unresponsive'), 1);
targetWindow.emit('unresponsive');
decoyWindow.emit('unresponsive');
assert.equal(session.snapshot().unresponsiveEvents, 1);
assert.equal(targetExternalEvents, 2);
assert.equal(decoyExternalEvents, 2);
});
test('rejects a missing or mismatched BrowserWindow before attaching listeners', async () => {
const api = await restoreSerializableFactory();
const targetWindow = new FakeBrowserWindow(
BROWSER_WINDOW_ID,
new FakeWebContents(WEB_CONTENTS_ID, RENDERER_PID)
);
let appMetricReads = 0;
const baseInput = {
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => {
appMetricReads += 1;
return [processMetric(RENDERER_PID, 1_000)];
},
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
};
assert.throws(
() =>
api.create({
...baseInput,
browserWindowFromId: () => null,
}),
/renderer-browser-window-missing/
);
assert.throws(
() =>
api.create({
...baseInput,
browserWindowFromId: () =>
new FakeBrowserWindow(
BROWSER_WINDOW_ID + 1,
targetWindow.webContents
),
}),
/renderer-browser-window-identity-mismatch/
);
assert.throws(
() =>
api.create({
...baseInput,
browserWindowFromId: () =>
new FakeBrowserWindow(
BROWSER_WINDOW_ID,
new FakeWebContents(WEB_CONTENTS_ID + 1, RENDERER_PID)
),
}),
/renderer-web-contents-identity-mismatch/
);
assert.equal(targetWindow.listenerCount('unresponsive'), 0);
assert.equal(targetWindow.listenerCount('responsive'), 0);
assert.equal(targetWindow.webContents.pidReads, 0);
assert.equal(appMetricReads, 0);
});
test('keeps PID and metric failures sticky while sampling the exact target every time', async () => {
const api = await restoreSerializableFactory();
const targetWebContents = new FakeWebContents(
WEB_CONTENTS_ID,
RENDERER_PID
);
const targetWindow = new FakeBrowserWindow(
BROWSER_WINDOW_ID,
targetWebContents
);
let appMetricReads = 0;
let metrics: readonly unknown[] = [processMetric(RENDERER_PID, 1_000)];
const session = api.create({
browserWindowFromId: () => targetWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => {
appMetricReads += 1;
return metrics;
},
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
targetWebContents.pid = RENDERER_PID + 1;
metrics = [processMetric(RENDERER_PID + 1, 5_000, CREATION_TIME + 1)];
const invalid = session.sample();
assert.deepEqual(invalid.rss, {
identity: {
creationTime: CREATION_TIME,
pid: RENDERER_PID,
},
missingSampleCount: 0,
peakRssBytes: null,
unavailableReason: 'renderer-process-identity-changed',
validSampleCount: 1,
});
targetWebContents.pid = RENDERER_PID;
metrics = [processMetric(RENDERER_PID, 10_000)];
assert.deepEqual(session.sample().rss, invalid.rss);
assert.equal(targetWebContents.pidReads, 3);
assert.equal(appMetricReads, 3);
const missingMetricSession = api.create({
browserWindowFromId: () => targetWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => [],
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
assert.deepEqual(missingMetricSession.snapshot().rss, {
identity: null,
missingSampleCount: 1,
peakRssBytes: null,
unavailableReason: 'renderer-process-metric-missing-at-start',
validSampleCount: 0,
});
targetWebContents.throwOnPidRead = true;
const invalidPidSession = api.create({
browserWindowFromId: () => targetWindow,
browserWindowId: BROWSER_WINDOW_ID,
getAppMetrics: () => [processMetric(RENDERER_PID, 1_000)],
rendererRssApi: createRendererProcessRssCaptureApi(),
webContentsId: WEB_CONTENTS_ID,
});
assert.equal(
invalidPidSession.snapshot().rss.unavailableReason,
'renderer-os-pid-invalid'
);
session.detach();
missingMetricSession.detach();
invalidPidSession.detach();
});
@@ -0,0 +1,174 @@
import type {
RendererProcessRssCapture,
RendererProcessRssCaptureApi,
} from './renderer-process-rss-capture';
export interface RendererWindowIdentity {
readonly browserWindowId: number;
readonly webContentsId: number;
}
export interface RendererWindowRssSessionMetrics {
readonly responsiveEvents: number;
readonly rss: RendererProcessRssCapture;
readonly unresponsiveEvents: number;
readonly windowIdentity: RendererWindowIdentity;
}
export interface RendererWindowWebContents {
readonly id: number;
getOSProcessId(): number;
}
export interface RendererWindow {
readonly id: number;
readonly webContents: RendererWindowWebContents;
off(event: 'responsive' | 'unresponsive', listener: () => void): unknown;
on(event: 'responsive' | 'unresponsive', listener: () => void): unknown;
}
export interface RendererWindowRssSession {
detach(): void;
sample(): RendererWindowRssSessionMetrics;
snapshot(): RendererWindowRssSessionMetrics;
}
export interface RendererWindowRssSessionCreateInput {
readonly browserWindowFromId: (
browserWindowId: number
) => RendererWindow | null;
readonly browserWindowId: number;
readonly getAppMetrics: () => readonly unknown[];
readonly rendererRssApi: RendererProcessRssCaptureApi;
readonly webContentsId: number;
}
export interface RendererWindowRssSessionApi {
create(
input: RendererWindowRssSessionCreateInput
): RendererWindowRssSession;
}
export function createRendererWindowRssSessionApi(): RendererWindowRssSessionApi {
const helpers = {
isValidIdentityId(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) > 0;
},
create(
input: RendererWindowRssSessionCreateInput
): RendererWindowRssSession {
if (!helpers.isValidIdentityId(input.browserWindowId)) {
throw new Error('renderer-browser-window-id-invalid');
}
if (!helpers.isValidIdentityId(input.webContentsId)) {
throw new Error('renderer-web-contents-id-invalid');
}
let browserWindow: RendererWindow | null = null;
try {
browserWindow = input.browserWindowFromId(
input.browserWindowId
);
} catch {
throw new Error('renderer-browser-window-missing');
}
if (browserWindow === null) {
throw new Error('renderer-browser-window-missing');
}
if (browserWindow.id !== input.browserWindowId) {
throw new Error('renderer-browser-window-identity-mismatch');
}
const webContents = browserWindow.webContents;
if (
webContents === null ||
typeof webContents !== 'object' ||
webContents.id !== input.webContentsId
) {
throw new Error('renderer-web-contents-identity-mismatch');
}
const windowIdentity = Object.freeze({
browserWindowId: input.browserWindowId,
webContentsId: input.webContentsId,
});
let rendererRss = input.rendererRssApi.create(
helpers.readOsProcessId(webContents),
helpers.readAppMetrics(input.getAppMetrics)
);
let responsiveEvents = 0;
let unresponsiveEvents = 0;
let detached = false;
const callbacks = {
onResponsive(): void {
if (!detached) {
responsiveEvents += 1;
}
},
onUnresponsive(): void {
if (!detached) {
unresponsiveEvents += 1;
}
},
snapshot(): RendererWindowRssSessionMetrics {
return Object.freeze({
responsiveEvents,
rss: rendererRss,
unresponsiveEvents,
windowIdentity,
});
},
};
browserWindow.on('unresponsive', callbacks.onUnresponsive);
try {
browserWindow.on('responsive', callbacks.onResponsive);
} catch (error) {
browserWindow.off('unresponsive', callbacks.onUnresponsive);
throw error;
}
return Object.freeze({
detach(): void {
if (detached) {
return;
}
detached = true;
browserWindow.off('unresponsive', callbacks.onUnresponsive);
browserWindow.off('responsive', callbacks.onResponsive);
},
sample(): RendererWindowRssSessionMetrics {
if (!detached) {
rendererRss = input.rendererRssApi.sample(
rendererRss,
helpers.readOsProcessId(webContents),
helpers.readAppMetrics(input.getAppMetrics)
);
}
return callbacks.snapshot();
},
snapshot: callbacks.snapshot,
});
},
readAppMetrics(
getAppMetrics: () => readonly unknown[]
): readonly unknown[] {
try {
const metrics = getAppMetrics();
return Array.isArray(metrics) ? metrics : [];
} catch {
return [];
}
},
readOsProcessId(webContents: RendererWindowWebContents): unknown {
try {
return webContents.getOSProcessId();
} catch {
return null;
}
},
};
return Object.freeze({ create: helpers.create });
}
@@ -0,0 +1,49 @@
/* eslint-disable playwright/expect-expect -- Node assertions define the loopback-only fixture server contract. */
import assert from 'node:assert/strict';
import test from 'node:test';
import { createSyntheticM3uFixture } from './synthetic-m3u';
import { startSyntheticM3uServer } from './synthetic-m3u-server';
test('serves and primes only the deterministic fixture on ephemeral IPv4 loopback', async () => {
const fixture = createSyntheticM3uFixture(10_000);
const server = await startSyntheticM3uServer(fixture);
try {
const url = new URL(server.resourceUrl);
assert.equal(url.protocol, 'http:');
assert.equal(url.hostname, '127.0.0.1');
assert.notEqual(url.port, '');
assert.equal(url.username, '');
assert.equal(url.password, '');
assert.equal(server.requestCount(), 0);
await server.prime();
assert.equal(server.requestCount(), 1);
const response = await fetch(server.resourceUrl);
assert.equal(response.status, 200);
assert.equal(
response.headers.get('content-length'),
String(fixture.bytes)
);
assert.equal(await response.text(), fixture.body);
assert.equal(server.requestCount(), 2);
const missing = await fetch(`${server.origin}/not-the-fixture.m3u`);
assert.equal(missing.status, 404);
assert.equal(server.requestCount(), 2);
} finally {
await server.close();
}
});
test('closes idempotently and rejects priming after close', async () => {
const server = await startSyntheticM3uServer(
createSyntheticM3uFixture(10_000)
);
await server.close();
await server.close();
await assert.rejects(server.prime(), /closed/i);
});
@@ -0,0 +1,114 @@
import { once } from 'node:events';
import { createServer, type Server } from 'node:http';
import type { SyntheticM3uFixture } from './synthetic-m3u';
export const SYNTHETIC_M3U_RESOURCE_PATH = '/synthetic-performance.m3u';
export interface SyntheticM3uServer {
readonly origin: string;
readonly resourceUrl: string;
close(): Promise<void>;
prime(): Promise<void>;
requestCount(): number;
}
export async function startSyntheticM3uServer(
fixture: SyntheticM3uFixture
): Promise<SyntheticM3uServer> {
let successfulRequestCount = 0;
const server = createServer((request, response) => {
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
if (
request.method !== 'GET' ||
requestUrl.pathname !== SYNTHETIC_M3U_RESOURCE_PATH ||
requestUrl.search.length > 0
) {
response.writeHead(404, {
'Content-Length': '0',
'Content-Type': 'text/plain; charset=utf-8',
});
response.end();
return;
}
successfulRequestCount += 1;
response.writeHead(200, {
'Cache-Control': 'no-store',
'Content-Length': String(fixture.bytes),
'Content-Type': 'application/vnd.apple.mpegurl; charset=utf-8',
});
response.end(fixture.body);
});
server.listen({
exclusive: true,
host: '127.0.0.1',
port: 0,
});
try {
await once(server, 'listening');
} catch (error: unknown) {
await closeServer(server).catch(() => undefined);
throw error;
}
const address = server.address();
if (
address === null ||
typeof address === 'string' ||
address.address !== '127.0.0.1' ||
!Number.isSafeInteger(address.port) ||
address.port <= 0
) {
await closeServer(server);
throw new Error(
'Synthetic M3U server did not bind exact IPv4 loopback'
);
}
const origin = `http://127.0.0.1:${address.port}`;
const resourceUrl = `${origin}${SYNTHETIC_M3U_RESOURCE_PATH}`;
let closePromise: Promise<void> | null = null;
return Object.freeze({
origin,
resourceUrl,
close: () => {
closePromise ??= closeServer(server);
return closePromise;
},
prime: async () => {
if (closePromise !== null) {
throw new Error('Synthetic M3U server is closed');
}
const response = await fetch(resourceUrl);
const body = await response.arrayBuffer();
if (
response.status !== 200 ||
Number(response.headers.get('content-length')) !==
fixture.bytes ||
body.byteLength !== fixture.bytes
) {
throw new Error(
'Synthetic M3U server priming response was invalid'
);
}
},
requestCount: () => successfulRequestCount,
});
}
async function closeServer(server: Server): Promise<void> {
if (!server.listening) {
return;
}
await new Promise<void>((resolvePromise, rejectPromise) => {
server.close((error) => {
if (error) {
rejectPromise(error);
return;
}
resolvePromise();
});
});
}
Loaded 100 of 317 files, more files were not shown because too many files have changed in this diff. Show more