* perf(electron): let hidden and minimized windows report themselves hidden The main window was created with backgroundThrottling: false (since #1123, without a stated reason). Electron then keeps document.visibilityState at "visible" for a hidden, minimized or fully covered window and never lets Chromium throttle it, so every renderer timer, rAF and CSS transition ran at full rate in the background, and the playback keep-awake gate, which releases the display for a minimized window, could never see one. Use Chromium's default. Audible media and picture-in-picture are exempt from background throttling in Chromium, and a local check confirmed HLS playback continues unchanged through more than six minutes minimized, audible and muted. Playwright's focus emulation pins every page it attaches to as visible, so the new window-visibility E2E launches the app without Playwright and drives it over raw CDP (electron-unautomated-launch.ts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): harden the unautomated Electron launch Review follow-ups for the window-visibility E2E: - Resolve `electron` in the main process through a require created from the `node:module` builtin instead of `process.mainModule`, which only exists when the app entry is CommonJS. - Bound teardown like closeElectronApplicationAndConfirmExit: SIGTERM, then SIGKILL, 5 s each, then fail instead of waiting forever. - Surface CDP protocol errors from Runtime.evaluate instead of returning undefined. - Wait until the window is actually shown before hiding it. The app shows its window on ready-to-show, and a hide() that lands earlier is undone by that show(); this was the first-attempt failure on the macOS shard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): check the playback display lock is released while hidden Review follow-ups for #1724: - Add an E2E that plays the webm fixture in the unautomated launch, records the main process's prevent-display-sleep blockers, and asserts the keep-awake lock is taken while visible, released when the window is hidden, and taken again when it is shown. The visibility tests alone would still pass if the renderer gate or the bridge stopped updating powerSaveBlocker. - Validate CDP replies before dispatch (CodeQL js/unvalidated-dynamic-method-call): only a numeric id with a pending settle function is called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): skip killing an Electron that already exited stopElectron now checks the recorded exit state before each signal and tolerates a kill that races the exit: on Windows taskkill throws for a PID that no longer exists. Startup cleanup can no longer replace the startup error that explains the failure; a cleanup failure there is logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stalker): keep the watchdog cadence while the window is hidden With background throttling on, Chromium wakes a hidden, silent page's timers at most once per minute after five minutes, so a portal that asks for get_events every 30 s would see pings at half its cadence while the window is minimized. Tick the watchdog from a dedicated worker (createBackgroundInterval, an inline blob worker allowed by the renderer CSP), whose timers are not subject to page throttling. It falls back to a page setInterval where no worker is available or the worker fails to load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(stalker): keep a stopped watchdog interval stopped A worker error that arrived after stop() started the page fallback interval, which nothing cleared, so pings continued for an inactive playlist. stop() now marks the interval stopped, detaches the worker handlers, and the fallback refuses to start afterwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
9.8 KiB
Electron debugging and tracing
Use this procedure for Electron/CDP tasks. Read the available electron skill
when automating the desktop app. These commands assume a bootstrapped worktree.
Start and attach
- Start the Electron development app with:
pnpm nx serve electron-backend - Package-script equivalent:
pnpm run serve:backend - Electron is configured to start with:
--remote-debugging-port=9222 - Connect Chrome DevTools Protocol tools to:
127.0.0.1:9222 - For Electron automation/debugging tasks, use the
electronskill - Do not auto-open DevTools during normal CDP automation. In development, DevTools is opt-in via
ELECTRON_OPEN_DEVTOOLS=1. - If DevTools is open,
agent-browser --cdp 9222 ...may attach to the DevTools page instead of the IPTVnator window. Symptoms:tab listshowsabout:blank, snapshots are empty, and screenshots are black. - If that happens, inspect targets with
curl http://127.0.0.1:9222/json/listand connect directly to the IPTVnator page websocket from thewebSocketDebuggerUrlfield. - The app holds a single-instance lock (
acquireSingleInstanceLockinapps/electron-backend/src/app/services/single-instance.ts): a second launch against the sameuserDataquits immediately and focuses the running window. To attach a second CDP-enabled instance to the same profile, setIPTVNATOR_ALLOW_MULTIPLE_INSTANCES=1— knowing that only one of the two processes will own the renderer's IndexedDB, so settings written by the other are lost. Before focusing, the guard forwards the second launch's argv toonSecondInstance, which is how a playlist path handed to an already-running app reaches the open queue.
Trace / Debug Startup
- Full startup tracing:
IPTVNATOR_TRACE_STARTUP=1 pnpm nx serve electron-backend
-
Narrower trace flags:
IPTVNATOR_TRACE_IPC=1traces rendererwindow.electron.*bridge callsIPTVNATOR_TRACE_DB=1traces DB worker requests and request-scoped DB eventsIPTVNATOR_TRACE_SQL=1traces SQLite statements in the main process and DB workerIPTVNATOR_TRACE_WINDOW=1traces BrowserWindow lifecycle and unresponsive eventsIPTVNATOR_TRACE_PLAYER=1traces external-player activity and bounded Embedded MPV runtime-probe stderrIPTVNATOR_TRACE_RENDERER_CONSOLE=1mirrors renderer console output into the Electron terminalIPTVNATOR_PERF_CAPTURE=1enables development/test-only, redacted M3U and Xtream preload IPC request/completion markers plus count-only M3U acquire/parse/normalize, Xtream main network/JSON-transform/success-response-ready/cancel-dispatch, and renderer store phase capture; renderer wrappers emit only while the benchmark installs its Symbol hook, benchmark tooling sets the flag explicitly, and production launches must leave it unset. It also keeps count-only main-process counters (startup phases, database worker SQL statements, and their values at main-window creation andready-to-show) and registers the main-onlyperformance:read-countersIPC handler, which the preload does not expose; see performance journeysIPTVNATOR_PERF_COUNT_SQL=1, together withIPTVNATOR_PERF_CAPTURE=1, also counts every SQL statement of the main-process and database-worker connections formain.sqlStatementsBeforeReadyToShow; it wraps each statement execution, including every row of a bulk insert, so only the launch journey sets it and the import benchmarks leave it unsetIPTVNATOR_PERF_WORKER_PROFILING=1enables development/test-only, request-scoped worker receive/work/response-post timestamps, thread CPU, event-loop utilization/delay, count-only playlist serialization/SQLite write/read/deserialization plus Xtream category/content/cache-clear/delete/in-source-search phase events, profiling-only worker cancel-receipt acknowledgements, valid-sample-counted isolate peak memory, and the database worker's idle-only one-shot post-GC heap probe; overlapping database requests are explicitly invalidated instead of misattributed, the performance benchmark sets the flag automatically, and production launches must leave it unsetIPTVNATOR_DISABLE_COMPILE_CACHE=1disables the main-process V8 compile cache;IPTVNATOR_COMPILE_CACHE_DIR=<dir>relocates it. The startup trace reports the outcome ascompile-cache
-
Settings, portal request/response, and trace payloads must use
@iptvnator/shared/loggingor the redacting portal logger before reachingconsole.*; never log raw credentials while debugging. -
If local Nx state gets weird before a rerun:
pnpm nx reset
agent-browser (global install)
agent-browser --cdp 9222 tab list
agent-browser --cdp 9222 tab 1
agent-browser --cdp 9222 snapshot -i -c -d 4
agent-browser --cdp 9222 screenshot /tmp/iptvnator-cdp.png
Fallback
npx --yes agent-browser --cdp 9222 tab list
DevTools Workaround
ELECTRON_OPEN_DEVTOOLS=1 pnpm nx serve electron-backend
curl http://127.0.0.1:9222/json/list
agent-browser connect ws://127.0.0.1:9222/devtools/page/<iptvnator-page-id>
agent-browser screenshot /tmp/iptvnator-cdp.png
E2E process cleanup and packaged diagnostics
Playwright launches Electron through cmd.exe on Windows. If graceful E2E
shutdown times out, terminate that process tree with taskkill /T /F; killing
only electronApp.process() can leave Electron holding the temporary profile
and make the next launch exit on the single-instance lock. The E2E workflow
checks this against a real Windows shell and child process. Test cleanup must
target only its own launch PID, never all Electron or IPTVnator processes.
Cleanup confirms process exit even if Playwright's close promise fails or
never settles. If termination fails, it retries and then throws instead of
allowing a relaunch against a potentially locked profile.
Capture the Node child-process handle immediately after launch and retain it
in a WeakMap keyed by the Electron application for cleanup and preparation
failures. This binding follows the application when restart callers replace
only the application/window fields on their fixture. After the last window
closes on Linux,
Electron can exit before cleanup begins; Playwright disposes its dispatcher,
so calling electronApp.process() at that point can throw even after a clean
exit. The retained handle still provides the actual exit code and signal.
Playwright sends Emulation.setFocusEmulationEnabled to every page it attaches
to, and Chromium implements focus emulation by raising the page's capturer
count. A window launched through launchElectronApp therefore always reports
document.visibilityState === 'visible' and is never background-throttled,
even when hidden or minimized. Tests of hidden-window behavior launch through
apps/electron-backend-e2e/src/electron-unautomated-launch.ts, which spawns
the app without Playwright and evaluates over raw CDP sockets
(window-visibility.e2e.ts is the example).
The Linux portable build uploads packaged-frame-copy-smoke reports and traces
even when the smoke fails. Check the paused-frame screenshot and trace before
classifying a zero rendered-frame signal as an infrastructure flake. A zero
signal also attaches frame-copy-diagnostics (the session's stream stats,
including mpv's drop counter, and its helper rings read from /dev/shm) and
mpv-log, the session's verbose mpv log. A latestSeq of 0 means the helper
never published; a latestFrameSignal of 0 means mpv rendered black; a
visible ring frame behind a black canvas points at the preload pump.
The Snap and Flatpak --embedded-mpv-runtime-probe launches in
build-and-make.yaml run under GNU timeout --verbose -k 10 300 with
ELECTRON_ENABLE_LOGGING=1, so a main process that throws before app ready
and blocks on Electron's uncaught-exception dialog under xvfb fails within
five minutes with a distinct ::error:: (exit 124, or 137 after timeout's
announced KILL escalation) and its stderr in the step log, instead of holding
the job until the 120-minute limit.
Main-process ownership
The process entry is apps/electron-backend/src/main.entry.ts (built to
dist/apps/electron-backend/main.js): it enables the V8 compile cache under
userData/v8-compile-cache and then requires the application bundle,
main.app.js, built from apps/electron-backend/src/main.ts. Before the window
loads, main.ts registers only what the renderer can call before its first paint
(window state, the close guard, playlist-open requests, request-header shims);
everything else, including the database, portal, EPG, download, player,
remote-control and update IPC, lives in
apps/electron-backend/src/app/startup/deferred-events.ts, built as the
deferred-events.js chunk and loaded inside the window's did-start-loading
listener. That import and its registrations finish within the same task, so no
renderer invoke can find a missing handler (app/startup/deferred-bootstrap.ts
holds the scheduler and its test); the startup trace reports it as
deferred-events:start and deferred-events:done. The cache is
disposable; IPTVNATOR_DISABLE_COMPILE_CACHE=1 turns it off and
IPTVNATOR_COMPILE_CACHE_DIR relocates it (E2E runs keep it inside
IPTVNATOR_E2E_DATA_DIR). nx-electron packages the backend through an
allowlist, so apps/electron-backend/project.json lists main.app.js and
deferred-events.js under the files option of the package and make
targets, and verify:package-layout fails when any of the entry files is missing
from app.asar. The preload is
apps/electron-backend/src/app/api/main.preload.ts, with handlers under
apps/electron-backend/src/app/events/. The window follows the saved startup mode
(normal/maximized/fullscreen); --fullscreen overrides a single launch. Use
workspace shell for window behavior,
DB worker for worker ownership and
Electron security for bridge boundaries.