Files
virtual-controller/remote-client-android/PERF_SESSION_2026-07-31.md
T
Paul Lipscomb 60c8255c2d Android perf: fix fader stutter (appendLog), drawText feedback widget, bisect docs
Bisect session with per-suspect kill switches (see BISECT_LOG.md):
- appendLog was the confirmed stutter culprit — ran per-message (~110/sec)
  on the main thread even with the log panel hidden; A/B verified. Disabled
  via DEBUG_DISABLE_APPEND_LOG; shippable visible-only fix still TODO.
- Per-message Log.d also disabled (freebie, no felt difference).
- Glow, gradient fill, network flash, feedback setText all exonerated and
  restored; switches left in place at false.
- FocusFeedbackWidgetView rewritten: TextView -> bare View + canvas.drawText.
  60hz updates are now field-assign + invalidate, no per-update text Layout.
- ContextMenuViewLogic: explicit color-picker branch for the new view type.
- Timecode chunkiness root cause was REAPER audio buffer size (source-side
  burst cadence), documented in PERF_SESSION_2026-07-31.md; stutter bug
  report stamped RESOLVED.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:46:07 -05:00

4.1 KiB

Performance Session — 2026-07-31

Android tablet remote: fader stutter + timecode feedback fluidity. Both root causes found and fixed. Companion docs: FEEDBACK_STUTTER_BUG.md (original symptom report, now resolved), BISECT_LOG.md (run-by-run test record).

The two problems (they were separate)

1. Fader stutter under 60hz feedback → appendLog

Root cause: appendLog() in ArrangeFunctionsWebSocket.java ran on every in/out WebSocket message (~110/sec during a drag with feedback streaming), even though the log panel is hidden by default:

  • TextView.append() on a selectable (editable/spannable) TextView — cost grows as the text grows
  • 2-3 posted main-thread runnables per message (~300/sec), queued ahead of touch events
  • Every ~0.5s, the line-trim: full 350-line copy/split/join/setText — a multi-millisecond spike that blew the 16ms frame budget. Periodic spikes = the rhythmic stutter.

Proof: A/B tested — kill switch off = smooth, on = stutter returns immediately. Second time this exact code was convicted (an earlier session already replaced get+concat+setText with append; the remaining per-message work was still the culprit).

Fix (current): DEBUG_DISABLE_APPEND_LOG = true — appendLog fully off. Side effect: log panel shows nothing. Shippable fix still TODO (see below).

2. Chunky timecode readout → REAPER buffer size

Root cause (found by Paul): REAPER emits OSC feedback per audio block. A high audio buffer size makes feedback updates leave REAPER in uneven bursts — no client-side rendering can smooth a chunky source. Lowering the buffer restored fluid timecode.

Rule of thumb: if timecode/dB readouts ever look chunky again on any client (Android, iOS, desktop log), check REAPER's audio buffer size FIRST, before suspecting client code.

Changes made this session

Change File Status
Kill switch: appendLog off arrange/ArrangeFunctionsWebSocket.java Active (DEBUG_DISABLE_APPEND_LOG = true)
Kill switch: per-message Log.d off same Active (DEBUG_DISABLE_RECEIVE_LOGCAT = true) — no felt difference, kept as freebie
Kill switch: network activity-flash same Inactive (false) — exonerated; tablet's Activity button (default OFF) already gated it
Kill switch: feedback setText same Inactive (false) — exonerated, 60hz setText was affordable
Kill switch: fader glow / software layer widgets/FaderWidgetView.java, widgets/FocusFaderWidgetView.java Inactive (false) — exonerated by Paul, glow restored
Kill switch: gradient fill alloc same two files Inactive (false) — exonerated, gradient restored
FocusFeedbackWidgetView rewritten: TextView → bare View + canvas.drawText widgets/FocusFeedbackWidgetView.java Permanent. Update = string compare + field assign + invalidate; no text Layout object per update; same-frame updates collapse to one draw. All sizing/pinch/font-weight/color behavior preserved.
Color picker: explicit branch for new view type arrange/ContextMenuViewLogic.java Permanent — was matching via instanceof TextView, which the rewrite would have silently broken

The lesson (codebase rule)

Main thread = one queue for touch, draw, and posted runnables. Per-message work is fine if small and constant (setText, a flash, a field assign). Banned on the message stream: anything unbounded (grows with history) or spiky (periodic big jobs). appendLog was both, for an invisible panel.

Follow-ups (not yet done)

  1. Shippable appendLog fix — replace the kill switch: log only while the panel is visible; bounded ring buffer for history so opening the panel isn't empty; then delete DEBUG_DISABLE_APPEND_LOG.
  2. Decide fate of the exonerated kill switches (probably delete for cleanliness; they're documented in BISECT_LOG.md if ever needed again).
  3. Optional hygiene: create the fader fill gradient once (setLocalMatrix) instead of per-onDraw; gate Log.d behind BuildConfig.DEBUG instead of a custom flag.
  4. Consider documenting the REAPER-buffer/feedback-cadence relationship in CLAUDE.md's DAW integration section.