Back to the paper

Artifact

Sixteen Milliseconds: Frame-Budget Measurements of 53 WebGL 2 and Face-Tracking Camera Effects in a Browser Photobooth, and Three Rendering Defects

# Sixteen Milliseconds: artifact

Benchmark harness, raw measurements and analysis scripts for the technical
report *Sixteen Milliseconds: Frame-Budget Measurements of 53 WebGL 2 and
Face-Tracking Camera Effects in a Browser Photobooth, and Three Rendering
Defects* (Yousof Selim, November 2026).

The harness drives Photoshoot's **shipped** renderer, shaders, effect registry
and MediaPipe face tracker (release 1.5.1, commit `fcdb7bf`) unchanged. It only
observes them: it wraps `GLRenderer.renderLiveFrame` and
`FaceLandmarker.detectForVideo` with timers. The camera is Chrome's fake capture
device fed a Y4M clip made from Photoshoot's public sample photograph (two
fictional adults, AI-generated; provenance in the Photoshoot repository at
`web/landing/assets/README.md`). No personal footage was used.

## What was measured, and what was not

| Run | Where | Status |
|---|---|---|
| Headless, all 53 effects, 640×480, 1280×720, 1920×1080, 2 runs × 3 s each, plus isolated GPU draws, MediaPipe variants (1280×720), landmark case study (1280×720), desynchronized-hint check | `results/headless/` | Complete. Headless Chrome 156 on the real GPU (WebGL reports `ANGLE Metal Renderer: Apple M1 Pro`); its frame clock is 60 Hz, not tied to a display. |
| Headed (windowed, 120 Hz built-in display), 640×480, 3 runs planned | `results/headed/headed-640x480-console.log`, `headed-640x480-summary.csv` | **Partial.** Stopped after 81 of 159 measurements (run 0 complete for all 53 effects, run 1 for 28) because its windows kept interrupting other work on the machine. The run had not yet written raw JSON; the CSV is parsed from the per-measurement summary lines it printed. |
| Headed 1280×720 subsets (probe runs) | `results/headed/raw-1280x720-headed-subset.json`, `raw-1280x720-headed-uncapped-subset.json`, `desync-headed.json` | Normal + Bug Out with MediaPipe variants; an uncapped (`--disable-gpu-vsync --disable-frame-rate-limit`) subset of five effects; the desynchronized-hint check in a window. |
| Windows | none | **Not measured.** Windows statements in the report come from project records (commit `c2a7333`, the Windows CI probe). |
| The Electron desktop build, the effects-browser thumbnail renderer, person-segmentation backgrounds, real webcams | none | Not measured. |

## Machine and software

- MacBook Pro 14-inch (MacBookPro18,3), Apple M1 Pro (8-core CPU: 6 performance + 2 efficiency; 14-core GPU), 16 GB, built-in Liquid Retina XDR display (ProMotion, measured refresh interval 8.3 ms in the headed run).
- macOS 27.0.1 (26A434); on AC power, battery 95 % and charging; Low Power Mode off.
- Google Chrome 156.0.8078.74 (stable channel, launched by Playwright).
- Node.js 26.0.0; @playwright/test 1.62.1; esbuild 0.28.2; @mediapipe/tasks-vision 0.10.18 (all from Photoshoot's `package-lock.json`).
- Measurements taken 2 November 2026 (Asia/Kuala_Lumpur).

## Contents

| Path | What it is |
|---|---|
| `harness/src/bench.ts` | The in-page benchmark (live loop, isolated draws, MediaPipe variants, landmark case study, rVFC probe, desynchronized check). |
| `harness/src/faceWrap.ts`, `harness/src/appStub.ts` | Pass-through wrapper between the renderer and the face tracker (frozen landmarks; the pre-1.5.1 mapping for the case study), and a stub for the app singleton. |
| `harness/build.mjs` | Bundles the page around Photoshoot's own sources. |
| `harness/make-y4m.mjs` | Builds the fake-camera clips (30-frame, 1-second seamless loop, 30 fps). |
| `harness/run.mjs` | Launches Chrome with the fake camera and runs everything. |
| `harness/analyse.mjs` | Raw JSON → `per-effect.csv`, `summary.json`. |
| `harness/parse-headed.mjs` | Headed console log → CSV. |
| `harness/figures.mjs` | The five SVG figures. |
| `results/headless/raw-*-headless.json` | Raw per-frame timestamps, CPU render times, GPU timer-query results, MediaPipe call times, isolated draws, landmark case study. |
| `results/headless/per-effect.csv`, `summary.json` | One row per effect and resolution; group summaries. |
| `results/headless/desync-headless.json`, `headless.log` | Desynchronized-hint check; console output. |
| `results/headed/` | The partial headed run and probe subsets described above. |

The Y4M clips (about 150 MB) are not included; `make-y4m.mjs` rebuilds them.

## Reproduce

```sh
git clone https://github.com/yeegz/photoshoot && cd photoshoot && git checkout fcdb7bf
ELECTRON_SKIP_BINARY_DOWNLOAD=1 npm ci --ignore-scripts && node node_modules/esbuild/install.js
cd ../harness && ln -s ../photoshoot/node_modules node_modules
PHOTOSHOOT=../photoshoot node build.mjs
node make-y4m.mjs ../photoshoot/web/landing/assets/photobooth-sample.jpg ../media
# headless (what the report's main tables use):
node run.mjs ../media results/headless --headless --runs=2 --measure=3000 --variants-res=1280x720 --port=4517
# headed (opens visible Chrome windows; needs Google Chrome installed):
node run.mjs ../media results/headed --runs=3 --measure=4000 --port=4517
node analyse.mjs results/headless
node figures.mjs results/headless ../figures
```

`run.mjs` serves the page on 127.0.0.1 (port from `--port`, default 4517).

## Definitions

- **Frame interval**: difference between consecutive `requestAnimationFrame` timestamps, recorded by a second rAF loop alongside Photoshoot's own.
- **Dropped frame**: a frame interval above 1.5 × 1000/60 ≈ 25 ms, i.e. at least one 60 Hz frame skipped. (Intervals on the 60 Hz clock read 16.7–16.8 ms after timestamp quantisation, so a bare > 16.7 ms test would count on-time frames.)
- **CPU render**: wall time of one `renderLiveFrame()` call (texture upload from the camera video plus draw submission), not including GPU execution.
- **GPU timer**: `EXT_disjoint_timer_query_webgl2` `TIME_ELAPSED` around one live frame's upload + draw; disjoint results discarded.
- **Isolated draw**: one camera upload, then 40 draws of the effect into the 1:1 canvas with constant-alpha blending, closed by a 1-pixel `readPixels`; median of 5 repetitions. Blending matters on Apple GPUs: opaque full-screen draws are removed by hidden-surface removal before shading, so the opaque figure (also recorded) understates cost.
- **MediaPipe call**: wall time of one synchronous `detectForVideo` call on the page's main thread.