#!/usr/bin/env python3 """Controlled synthetic image-discovery / image-space experiments, not site audits. Install: python -m pip install -r requirements-tools.txt Run from package root: python lab/run_lab.py --experiment lcp --repeats 5 Then run the same command with --experiment cls. Fixtures are injected into a blank page with page.set_content. Images are embedded as data URLs. No network requests, server timing or transport benchmark is involved. """ from __future__ import annotations import argparse, base64, hashlib, io, json, platform, shutil, statistics from datetime import datetime, timezone from importlib.metadata import version from pathlib import Path from PIL import Image from playwright.sync_api import sync_playwright ROOT = Path(__file__).resolve().parents[1] DISCOVERY_DELAY_MS = 650 OBSERVATION_MS = 1100 OBSERVE = r""" window.__measure = {lcp: [], shifts: [], errors: []}; try { new PerformanceObserver(list => { for (const e of list.getEntries()) window.__measure.lcp.push({ startTime:e.startTime, renderTime:e.renderTime, loadTime:e.loadTime, size:e.size, element:e.element?.id || null, url:e.url }); }).observe({type:'largest-contentful-paint', buffered:true}); } catch(e) { window.__measure.errors.push(String(e)); } try { new PerformanceObserver(list => { for (const e of list.getEntries()) window.__measure.shifts.push({ startTime:e.startTime, value:e.value, hadRecentInput:e.hadRecentInput, sources:(e.sources||[]).map(s=>({id:s.node?.id||s.node?.tagName||null, previousRect:s.previousRect.toJSON(), currentRect:s.currentRect.toJSON()})) }); }).observe({type:'layout-shift', buffered:true}); } catch(e) { window.__measure.errors.push(String(e)); } """ def make_image() -> bytes: # Deterministic, non-photographic pixel pattern. Fixture data, not editorial artwork. w, h = 800, 400 pixels = bytearray() for y in range(h): for x in range(w): pixels.extend(((x*7+y*3)%256, (x*3+y*11)%256, ((x//12+y//12)%2)*150+40)) image = Image.frombytes('RGB', (w,h), bytes(pixels)) out = io.BytesIO(); image.save(out, format='PNG', optimize=False) return out.getvalue() def page_html(experiment: str, variant: str, image: bytes) -> str: attrs = ' width="800" height="400"' if experiment=='lcp' or variant=='reserved' else '' uri = 'data:image/png;base64,'+base64.b64encode(image).decode() delayed = experiment=='cls' or variant=='delayed' src = '' if delayed else f' src="{uri}"' script = '' if not delayed else f'' paras = ''.join(f'

Próbka tekstu do badania zmiany układu. Ta sama treść w każdym wariancie. Test lokalny nie opisuje strony produkcyjnej.

' for i in range(6)) return f''' Próba kontrolowana

Próba kontrolowana

Testowy wzór pikseli {paras}
{script}''' def cls_score(entries: list[dict]) -> float: """Largest session window: gap <1s, total span <5s; ignore recent input.""" maximum=current=0.0; first=last=None for e in entries: if e['hadRecentInput']: continue t=e['startTime'] if first is None or t-last>=1000 or t-first>=5000: current=0.0; first=t current += e['value']; last=t; maximum=max(maximum,current) return maximum def run(args): if args.repeats<1: raise ValueError('repeats must be positive') image=make_image() variants=['delayed','early'] if args.experiment=='lcp' else ['unreserved','reserved'] fixtures={v:page_html(args.experiment,v,image).encode() for v in variants} results=[]; env={} with sync_playwright() as p: executable=args.browser or shutil.which('chromium') or shutil.which('google-chrome') opts={'headless':True,'args':['--no-sandbox']} if executable:opts['executable_path']=executable browser=p.chromium.launch(**opts) env={'browser':browser.version,'playwright':version('playwright'),'pillow':version('Pillow'), 'python':platform.python_version(),'platform':platform.platform(),'headless':True, 'network':'none: data URL image and set_content on about:blank; requests blocked', 'cpu_throttling':'none','device_scale_factor':1,'cache':'new browser context for every run; no HTTP cache involved', 'viewport_not_real_device':True} for viewport in [{'width':390,'height':844},{'width':1280,'height':900}]: for repeat in range(args.repeats): # Interleaved AB / BA, no run discarded. for v in (variants if repeat%2==0 else variants[::-1]): ctx=browser.new_context(viewport=viewport,device_scale_factor=1,service_workers='block') page=ctx.new_page();errors=[];page.on('pageerror',lambda err:errors.append(str(err))) page.route('**/*', lambda route: route.abort()) page.set_content(fixtures[v].decode(),wait_until='domcontentloaded') page.wait_for_timeout(OBSERVATION_MS) page.wait_for_function('document.getElementById("hero").complete && document.getElementById("hero").naturalWidth === 800') d=page.evaluate('''() => ({...window.__measure, resources:performance.getEntriesByType('resource').filter(e=>e.name.endsWith('/hero.png')).map(e=>({ startTime:e.startTime,responseEnd:e.responseEnd,transferSize:e.transferSize, encodedBodySize:e.encodedBodySize,decodedBodySize:e.decodedBodySize})), image:{naturalWidth:document.getElementById('hero').naturalWidth, naturalHeight:document.getElementById('hero').naturalHeight}})''') lcp=d['lcp'][-1] if d['lcp'] else None if errors or d['errors'] or not lcp or False: raise RuntimeError(f'Invalid run: {errors}, {d}') if args.experiment=='lcp' and lcp['element']!='hero': raise RuntimeError('LCP candidate is not the test image') results.append({'experiment':args.experiment,'variant':v,'repeat':repeat+1, 'viewport':viewport,'lcp_ms':lcp['startTime'], 'cls':cls_score(d['shifts']),'observations':d,'page_errors':errors}) ctx.close() browser.close() summary=[] for width in [390,1280]: for v in variants: rows=[r for r in results if r['variant']==v and r['viewport']['width']==width] metrics={'lcp_ms':[r['lcp_ms'] for r in rows],'cls':[r['cls'] for r in rows]} summary.append({'variant':v,'viewport_width':width,'n':len(rows),**{ key:{'median':statistics.median(vals),'min':min(vals),'max':max(vals)} for key,vals in metrics.items()}}) out={'schema':'cometweb.synthetic-speed-lab/1','measured_at':datetime.now(timezone.utc).isoformat(), 'experiment':args.experiment,'environment':env, 'controlled_delays_ms':{'delayed_source_assignment':DISCOVERY_DELAY_MS}, 'observation_ms':OBSERVATION_MS,'image_sha256':hashlib.sha256(image).hexdigest(),'image_bytes':len(image), 'fixture_sha256':{k:hashlib.sha256(v).hexdigest() for k,v in fixtures.items()}, 'script_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), 'limitations':['Synthetic fixture, not cometweb.io or an audit-tool test','Five repeated lab runs per variant/viewport, not p75 field data', '650ms timer illustrates late image availability, not an expected production improvement', 'Data URL / set_content fixture: no real navigation, network discovery or compression measurement', 'No INP, Lighthouse score, RUM or CrUX measurement','No Safari/Firefox or real-device validation'], 'summary':summary,'runs':results} outdir=Path(args.output) if args.output else ROOT/'evidence';outdir.mkdir(parents=True,exist_ok=True) (outdir/f'{args.experiment}-lab-results.json').write_text(json.dumps(out,ensure_ascii=False,indent=2)+'\n') fixturesdir=ROOT/'lab'/'fixtures';fixturesdir.mkdir(exist_ok=True) for k,data in fixtures.items():(fixturesdir/f'{args.experiment}-{k.rsplit("/",1)[-1]}.html').write_bytes(data) (fixturesdir/'hero.png').write_bytes(image) print(json.dumps({'experiment':args.experiment,'summary':summary,'environment':env},ensure_ascii=False,indent=2)) if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--experiment',choices=['lcp','cls'],required=True) parser.add_argument('--repeats',type=int,default=5) parser.add_argument('--browser');parser.add_argument('--output') run(parser.parse_args())