Lab run records

The SfM lab at /lab/ can send the measurement of one run to the site's collector, so that runs on hardware the maintainers do not own can be compared. Sending is a choice the visitor makes with the consent checkbox. Nothing is sent without it. With the checkbox selected before a run, the record is sent as soon as the run completes or fails; selected after a run, the Send record button sends it.

What a record holds

website/src/lib/run-record.ts defines the record (typecv-run-record-v2) and its validator. One record is one run, completed or failed:

Field Content
site The site commit (/version.json) and the runtime path
client Browser user agent, platform, language, core count, device memory class, screen size and DPR
gpu WebGPU adapter vendor, architecture, device, description, fallback flag, two buffer limits
input Kind (sample, prepared JSON, video), frame count, analysis and source resolution, duration
options The front end (corner or sift), the lab's numerical controls, the assumed focal ratio and the submission mode
result Registered views, landmarks, observations, mean reprojection error, matched pairs, features
timing Total wall time, device reuse (cold or warm), per-operation totals, per-frame times, the GPU kernels in first-use order
transfers Application upload and readback bytes
benchmark pyramid-8 and cold or warm when the standard benchmark produced the run, else null
reference Agreement with a sample's reference cameras (below), else null
failure Null for a completed run; else the error, the operation, the kernel that started last, the device-loss reason and the frames processed
note Up to 500 characters the visitor typed

A record holds no video, image, file name or network address. The page shows the exact JSON before it is sent, and offers the same JSON for download or copy when the visitor prefers to share it another way. The collector adds server.id, server.receivedAt, the Cloudflare country code and colo. The sender's address is hashed with the hour for rate limiting only and is not stored.

The standard benchmark

The lab's Standard benchmark button loads the eight-frame pyramid sample and runs it twice in one worker: a cold pass that includes shader compilation, then a warm pass on the retained device. Each pass produces one record with benchmark.pass set. With the consent box checked before the benchmark starts, both records are sent as they complete.

The COLMAP sample

The lab's two COLMAP south-building buttons load twelve photographs of the COLMAP demo dataset south-building (image credit Christopher Zach), resized to 640 × 480, with the dataset's refined intrinsics scaled to that size and the poses of its bundled sparse model (website/public/sample/south-building/, derived by scripts/prepare-lab-colmap-sample.py). The first button runs the whole GPU pipeline on the photographs with the lab's front end, scale-space SIFT (extractSiftGpuResident) and the tiled L2 matcher with a 0.8 ratio test, and the calibrated intrinsics held fixed (PHOTO_PROFILE in sfm.worker.ts). The second feeds the dataset's own SIFT keypoints and raw matches (colmap-sift.bin, the matched keypoints only) to the GPU mapper, so the mapper is measured apart from the lab's feature front end.

After each snapshot and at the end, the page similarity-aligns the registered cameras to the COLMAP poses on their common views (website/src/lib/pose-alignment.ts, the method of colmap_dataset.py compare) and shows the common view count, the median and maximum rotation difference in degrees, and the median and maximum camera center difference as a fraction of the reference trajectory's radius. The record's reference field carries the same numbers. The COLMAP model is a reference, not ground truth: both systems hold the same intrinsics fixed, and with the second button they also share the same features and matches.

A lost device

When the browser loses the WebGPU device during a run (on Windows, DXGI_ERROR_DEVICE_HUNG is the GPU watchdog resetting a device whose command stream ran too long), the page reports the operation and the kernel that started last, and builds a failure record with everything measured before the loss: the counts of the last map snapshot, the per-operation totals, the kernels in first-use order and the failure field. With consent given before the run, the record is sent at once. Open the lab with ?bounded=1 to run the same job with fenced submissions: the worker waits for the queue after each group of dispatches and verifies eight models per batch (ShadeDevice.open({ boundedSubmissions: true, maxModelsPerBatch: 8 })). The run is slower. The record's options.boundedSubmissions says which mode ran.

To run the suspect kernel on the machine that lost the device, with a checkout of the repository, Node 24 and npm ci && npm run build && npx playwright install chromium:

$env:TYPECV_HARDWARE_GPU = '1'
node --test tests/typeshade/south-building-triangulation-webgpu.test.mjs
node --test --test-concurrency=1 tests/typeshade/webgpu.test.mjs

The first file runs the triangulation job of the lost run (tests/fixtures/south-building-pair-triangulation.json) in three variants, each in its own browser, from the simplest to the full kernel. The variant that hangs or loses the device names the loop. The second file is the general WebGPU suite.

Collector

website/worker.mjs runs in front of the static assets for /api/* only (wrangler.jsonc: run_worker_first). Routes:

Route Behavior
POST /api/records/ Same-origin only; body at most 256 KiB; validated; 30 per address per hour; stored in KV
GET /api/records/ Authorization: Bearer <RECORDS_READ_TOKEN>; lists limit (at most 100) records with metadata and a cursor
GET /api/records/<id>/ Same token; returns one stored record

The host answers every path without a trailing slash with a 301 to the path with one, and a browser follows that redirect as a GET. The page therefore posts to /api/records/, and the Worker routes both spellings alike.

Records live in the KV namespace bound as RECORDS under record/<id>, with listing metadata (received time, GPU, input kind, frames, registered views, total ms, device reuse, benchmark name, and for a failed run the kernel or operation). website/worker.test.mjs tests the handler with an in-memory KV.

Setup

The deploy workflow (.github/workflows/deploy.yml) runs scripts/ensure-run-records.mjs before wrangler deploy. The script lists the account's KV namespaces with the deploy token, creates typecv-records when it is missing, writes the namespace id into wrangler.jsonc for that deploy, and stores the RECORDS_READ_TOKEN Actions secret as the Worker's secret when the repository has one. The deploy token (CLOUDFLARE_API_TOKEN) needs Workers KV Storage: Edit for this; without it the script removes the binding, warns, and the site deploys with the collector answering 503. scripts/setup-run-records.ps1 does the same steps by hand from the owner's machine. The read routes stay closed until a RECORDS_READ_TOKEN secret exists.

Reading the records

The Export lab run records workflow (.github/workflows/lab-records.yml, run by hand from the Actions tab or by an agent through the API) reads the KV namespace with the deploy's CLOUDFLARE_API_TOKEN (scripts/export-run-records.mjs, the Cloudflare REST API) and needs no read token. It prints one summary line per record to the job log, prints the failed runs in full (print: all prints every record), and uploads the JSON files as the lab-records artifact for 14 days. The inputs are limit (newest records, default 50) and print (failures, all or none).

With a RECORDS_READ_TOKEN secret, the collector's read routes answer too:

curl -H "Authorization: Bearer $TOKEN" 'https://cv.typeshade.dev/api/records/?limit=50'
curl -H "Authorization: Bearer $TOKEN" 'https://cv.typeshade.dev/api/records/<id>/'

Or on the Cloudflare dashboard: Workers & Pages → KV → typecv-site-typecv-records.