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
- 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.
- Confirm the startup crypto burst is bounded and not re-running on every reconnect (which would re-heat repeatedly on flaky network).
- 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.
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
com.quilibrium.quorummobile(live build, NOT.debug)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.
mqt_v_js(RN JS thread)HeapTaskDaemon(GC)DefaultDispatch(native crypto)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
topsnapshots, so the result is not a sampling artifact.Startup crypto burst (likely expected, noted for completeness)
Right after launch/connect,
DefaultDispatch(KotlinDispatchers.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, noSkipped N frames, noDavey. 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
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.Reproduce (read-only, safe)
Do NOT uninstall the app — live build, real user data. All profiling above is read-only.