Files
virtual-controller/remote-client-android/FEEDBACK_STUTTER_BUG.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.6 KiB

Android Feedback Widget Stutter Bug

RESOLVED 2026-07-31 — two separate root causes: (1) fader stutter was appendLog() running per-message on the main thread even with the log panel hidden (A/B confirmed, disabled via kill switch); (2) chunky timecode was REAPER's audio buffer size making OSC feedback leave in bursts. Full write-up: PERF_SESSION_2026-07-31.md; run-by-run record: BISECT_LOG.md. The "custom Canvas view" hypothesis below was also acted on — FocusFeedbackWidgetView now draws via canvas.drawText.

Summary

When the Android tablet app receives high-frequency network feedback updates (60hz timecode from desktop), the feedback widget AND the fader control stutter and become unresponsive. Works perfectly on iOS and desktop apps at the same 60hz update rate.

Symptoms

  • Feedback widgets (showing timecode, etc.) display with visible frame drops/stutter
  • Fader touch input becomes intermittent and unresponsive while feedback updates are happening
  • Stutter correlates directly with network update frequency — higher frequency = worse stutter
  • But: When feedback updates are disabled entirely, fader moves smoothly

What Works Fine

  • iOS: Receives and displays 60hz feedback updates smoothly while handling touch input perfectly
  • Desktop (PyQt): No performance issues at all with high-frequency updates
  • Android: Fader drag is smooth and responsive when feedback updates are NOT happening

Current Code Flow

  1. WebSocket listener receives message on background thread
  2. ArrangeFunctionsWebSocket.handleWidgetFeedback() processes the JSON
  3. Calls vc.runOnUiThread() to queue update on main thread
  4. Inside runOnUiThread: calls widget.setTextFromNetwork(text)
  5. FocusFeedbackWidgetView.setTextFromNetwork():
    • Checks if text changed (skip if identical)
    • Calls setText(text)
    • Was calling requestFit() (recalculates widget size) — REMOVED

What We've Tried

  1. Removed requestFit() calls — was recalculating widget dimensions 60x/sec, expensive
  2. Added text equality check — only call setText() if text actually changed
  3. Removed requestFit() from setText() — no longer triggers layout recalculation
  4. Tried Handler-based batching — user said to revert immediately (didn't help)

Result: No improvement. Still stutters.

Hypothesis: setText() Itself is Expensive

Even a simple setText() call triggers Android's view measurement/layout system:

  • Measure text width/height
  • Recalculate layout
  • Trigger redraw

At 60hz, this is apparently enough to starve the main thread of time to process touch events.

But this seems wrong. TouchOSC works fine on Android with similar high-frequency updates.

Investigation Needed

  1. Is it actually setText()? — Test: completely disable feedback updates (don't call setTextFromNetwork at all). If fader still stutters, problem is elsewhere.
  2. Is flashCard() the culprit?handleWidgetFeedback() calls flashCard(uid) after setText. That triggers activity ring animation. Could that be the expensive part?
  3. Is there something else? — Maybe the problem is in the WebSocket thread itself, not the UI updates?
  4. Custom Canvas View? — TouchOSC might use custom Canvas-based rendering instead of TextViews. Maybe we need to do the same.

Architecture Difference: iOS vs Android

iOS:

  • Render server is completely separate from main thread
  • Touch events handled by separate high-priority daemon, never blocked by UI work
  • Core Animation batches updates intelligently

Android:

  • Single main thread handles both touch events AND UI updates
  • If main thread is busy with view measurement/layout, touch events get dropped
  • No separation between UI work and input handling

Files Involved

  • ArrangeFunctionsWebSocket.java — handleWidgetFeedback() method (line ~290)
  • FocusFeedbackWidgetView.java — setTextFromNetwork() method (line ~143)
  • ArrangeCardView.java — flashCard(), refitFeedback() methods (for context on what happens after setText)

Next Steps for Fable

  1. First: Test if disabling feedback updates entirely makes fader smooth (confirm setText is actually the problem)
  2. If still stutters: Look at flashCard() and activity ring animation
  3. If not setText/flashCard: Investigate WebSocket thread behavior
  4. If those aren't it: Prototype a custom Canvas-based feedback view as a replacement for TextView

Key Question

Why does TouchOSC handle this fine on Android if TextView updates are so expensive? Either:

  • They don't update as frequently as 60hz
  • They use a custom view for rendering
  • They've optimized something we haven't found yet