import { test, type Page } from "@playwright/test"; import { execFileSync } from "node:child_process"; import { mkdirSync, writeFileSync } from "node:fs"; import { retryConnect } from "./retryConnect"; /** * The speed baseline: numbers, not assertions — read by `scripts/audit_metrics.py --speed`, which * runs this as the `speed` project (never part of the e2e suite: its file name is not a `.spec`). * Each reading is the median of RUNS cold page loads (a fresh browser context, so no HTTP cache), * against the localdev stack and the seeded E2E account. It is a baseline for before/after within * one machine, not an absolute figure: the same measurement on a phone over the internet is slower. * * Not measured here: the in-app players (the E2E account owns no download, Plex item or watch * history to open), and any iOS reading (Playwright's WebKit decodes media through the host OS — * see playwright.config.ts) — the owner's phone gives that one by hand. */ const RUNS = 5; const OUT = "e2e/.speed/latest.json"; const CONTAINER = process.env.E2E_API_CONTAINER || "siftlode-api-1"; const median = (xs: number[]) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)] ?? NaN; /** JavaScript the page pulled in, uncompressed: the same on every stack (localdev serves it raw, * prod gzips it at the proxy), and what the browser has to parse. */ const scriptBytes = (page: Page) => page.evaluate(() => (performance.getEntriesByType("resource") as PerformanceResourceTiming[]) .filter((r) => r.initiatorType === "script" || r.name.endsWith(".js")) .reduce((sum, r) => sum + r.decodedBodySize, 0), ); /** Public share links on the local stack that could play: no password, not expired, asset ready. * The page itself is the judge of the last step (its file must be on THIS disk — a localdev DB * seeded from prod lists assets whose files never came along), so the caller probes them. Read * through the API container: the database keeps a link's token only hashed and encrypted, and only * the app holds the key to read it back (`links.recover`). */ const SHARE_TOKENS_PY = ` from sqlalchemy import select from app.db import SessionLocal from app.downloads import links from app.models import DownloadJob, DownloadLink, MediaAsset with SessionLocal() as db: for link in db.execute( select(DownloadLink) .join(DownloadJob, DownloadJob.id == DownloadLink.job_id) .join(MediaAsset, MediaAsset.id == DownloadJob.asset_id) .where(DownloadLink.password_hash.is_(None), MediaAsset.status == "ready") .order_by(DownloadLink.id) ).scalars(): token = None if links.is_expired(link) else links.recover(link) if token: print(token) `; function shareTokens(): string[] { // Not caught: an API container this cannot reach (no docker, a renamed container) is a failed // measurement, not "no playable share link on the local stack". const out = execFileSync("docker", ["exec", CONTAINER, "python", "-c", SHARE_TOKENS_PY], { encoding: "utf8", }); return out .split("\n") .map((t) => t.trim()) .filter(Boolean); } test("speed baseline", async ({ browser, page: page0 }) => { test.setTimeout(180_000); const result: Record = { measured_at: new Date().toISOString(), runs: RUNS, }; // Feed: navigation start → the first video card on screen. const feedMs: number[] = []; let feedJs = 0; for (let i = 0; i < RUNS; i++) { const ctx = await browser.newContext(); const page = await ctx.newPage(); await page.goto("/"); // Timed inside the page, every frame: an `expect` would read the clock on its own retry // schedule (100 ms … 1 s steps), not when the card appeared. const at = await page.waitForFunction( () => document.querySelector('[data-testid="video-card"]') && performance.now(), null, { polling: "raf", timeout: 30_000 }, ); feedMs.push((await at.jsonValue()) as number); feedJs = await scriptBytes(page); await ctx.close(); } result.feed_first_card_ms = Math.round(median(feedMs)); result.feed_js_raw_bytes = feedJs; // /watch: navigation start → the shared video playing (started muted by script, standing in for // the visitor's tap — the page has no autoplay). let token: string | null = null; for (const t of shareTokens()) { if ((await retryConnect(page0.request).get(`/api/public/watch/${t}`)).ok()) { token = t; break; } } if (token) { const watchMs: number[] = []; let watchJs = 0; for (let i = 0; i < RUNS; i++) { const ctx = await browser.newContext({ storageState: { cookies: [], origins: [] } }); const page = await ctx.newPage(); await page.goto(`/watch/${token}`); await page.waitForFunction(() => document.querySelector("video, audio"), null, { polling: "raf", timeout: 30_000, }); const video = page.locator("video, audio").first(); watchMs.push( await video.evaluate( (el: HTMLMediaElement) => new Promise((resolve, reject) => { el.muted = true; el.addEventListener("playing", () => resolve(performance.now()), { once: true }); el.play().catch(reject); }), ), ); watchJs = await scriptBytes(page); await ctx.close(); } result.watch_time_to_play_ms = Math.round(median(watchMs)); result.watch_js_raw_bytes = watchJs; } else { result.watch_time_to_play_ms = null; result.watch_js_raw_bytes = null; result.watch_note = "no playable share link on the local stack"; } mkdirSync("e2e/.speed", { recursive: true }); writeFileSync(OUT, JSON.stringify(result, null, 2) + "\n"); });