#!/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
{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())