Skip to content

Device heating: JS thread burns CPU continuously while app is idle (release build) #137

Description

@lamat1111

Summary

On the live/release build, the React Native JS thread (mqt_v_js) consumes CPU continuously while the app is idle (on screen, no user input). Measured ~21 s of CPU over 185 s of idle = ~11.5% of one core, sustained, with correlated GC churn. The device sits at the first thermal throttle band (skin 38 °C, Thermal Status 1) during this idle window. This causes noticeable device heating during normal "just sitting on the conversation list" use.

A separate, bounded crypto burst occurs at startup (see below) and is likely expected; it is not the focus of this issue.

Device / build

Phone Motorola Edge 50 Fusion
SoC Qualcomm Snapdragon 7s Gen 3 (SM7435), 8 cores, arm64-v8a
RAM ~11.6 GB
OS Android 16 (SDK 36), build W1UUIS36H.110-51-1-1, security patch 2026-05-01
App com.quilibrium.quorummobile (live build, NOT .debug)
App version versionName 1.1.0, versionCode 45, built 2026-06-11

Profiling method: USB + ADB only, read-only. No Metro / dev env running (so this is real release behavior, not a dev-mode artifact).

Evidence

Idle JS burn (core finding)

Pre-committed protocol: sample each thread's cumulative CPU (ms) every 15 s for 185 s, screen on, app on conversation list, untouched; interpret only after all 13 samples collected.

Thread start end delta over 185 s idle % of one core
mqt_v_js (RN JS thread) 85000 ms 106346 ms +21,346 ms (~21.3 s) ~11.5% continuous
HeapTaskDaemon (GC) 19062 ms 24288 ms +5,226 ms (~5.2 s) ~2.8%
DefaultDispatch (native crypto) flat flat ~0 quiet

Per-15s-interval JS deltas (ms): 2352, 1463, 1162, 3080, 583, 1282, 1381, 850, 1094, 677, 1314, 6108.

This is near-continuous churn, not a single periodic timer waking briefly every N seconds. Points at a continuous loop, an off-screen re-render storm, or a short-interval resync/subscription.

Cumulative CPU counters (monotonic) were used rather than instantaneous top snapshots, so the result is not a sampling artifact.

Startup crypto burst (likely expected, noted for completeness)

Right after launch/connect, DefaultDispatch (Kotlin Dispatchers.Default → Rust/UniFFI crypto) ran a full core at 100% for tens of seconds (cumulative ~110 s CPU), then stopped. schedstat: ~109.6 s running vs ~0.74 s waiting → CPU-bound, not blocked. It is not an infinite loop — it ends and stays quiet at idle. Probable initial backlog decryption + ratchet/DKG setup on connect (inferred, not stack-confirmed; native stack needs root on a production device).

Thermals

During hot windows (dumpsys thermalservice): CPU cores 50–58 °C (hottest subsystem), GPU 41–48 °C (moderate; not a rendering problem), skin ~38 °C, battery 28 °C. Skin throttle threshold table starts at 38 °C. Device reached Thermal Status 1 and held there throughout idle.

Logcat

adb logcat --pid=<pid> produced no app output during bursts (release build strips logs). No ANR, no Skipped N frames, no Davey. Rules out main-thread block and rendering overhead as the cause.

Measured vs inferred

Measured (high confidence): idle JS CPU burn (~21 s / 185 s, ~11.5% of a core, continuous) with correlated GC; startup crypto burst (~110 s then stops, proven not-a-loop); CPU is the hot subsystem; device reaches first throttle band at idle.

Inferred (for confirmation): startup burst = backlog decrypt + ratchet/DKG (plausible, not stack-confirmed); idle JS burn = a continuous loop / re-render storm / short-interval resync running with no UI mounted.

Suggested next steps

  1. Hunt the idle JS work first — clearest bug. Look for setInterval/timers/subscriptions/effects that run with no screen mounted (WebSocket keepalive/resync, notification polling, recurring config/blob refetch, or a component re-rendering on a timer). A Hermes sampling profiler or temporary timing logs around suspected intervals should name it quickly.
  2. Confirm the startup crypto burst is bounded and not re-running on every reconnect (which would re-heat repeatedly on flaky network).
  3. Consider debounce/cache/cancellation so reconnect and idle don't recompute.

Reproduce (read-only, safe)

PID=$(adb shell pidof com.quilibrium.quorummobile)
# resolve thread names:
adb shell top -H -b -n 1 -p $PID | awk 'NR>7 && $9+0>25 {print $1, $9"%"}' \
  | while read tid pct; do echo "$(adb shell cat /proc/$PID/task/$tid/comm) $pct"; done
# idle JS burn: diff mqt_v_js schedstat field 1 (ns running) across a few minutes of idle
adb shell cat /proc/$PID/task/<mqt_v_js_TID>/schedstat
adb shell dumpsys thermalservice | grep -E "Thermal Status|mName=skin"

Do NOT uninstall the app — live build, real user data. All profiling above is read-only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions