"""Real headless Chromium sessions measuring page loads while the k6 users run. Each session is its own browser context (own cookies), signs in through the login form and cycles through everyday pages, recording the browser's own Navigation Timing and Largest Contentful Paint for every page view. Writes JSON lines to OUTPUT. Chromium keeps Secure cookies only on https or localhost, so it talks to http://localhost:RELAY_PORT, relayed to TARGET. """ import asyncio import json import os import random import time from playwright.async_api import async_playwright TARGET_HOST, TARGET_PORT = os.environ.get("TARGET", "serviceops.operations.svc.cluster.local:80").split(":") RELAY_PORT = int(os.environ.get("RELAY_PORT", "8080")) BROWSERS = int(os.environ.get("BROWSERS", "30")) FIRST_USER = int(os.environ.get("FIRST_USER", "1001")) DURATION = int(os.environ.get("DURATION_SECONDS", "600")) PASSWORD = os.environ["LOADTEST_PASSWORD"] OUTPUT = os.environ.get("OUTPUT", "/results/browser.jsonl") BASE = f"http://localhost:{RELAY_PORT}" PAGES = [("/", "dashboard"), ("/tickets/incident", "incident list"), ("TICKET", "ticket detail"), ("/task-board", "task board"), ("/ui/search?q=load", "search"), ("/cmdb", "cmdb")] METRICS_JS = """() => new Promise(resolve => { const nav = performance.getEntriesByType('navigation')[0]; let lcp = 0; try { new PerformanceObserver(list => { for (const e of list.getEntries()) lcp = Math.max(lcp, e.startTime); }) .observe({type: 'largest-contentful-paint', buffered: true}); } catch (e) {} setTimeout(() => resolve({ ttfb: nav ? nav.responseStart : null, dom_content_loaded: nav ? nav.domContentLoadedEventEnd : null, load: nav ? nav.loadEventEnd : null, lcp: lcp || null, }), 50); })""" async def relay(reader, writer): try: up_reader, up_writer = await asyncio.open_connection(TARGET_HOST, int(TARGET_PORT)) except OSError: writer.close() return async def pipe(src, dst): try: while data := await src.read(65536): dst.write(data) await dst.drain() except (ConnectionError, asyncio.CancelledError): pass finally: dst.close() await asyncio.gather(pipe(reader, up_writer), pipe(up_reader, writer)) async def session(browser, index, out, deadline): username = f"loadtest-{FIRST_USER + index:04d}" ip = f"203.0.113.{200 + index % 50}" context = await browser.new_context(extra_http_headers={"X-Forwarded-For": ip, "X-Forwarded-Proto": "https"}) page = await context.new_page() await asyncio.sleep(index * 2) # stagger sign-ins try: await page.goto(f"{BASE}/login", wait_until="load") await page.fill('input[name="username"]', username) await page.fill('input[name="password"]', PASSWORD) await asyncio.gather(page.wait_for_load_state("load"), page.press('input[name="password"]', "Enter")) ticket_links = [] while time.time() < deadline: path, label = random.choice(PAGES) if path == "TICKET": if not ticket_links: await page.goto(f"{BASE}/tickets/incident", wait_until="load") ticket_links = await page.eval_on_selector_all( 'a[href^="/ticket/"]', "els => els.map(e => e.getAttribute('href'))") if not ticket_links: continue path = random.choice(ticket_links) started = time.time() error = None try: response = await page.goto(f"{BASE}{path}", wait_until="load", timeout=60000) status = response.status if response else 0 metrics = await page.evaluate(METRICS_JS) except Exception as exc: # timeouts are a result, not a crash status, metrics, error = 0, {}, type(exc).__name__ out.write(json.dumps({"t": started, "user": username, "page": label, "status": status, "wall_ms": round((time.time() - started) * 1000, 1), "error": error, **metrics}) + "\n") out.flush() await asyncio.sleep(random.uniform(3, 8)) finally: await context.close() async def main(): server = await asyncio.start_server(relay, "127.0.0.1", RELAY_PORT) os.makedirs(os.path.dirname(OUTPUT), exist_ok=True) deadline = time.time() + DURATION async with server, async_playwright() as playwright: browser = await playwright.chromium.launch(args=["--disable-dev-shm-usage", "--no-sandbox"]) with open(OUTPUT, "w") as out: await asyncio.gather(*(session(browser, i, out, deadline) for i in range(BROWSERS))) await browser.close() if __name__ == "__main__": asyncio.run(main())