SIGNAL·WATCHER DEV

--:--:--
EV·ENGINE
MONITOR starts a live feed and keeps running until pressed again
ONE-SHOT TEST fires once, reports to the log, then stops on its own
REPORT reads what has been collected, changes nothing

Speed — GPS native vs computed (haversine) idle

-- km/h native
coords.speed × 3.6 (Doppler, direct from GPS chip)
-- km/h computed
haversine over consecutive fixes (fallback when native = null)
-- m/s² Δv
acceleration derived from speed delta

Geolocation raw idle

latitude
longitude
accuracy
altitude
altitudeAccuracy
heading (null when stationary)
speed (raw m/s)
native km/h
haversine km/h
Δ native − haversine
timestamp
fix age
update rate
updates0
permission state

DeviceMotion idle

accel x / y / z
+gravity x / y / z
rotationRate α/β/γ
interval
events0

DeviceOrientation idle

alpha / beta / gamma
absolute
events0

Generic Sensor API idle

Accelerometer
LinearAcceleration
GravitySensor
Gyroscope
Magnetometer
AmbientLight
RelativeOrientation
AbsoluteOrientation

Network

online
connection type
effectiveType
downlink
downlinkMax
rtt
saveData
fetch latency test
WebSocket
WebRTC (RTCPeer)
WebTransport

GPU / graphics

WebGL 1
WebGL 2
WebGPU
GL vendor (unmasked)
GL renderer (unmasked)
GL vendor (masked)
GL renderer (masked)
masking shape
max texture
rAF framerate
OffscreenCanvas
WebGPU vendor
WebGPU architecture
WebGPU device
WebGPU description
adapter.limits maxTextureDimension2D (hardware)
device.limits (not queried)
adapter.isFallbackAdapter
The masked renderer strings are worthless as GPU identity — Chrome hardcodes "WebKit WebGL", Firefox sanitizes, and under privacy.resistFingerprinting the debug extension returns null — but the SHAPE of the masking identifies the engine and the privacy configuration, which is why both halves of the pair are shown. getContext('webgl') returning null is meaningful in its own right: Chrome 137+ desktop removed the automatic SwiftShader fallback, so null now means no usable GPU rather than a slow software path. Adapter limits are the hardware's; a bare requestDevice() would report spec defaults (8192) instead, so no device is requested here and none is reported. isFallbackAdapter is documented as always false on users' devices — probed for completeness, non-functional as a signal. requestAdapterInfo() is never called: it was removed in Chrome 135 and its absence serves as a version rung instead.

Performance & jank idle

rAF avg (rolling 120)
display period (median of 120)
frame gap p95 / p99
gap histogram (×period)
worst frame gap
dropped frames (round(Δ/period)−1)0
visibilityState while measuring
LoAF count / worst
long tasks count / worst
CLS (layout-shift)
INP-ish worst event
JS heap used
JS heap total
JS heap limit
measureUserAgentSpecificMemory
UA-specific memory needs crossOriginIsolated and a button press (it is async and gated)
The display period is measured, not assumed: 90 Hz and 120 Hz panels exist and scoring them against a hardcoded 16.67 ms invents jank that is not there. Percentiles are reported rather than a mean because a 250 ms stall inside an otherwise good second still averages ~55 fps and looks healthy. Read this alongside visibilityState — a backgrounded tab is throttled to about 1 Hz and would otherwise read as catastrophic jank.

Compute Pressure idle

PressureObserver
1 · constructor present
2 · observe() accepted
3 · sample actually received
knownSources
cpu state
last change
samples0
origin trial in M125, desktop only — absence here is expected on Android and in-car. The three stages are reported separately because presence is not function: the constructor is undefined in headless Chrome and a function in headed Chrome, and even headed it delivered no sample in 12 s on macOS. A green stage 1 with a silent stage 3 is a real and common outcome.

Audio output routing idle

AudioContext.setSinkId (M110)
HTMLMediaElement.setSinkId
selectAudioOutput
audiooutput device count
default sink id
ctx sampleRate
ctx state
ctx baseLatency
ctx outputLatency
context values fill in after "Audio route probe" — no AudioContext is built on load, so nothing steals audio focus

Audio glitch — playback statistics idle

variant found
read attempt
underrun events
underrun duration
total duration
dropout ratio
latency min / avg / max
Δ since previous read
counters observed moving
raw keys on the object
Two names exist for the same quantities and they differ by 1000×. Legacy playoutStats (fallbackFramesDuration/Events, totalFramesDuration) is in MILLISECONDS; shipped playbackStats (underrunDuration/Events, totalDuration) is in SECONDS. Both are probed and normalised here — durations to seconds, latencies to ms. Counters are cumulative, so this card diffs successive reads rather than reporting an instant. Availability is contested upstream (chromestatus lists it behind a flag at milestone 146; an independent test found it live with no flags on 150). This page makes no support claim: it reports only which name exists here, whether reading threw, and whether the numbers moved.

Audio clock drift idle

context state
rate this window
worst rate seen
windows scored
consecutive bad
verdict
constant offset (≈outputLatency)
currentFrame gap detector
Portable fallback for browsers with no playback statistics: the RATE at which currentTime advances against the wall clock, measured over a sliding window. Healthy sits at 0.999–1.003; a starved graph reads ~0.33. The absolute offset is NOT drift — it is a constant ≈31 ms of outputLatency, shown separately and never scored. The first window is discarded (startup reads ~0.94) and two consecutive bad windows are required before starvation is called.

Chromium milestone ladder — feature-bracketed probing…

outputLatency → ≥102
AudioContext.setSinkId → ≥110
wgsl packed_4x8_integer_dot_product → ≥123
wgsl readonly_and_readwrite_storage_textures → ≥124
adapter.info sync attribute → ≥127
AudioContext.onerror → ≥128
requestAdapterInfo removed → ≥135
isFallbackAdapter moved onto info → ≥140
inferred lower bound
UA-parsed version
features vs UA
Brackets the engine from what it can DO, then cross-checks the UA string. A disagreement is itself the finding: it means the UA is reduced, frozen or spoofed. The bound is a floor only — no upper limit is claimed, because a rung being absent can also mean the feature was removed.

Engine discriminators

AudioContext.setSinkId (Chromium 110+ only)
selectAudioOutput (Firefox 116+ only)
getAutoplayPolicy (Firefox only)
navigator.audioSession (Safari 16.4+ only)
HTMLMediaElement.setSinkId (not Chrome Android)
speaker-selection permission query
inferred engine
Tesla in-car browser (UA)
QtCarBrowser (pre-2018 MCU1)
TeslaMusic webview (separate runtime)
Each row isolates one engine. AudioContext.setSinkId present with HTMLMediaElement.setSinkId absent identifies Chrome Android with no UA involved. On iOS every browser is WebKit, so audioSession present alongside a Chrome UA means iOS Chrome, not Blink. permissions.query({name:'speaker-selection'}) throws a TypeError in Chromium instead of returning a status — throw-vs-return is recorded, because that difference is itself a discriminator.

Autoplay policy idle

getAutoplayPolicy (Firefox only)
AudioContext.state at construction
gesture had occurred by then
observed at
Because getAutoplayPolicy is Firefox-only, the universal oracle is the state of a freshly constructed AudioContext: running = autoplay allowed, suspended = blocked. It is captured on the first context this page builds (never on load) and timestamped, because in Chromium it is not stable per-origin — the Media Engagement Index can flip it between visits.

Display & user preferences

screen.isExtended
colorDepth / pixelDepth
devicePixelRatio
orientation.type
prefers-reduced-motion
prefers-color-scheme
prefers-contrast
prefers-reduced-data
prefers-reduced-transparency
forced-colors
dynamic-range

Audio engine idle

AudioContext
state
sampleRate
baseLatency
outputLatency
destination.maxChannelCount (addressable channels)
AudioWorklet
mp3 / aac / ogg / wav
decodingInfo · supported (audio)
decodingInfo · powerEfficient / smooth
MediaSession API
speechSynthesis voices
"addressable channels" is what maxChannelCount reports — a channel budget, not a speaker count. No web API exposes the real speaker layout, so a value of 6 does not mean six speakers exist. On decodingInfo, only the supported column carries information for audio-only configurations: every supported audio codec reports powerEfficient: true in Chromium, and smooth is defined against video frame pacing. Both are shown but neither is meaningful here.

Media / video

h264 / vp9 / av1 (mp4|webm)
HLS native
MediaSource (MSE)
EME / Widevine
getUserMedia
audioinput devices
videoinput devices
audiooutput devices
Picture-in-Picture

Input / touch idle

maxTouchPoints
pointer type
active touches0
last pointer
pressure
keyboard events0
press a key on the car keyboard…
multi-touch test area — put fingers here

Gamepad idle

API present
connected pads
pad 0 id
axes
buttons pressed

Hardware bridges (OBD path)

Web Bluetooth
Web Serial
Web USB
Web HID
Web NFC
Web MIDI
any "present" here = potential in-browser route to a CAN/OBD dongle without phone

Storage / persistence

localStorage
sessionStorage
IndexedDB
Cache API
Service Worker
quota estimate
usage
storage.persisted()
cookies enabled
persisted survives reload?
usageDetails (per-bucket)
indexedDB.databases()
usageDetails is a Chromium extension to the standard estimate and breaks the total down per storage system — its presence alone is an engine signal, and the bucket NAMES it returns move between Chromium versions.

Environment

secure context
protocol
Chromium version
platform
UA-CH platform
UA-CH formFactors
UA-CH bitness / wow64
UA-CH architecture
userActivation.isActive
userActivation.hasBeenActive
crossOriginIsolated
languages
CPU cores
deviceMemory
screen
viewport
DPR / colorDepth
orientation
battery
webdriver flag
timezone
visibility
time hidden (suspend test)0 s

Permissions query

querying…

WebGL parameter dump

extension count (WebGL1)
extension count (WebGL2)
MAX_TEXTURE_SIZE
MAX_VIEWPORT_DIMS
MAX_RENDERBUFFER_SIZE
MAX_CUBE_MAP_TEXTURE_SIZE
MAX_VERTEX_ATTRIBS
MAX_VARYING_VECTORS
MAX_VERTEX_UNIFORM_VECTORS
MAX_FRAGMENT_UNIFORM_VECTORS
MAX_TEXTURE_IMAGE_UNITS
MAX_COMBINED_TEXTURE_IMAGE_UNITS
ALIASED_LINE_WIDTH_RANGE
ALIASED_POINT_SIZE_RANGE
MAX_ANISOTROPY (EXT)
vertex HIGH_FLOAT (min/max/prec)
vertex MEDIUM_FLOAT
fragment HIGH_FLOAT
fragment MEDIUM_FLOAT
WebGL2 MAX_3D_TEXTURE_SIZE
WebGL2 MAX_ARRAY_TEXTURE_LAYERS
WebGL2 MAX_DRAW_BUFFERS
WebGL2 MAX_SAMPLES
WebGL2 MAX_UNIFORM_BUFFER_BINDINGS
extension list (click)
The extension COUNT is more discriminating than any single limit: it moves with driver, GPU family, ANGLE backend and Chromium version together. Precision formats separate mobile GPUs that lack true high-float in fragment shaders from desktop ones that do.

WebGPU adapter detail

adapter.features count
wgslLanguageFeatures count
adapter.info fields
adapter.isFallbackAdapter
adapter.limits reported
device.limits
features · wgsl · limits (click)
Every value here is the ADAPTER's — the hardware ceiling. No requestDevice() is called anywhere on this page: a device reports spec defaults rather than hardware, and on Chrome 140+ requesting one consumes the adapter. GPUSupportedLimits exposes its values on the prototype, not as own properties, so the limits are read from a fixed name list rather than enumerated — a name missing from that list is a limit this page does not ask for, not a limit the adapter lacks.

Fingerprint hashes

canvas 2D hash
canvas 2D payload length
WebGL render hash
AudioContext sum
AudioContext hash
audio render status
combined
All three are promptless and silent — the audio one renders through an OfflineAudioContext, which produces no sound and takes no audio focus. FNV-1a, inline, no dependencies. A hash that changes between two reloads on the same device is the finding: it means the browser is randomising the surface (Brave's farbling, privacy.resistFingerprinting), which is far more informative than the hash itself.

Font enumeration (promptless)

fonts probed
detected present
Windows-family hits
macOS-family hits
Linux-family hits
Android-family hits
web-safe hits
inferred OS from fonts
document.fonts.check() behaviour
detected font list (click)
queryLocalFonts() is never called — it prompts. Detection is the measureText width method: a family is present if its rendered width differs from all three generic fallbacks. document.fonts.check() is probed too but is NOT used as the detector: in Chromium it answers true for families that do not exist, so what it reports is its own behaviour rather than the font set. Whether it discriminates is recorded as its own row. The SET of families is one of the strongest OS discriminators reachable without a permission.

Intl & locale surface

resolved locale
timeZone
calendar
numberingSystem
hourCycle / hour12
NumberFormat resolved
navigator.languages
supportedValuesOf timeZone
supportedValuesOf currency
supportedValuesOf calendar
UTC offset now
UTC offset January
UTC offset July
DST observed
Intl.Segmenter
Intl.DurationFormat
Intl.ListFormat
Intl.RelativeTimeFormat
The January/July offset pair reveals the DST RULE rather than the current offset, which identifies the timezone far more tightly than the zone name alone — and disagrees loudly when the zone name is spoofed but the underlying ICU data is not. The supportedValuesOf counts track the bundled ICU version, which moves with the browser build.

Crypto idle

crypto.subtle
crypto.randomUUID
crypto.getRandomValues
AES-GCM generateKey
RSA-OAEP generateKey
ECDSA P-256 generateKey
Ed25519 generateKey
X25519 generateKey
Presence of crypto.subtle says nothing about which algorithms work, so the keys are actually generated — behind a button, because RSA-OAEP alone can cost hundreds of milliseconds. Ed25519 and X25519 are the sharp rungs: they shipped unflagged in Chromium 137, long after the rest, so a working Ed25519 dates the build on its own.

WebRTC detail idle

RTCPeerConnection
gathering state
candidates seen
host candidates
srflx candidates
mDNS (.local) obfuscation
mDNS verdict
audio codecs (count)
video codecs (count)
header extensions
codec lists (click)
No STUN server is configured, so this works fully offline and reveals only host candidates — which is the point. Whether those hosts arrive as raw private IPs or as .local mDNS hostnames is a per-engine, per-version policy and a strong discriminator. getCapabilities() needs no peer connection and no permission at all; the codec list and its clock rates vary by platform build far more than the codec matrix above suggests.

Media device breakdown

enumerateDevices
total devices
audioinput
audiooutput
videoinput
labels populated
distinct groupIds
supported constraints (count)
constraint dictionary (click)
enumerateDevices() itself never prompts. Empty labels with a non-zero device count is the un-granted state; populated labels mean camera or microphone permission has already been given to this origin — so the label column is read as a permission probe, not as device names. getSupportedConstraints() varies widely across platform builds and is one of the cheapest breadth wins available.

Speech synthesis voices

speechSynthesis
voice count
localService
remote (network)
distinct langs
default voice
voiceschanged fired
voice list (click)
Zero voices is a real and informative result, not a failure — it is what an embedded or in-car build with no TTS engine installed reports, and it means every speech feature in the app above is dead on that device. The list populates asynchronously in most engines, so voiceschanged is listened for and the card updates itself; a count that stays at zero after the event has fired is a much stronger claim than one read once on load.

Legacy & misc discriminators

navigator.plugins length
plugin names
navigator.mimeTypes length
pdfViewerEnabled
doNotTrack
globalPrivacyControl
error stack first line
stack shape → engine
native toString shape
0.1 + 0.2
Math.tan(-1e300)
Math.pow(2,1023)
Math.sinh(1) / expm1(1)
permissions policy
allowedFeatures count
performance.timeOrigin
observed timer granularity
granularity verdict
allowedFeatures list (click)
Math.tan(-1e300) is not a stunt: the result comes from the platform's libm and differs between V8 on glibc, V8 on Android's bionic, JavaScriptCore and SpiderMonkey, so it separates engines that every string-based check reports as identical. The observed timer granularity is measured by spinning performance.now() until it moves — 5 µs means crossOriginIsolated, 100 µs is the standard Spectre-mitigated clamp, and coarser values mean an additional privacy mode is active. navigator.plugins is a fixed fake set in Chromium rather than a real plugin list, which is exactly why its length and contents identify the engine.

Full API support matrix

User agent

Deze probe leest alleen wat de browser aan een webpagina geeft. CAN-bus data (pedaalpositie, motor-RPM, echte wielsnelheid) is hier per definitie onbereikbaar — de auto geeft die niet aan webcontent. "Native speed" komt van de GPS-ontvanger via de Geolocation API; in de Tesla-browser wordt geolocation vaak op autoniveau geblokkeerd. De "computed" kolom is de fallback: snelheid berekend uit opeenvolgende posities. Groene badges = werkt op jouw firmware, rood = geblokkeerd of afwezig. Exporteer het rapport en vergelijk tussen firmware-versies.