Stats & device load
The in-call debug panel (CallStatsPanel, opened with the (i) button) shows
what the media stack is actually doing. Its guiding constraint: it costs
nothing when it’s closed.
Zero-cost-when-closed
// CallScreen
DisposableEffect(statsToggled) {
onStatsVisibilityChange(statsToggled) // → webRtcClient.startStatsPolling() / stop
onDispose { onStatsVisibilityChange(false) }
}- The poll loop only exists while
statsToggledis true. - It runs inside
repeatOnLifecycle(Lifecycle.State.STARTED), so a backgrounded call with the panel left open also stops polling. DeviceLoadSampler.reset()is called when polling (re)starts so the first CPU delta is over a fresh interval.
A call with the panel closed never calls getStats, never reads
/proc/self/stat, and never registers the battery sticky-intent receiver.
The poll loop
pollStats() runs on a coroutine, ~1 Hz:
peerConnection.getStats { report ->
val stats = parseStats(report) // on the signaling thread
mainHandler.post { onStats(stats) } // → CallStats state → panel recomposes
}parseStats - byte-delta bitrate
WebRTC’s RTCStatsReport gives cumulative bytesSent / bytesReceived.
parseStats diffs them against the previous poll and the wall-clock interval:
bitrateKbps ≈ (bytesNow - bytesPrev) * 8 / elapsedSeconds / 1000The byte baselines (lastVideoBytesSent, lastVideoBytesRecv,
lastAudioBytesSent, lastAudioBytesRecv, lastStatsAtMs) are @Volatile
because parseStats runs on the signaling thread and resetStatsBaseline() is
called from the UI loop - the volatile stops a resumed poll from diffing against
a half-updated baseline.
From the same report it reads:
| Field | Source stat |
|---|---|
| Send / recv resolution | outbound-rtp / inbound-rtp frameWidth × frameHeight |
| Send / recv FPS | framesPerSecond |
| RTT | candidate-pair currentRoundTripTime (ms) |
| Packet loss % | inbound-rtp packetsLost / packetsReceived |
| Video codec | codec mimeType (e.g. video/H265) |
| Audio codec / sample rate / stereo | codec mimeType, clockRate, sdpFmtpLine stereo=1 |
| HW encode / decode | outbound-rtp encoderImplementation / inbound-rtp decoderImplementation - “HW” unless the string looks like a software codec |
Output is a CallStats data class snapshot. Bitrates
are labelled approximate for exactly this reason.
Device load (DeviceLoadSampler.kt)
Three cheap signals, each of which degrades to null (and the panel omits the
line) when a device doesn’t report it.
CPU %
This process’s CPU time over the interval:
utime + stime from /proc/self/stat (fields 14, 15 - parsed after the last ')')/proc/self/statis always readable for your own process - no permission, no SELinux issue.- Android hardcodes
_SC_CLK_TCK = 100, so one tick = 10 ms of CPU time. - Normalised by
Runtime.availableProcessors()so the result is 0–100 no matter how many cores are pinned. - First sample after a
reset()returnsnull(no prior interval).
Battery temperature
ACTION_BATTERY_CHANGED sticky intent → EXTRA_TEMPERATURE (tenths of °C),
sanity-bounded to -50 … 100 °C. This is the most portable proxy for “is the
SoC heating up under this encode load” - nearly every device reports it, and it
moves visibly during a sustained 4K call.
Instantaneous power draw
watts = |BATTERY_PROPERTY_CURRENT_NOW| (µA) × 1e-6 × EXTRA_VOLTAGE (mV) / 1000CURRENT_NOWis signed and OEMs disagree on the sign convention (some negative = discharging, some the opposite), so only the magnitude is used.- Sanity-bounded to
0.05 … 60 W- outside that range the device is reporting garbage and the field isnull. - Together with battery temperature this gives a rough “how hard is this call working the phone” reading without polling GPU counters (which are inconsistent and often unavailable).
Why not GPU / thermal-status APIs
PowerManager.getThermalHeadroom is API 30+, throttled, and returns
NaN on many devices. GPU load has no stable public API. Battery temp +
power draw is the metric that actually works across the S9+ ↔ Pixel range.
Panel layout
Grouped into Video (encode/decode HW/SW, codec, send ↑ / recv ↓ with bitrate + resolution + FPS), Audio (codec, sample rate, stereo/mono, mic mode, send/recv kbps), Network (latency, loss - only shown when non-trivial), and Device (CPU, power, battery - only the lines the device reports). Everything hugs its content; the panel is persistent (not tied to the chrome auto-hide).