15 KiB
ADR 0005: Web browser qualification and reference performance passport
- Status: proposed; awaits independent security review of S00-006.
- Task / requirements: S00-006 / NFR-003, NFR-005; specification §27.
- Date: 2026-10-15.
- Decision owner: auto-orchestrator-710c6ba5d8ea (orchestrator).
- Reviewer: assigned security reviewer, not yet recorded.
Context and decision
Input: accepted ADR 0001, exact requirements, architecture and contract registry, S00-002 executor/review evidence. Output: one qualification passport with concrete hardware, browser policy, measurable budgets and permission/fallback procedures. This decision adds no shared interface, application implementation or dependency. Budgets are proposed engineering acceptance targets, not observed performance or a universal SLA. Approval selects reference devices; actual product qualification requires later browser runs on these devices and the production app.
Select six targets below. The machine-readable table and budget values are in
experiments/0005-browser-matrix/matrix.json; this ADR defines their meaning.
| ID | Reference hardware | OS family | Browser / syntax lower bound | CSS viewport / DPR | Modes |
|---|---|---|---|---|---|
| win-chrome | ThinkPad T14 Gen 2 Intel, i5-1135G7, 16 GiB | Windows 11 24H2 x64 | Chrome stable / 111 | 1366×768 / 1 | tab, installed |
| win-edge | Same ThinkPad | Same Windows | Edge stable / 111 | 1366×768 / 1 | tab, installed |
| linux-firefox | Same ThinkPad | Ubuntu 24.04 LTS x64, GNOME Wayland | Firefox stable / 114 | 1366×768 / 1 | tab |
| mac-safari | MacBook Air M1 2020, 8 GiB, 7-core GPU | macOS 15 | Safari stable / 18 | 1280×800 / 2 | tab, installed web app |
| android-chrome | Pixel 6a 128 GB, Tensor, 6 GiB | Android 15 | Chrome stable / 111 | 393×851 / 2.75 | tab, installed |
| ios-safari | iPhone 13 128 GB, A15, 4 GiB | iOS 18 | Safari stable / 18 | 390×844 / 3 | tab, Home Screen |
All measurements use 60 Hz. Desktop SSD, mobile internal storage; leave ≥20% free. Mobile portrait, standard display scaling/font size, browser UI expanded at navigation; record actual inner viewport after browser chrome. Desktop set window inner viewport explicitly. Also test mobile landscape, 320 CSS px width, wide 1440 CSS px, keyboard/IME, dark mode, 200% text/zoom and reduced motion as functional variants; they are not substitutes for physical performance devices.
Support policy: qualify the vendor-supported stable build available for each OS at release cutoff, then freeze its full build in the run record. Floors above come from ADR 0001, with Safari 18 selected for the named OS families; they do not authorize using obsolete vulnerable builds online or promise support for all versions since the floor. If an OS/browser is no longer vendor supported at the cutoff, revise this passport through review before qualification. Beta/dev, embedded webviews, other iOS browsers, private profiles and extensions are not qualified targets. Private/blocked storage must still fail honestly.
Exact patch versions are intentionally resolved at qualification, rather than
inventing future release pins now. Every run MUST supply all freeze_fields in
the JSON: full OS/browser build, acquisition source and installer hash (or Apple
OS build), app/asset/fixture hashes, profile reset recipe, tools, time, actual
viewport/DPR/refresh rate and network settings. Archive synthetic fixtures and
build assets with the record. A record saying only “latest” or “stable” is
invalid. Retest the frozen builds for comparison; qualify a new stable version
with a new record after security updates. No automatic acceptance carries over.
Physical devices may be rented/lab-owned; no claim of their availability here.
Emulation/Playwright WebKit alone cannot qualify Safari or mobile hardware.
PWA and capability policy
Tab mode is primary. Installation is optional progressive enhancement, not a native Desktop/Android client. No automatic install prompt is assumed. Firefox uses tab mode in this passport. iOS ordinary Safari tab has no promised Web Push; Home Screen mode with granted permission, valid subscription and configured provider is a separate required push scenario. No installation mode promises continuous sockets/media after hiding, locking, closing or terminating browser. Resume requires reconciliation; password re-entry/unlock follows its accepted policy. No offline send/delivery guarantee is introduced.
The JSON fallbacks table is normative for all six targets. Probe capabilities
and actual initialization outcomes, not user agent names. Missing required
crypto/secure transport blocks the protected operation. Missing optional media,
location, push, install or biometric capability disables only the dependent
feature and gives a reason. Synthetic fixtures never replace production crypto,
IndexedDB or transports. Storage failure and eviction follow accepted ADR 0004;
this cache budget does not revise persistence schemas or unlock policy.
Budgets and measurement protocol
| Metric | Desktop | Mobile | Gate |
|---|---|---|---|
| Cold startup p95 | 2500 ms | 4000 ms | navigation start → usable local shell |
| Warm startup p95 | 1200 ms | 2000 ms | same end point, cached assets/history |
| Action → painted result p95 | 100 ms | 150 ms | open chat, select, type, scroll |
| rAF gap p95 / maximum | 20 / 100 ms | 25 / 150 ms | foreground scripted scroll, 60 Hz |
| Peak app process RAM | 350 MiB | 250 MiB | 10-minute non-call session |
| Retained RAM growth | 30 MiB | 25 MiB | final settled minute minus first |
| Disposable cache ceiling | 128 MiB | 64 MiB | all accounts combined per origin |
Protocol for a later production qualification (not executed in this task):
- Build production assets with accepted ADR 0001 pins and lockfile. Serve via valid HTTPS with ordinary certificate validation; no dev server timings. Use no extensions, other tabs or competing foreground apps; power connected, power saver off, thermal nominal, ≥50% battery. Record OS/browser builds and tool versions before runs. Disable diagnostics during timing except required trace collection. Keep measurements with tracing separately labeled.
- Deterministic synthetic dataset: one active account, 100 conversations × 100
messages, text
message-00000throughmessage-09999, each padded with ASCIIxto 512 characters; 50 visible messages in selected chat. No real JIDs, credentials or conversations. Seed through future test fixture adapters, not production authentication bypass. Hash the exact serialized fixture and seed script; check local row count before each run. Cache workload: 1 MiB synthetic byte blobs until the ceiling plus two blobs. No audio/video running in the RAM workload; call/media owners must later define separate active-call budgets. - Controlled network: 10 Mbit/s download, 2 Mbit/s upload, 80 ms round-trip, zero intentional loss, no CPU throttle. Use a recorded external shaper for physical devices; record tool version/config and measured throughput/RTT to fixture host. WSS/MAM/server latency is measured separately and not part of usable-local-shell time. “Ready” means conversation list and 50 local messages painted, composer focusable and input accepted. Login/unlock/password human time is excluded and reported separately. Never mark connected/crypto ready merely to satisfy this local timing gate.
- Thirty cold runs: retain seeded IndexedDB, clear HTTP/disposable asset cache and unregister prior service worker, terminate browser, reopen at target URL. “Cold” here means application asset cold, not OS disk cold. Thirty warm runs: first prime assets/worker and fixture, close tab and reopen with same profile. Installed/Home Screen mode repeats both sequences and reports separately. Record navigation start and app-ready mark using Performance API; confirm visible ready UI in trace/screenshot. Report raw runs, failures and p95 via sorted nearest-rank ceil(0.95*n). Do not discard slow runs or mix modes.
- For each mode collect 100 actions each (open chat, select message, type one character, scroll one viewport), paced 500 ms apart; 60 seconds continuous deterministic scroll. Measure input timestamp → frame containing changed UI with browser performance trace. A double-rAF marker is only a proxy and must be labeled. Collect rAF timestamps in foreground; compute consecutive gaps, p95 and maximum. Record hidden/throttled runs separately, not as foreground passes. Any action failure fails function regardless of percentile.
- RAM: record baseline blank profile browser process tree, then sample app renderer/WebContent plus dedicated workers every second during 10 minutes of repeated chat switching/scrolling. Chromium task manager/process trace, Firefox about:processes plus OS RSS, Safari WebContent/worker OS RSS (macOS Activity Monitor; iOS Instruments on physical device). Use isolated browser profile so shared workers are attributable. Report sum RSS and baseline separately, not JS heap as total RAM. Peak must fit ceiling; last-minute median minus first-minute median must fit growth budget, with no forced GC. If attribution/tool access is unavailable, RAM gate is unverified, never 0.
- Cache: sum owned disposable asset/media payload bytes with adapter inventory; compare browser origin storage estimate as advisory, not exact cache size. Limit is min(reference ceiling, 10% reported quota) when estimate available; if unavailable, use reference ceiling and stop on actual quota error. Before admitting another blob evict disposable LRU entries or reject the write; never exceed budget deliberately. History, crypto/trust and pending unsynced work are excluded from disposable cache and must not be evicted by this gate. Confirm counts after reopen; inject QuotaExceededError and test actual device low-disk failure separately. No checkpoint advance on save failure. Browser may evict the entire origin: verify explicit recovery state after user clear.
Pass each target/mode independently; a fast desktop cannot offset mobile failure. These numbers are initial product targets chosen for responsiveness and bounded resources, not a benchmark derived from the small S00-002 counter. Later measured infeasibility requires reviewed budget/device change, not silently relaxing gates.
Contextual permissions and negative scenarios
For each target AND mode, run the following against a trusted HTTPS test origin with synthetic identities. Fresh profile has no prompts at startup. Trigger only via explicit relevant action: Start call/record (mic/camera), Send location (geolocation), Enable notifications (push), Enable unlock (only S00-007 policy). Storage persistence request must explain context rather than masquerade as login.
| State | Reproduction | Required observation |
|---|---|---|
| prompt / granted | Reset site and OS permissions; invoke each action and grant | Only requested capability activates; no unrelated permission prompt |
| denied | Deny browser prompt, then deny at OS level separately | Reason/actionable settings path; text still usable; no retry prompt loop |
| hardware missing | Disconnect USB camera/mic on desktop; use OS-disabled device on mobile | Capture fails honestly; camera-only failure may offer working audio |
| revoked during session | Start capture then revoke site/OS permission; unplug device; separately revoke location/notification permission | End/reconcile capture, stop watches; clear usable/push state on observed failure or resume; no stale success |
| unsupported | Disable API via test-only fixture boundary or use naturally absent mode | Each JSON fallback observed, protected operations remain blocked |
| ignored prompt / busy | Leave prompt unanswered then cancel action; separately hold camera in another app | Pending is cancellable; late grant is stopped/ignored; busy error not labeled denial |
| lifecycle / loss | Hide, lock, sleep, offline, close, reopen; remove subscription and clear origin | No continuous session promise; resync/unlock and explain lost local state |
A Permissions API query may be unsupported for a descriptor: catch failure and rely on real API result and track/error events; recheck on focus/resume. Revocation may arrive through track ended or subsequent failed operation, not a uniform permission event. Include denied→settings grant→explicit retry and account switch while prompt pending. Do not log coordinates, device labels, endpoints or secrets. Screenshots contain synthetic UI only. Browser/OS policy injection is labeled as fixture evidence; it does not prove physical device revoke or hardware absence.
Candidates, sources and constraints
Primary sources inspected 2026-10-15; these describe mechanisms, not results on our selected devices:
- Vite build compatibility: syntax floors and lack of automatic API polyfills. The matrix does not change ADR 0001 targets.
- Chrome release channels: stable qualification is chosen over beta/dev; exact run builds still required.
- WebKit iOS Web Push: Home Screen eligibility and direct user interaction for permission requests.
- Media Capture and Streams: permission/device errors and track lifecycle underpin media negative scenarios.
- Permissions: permission state and change handling; implementation support for individual descriptors remains conditional.
- Storage Standard: persistence/estimates are browser controlled; ceilings are application policy, not guaranteed disk quota.
- Web Authentication Level 3: authenticator operations do not establish a universal browser hardware-backed storage vault.
Rejected: Chromium-only coverage (misses independent engines/iOS behavior), “all modern browsers” without reference hardware, emulator-only performance, unconditional PWA push/background claims and frozen obsolete online binaries. A broad rolling channel without recorded full builds is also insufficient. Selected: bounded three-engine/six-target qualification with immutable run records.
No added libraries: dependency license review is not needed for this document and stdlib checker. Existing stack/library notices remain with their respective ADRs. No secrets, crypto implementation, network proxy, CORS/TLS exception or application module is introduced. S01 contracts and downstream performance/browser/provider implementation tasks retain ownership of exact adapter and telemetry interfaces.
Focused experiment and actual result
python3 experiments/0005-browser-matrix/check.py validates the passport's target
coverage, reproduction fields, budget consistency and fallback coverage; negative
mutations prove incomplete targets, missing fallback and relaxed guards fail.
This is document/fixture validation only. No physical reference device, browser
permission behavior, production startup/RAM/render/cache measurement or provider
integration was executed. Their procedures above are external qualification
checks, not passed results. S00-006 selects a passport; it does not certify release.
Acceptance/qualification failure, OS vendor support ending, new engine/API need,
or changed resource workload requires a separately reviewed passport revision.