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>
This commit is contained in:
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -1,4 +1,4 @@
|
||||
#Thu Jul 30 19:08:15 CDT 2026
|
||||
#Fri Jul 31 17:37:31 CDT 2026
|
||||
base.0=E\:\\20260608-virtual-controller-py\\remote-client-android\\app\\build\\intermediates\\dex\\debug\\mergeExtDexDebug\\classes.dex
|
||||
base.1=E\:\\20260608-virtual-controller-py\\remote-client-android\\app\\build\\intermediates\\dex\\debug\\mergeProjectDexDebug\\0\\classes.dex
|
||||
base.2=E\:\\20260608-virtual-controller-py\\remote-client-android\\app\\build\\intermediates\\dex\\debug\\mergeProjectDexDebug\\13\\classes.dex
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+22
-1
@@ -37,6 +37,24 @@ import okio.ByteString;
|
||||
public class ArrangeFunctionsWebSocket {
|
||||
private static final String TAG = "VCWebSocket";
|
||||
|
||||
// ---- Stutter-bisect kill switches (see BISECT_LOG.md) ----
|
||||
// Flip to false to restore the original behavior. Suspect #1+#2 in the
|
||||
// Focus Fader lag investigation: appendLog runs on EVERY in/out message
|
||||
// (~110/sec under 60hz feedback) even while the log panel is hidden.
|
||||
private static final boolean DEBUG_DISABLE_APPEND_LOG = true;
|
||||
// Suspect #8: Log.d with full-JSON string concat on EVERY incoming message
|
||||
// (~110/sec under 60hz feedback) — runs on the OkHttp reader thread, but
|
||||
// the concat allocs + logd writes are pure per-message overhead.
|
||||
private static final boolean DEBUG_DISABLE_RECEIVE_LOGCAT = true;
|
||||
// Suspect #4: activity-ring flash on every incoming network message —
|
||||
// removeCallbacks + postDelayed on the main thread at 60hz+ per widget.
|
||||
// Local touches still flash their card; only network-triggered flashes stop.
|
||||
private static final boolean DEBUG_DISABLE_NETWORK_FLASH = false;
|
||||
// Suspect #5: setText() on the feedback TextView at 60hz — new text layout
|
||||
// + redraw on the main thread per message. While true, feedback readouts
|
||||
// (timecode etc.) freeze at their last shown text.
|
||||
private static final boolean DEBUG_DISABLE_FEEDBACK_SETTEXT = false;
|
||||
|
||||
private final ArrangeActivity vc;
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
|
||||
@@ -65,7 +83,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
Log.d(TAG, "Received: " + text);
|
||||
if (!DEBUG_DISABLE_RECEIVE_LOGCAT) Log.d(TAG, "Received: " + text);
|
||||
dispatch(text);
|
||||
}
|
||||
|
||||
@@ -291,6 +309,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
String uid = envelope.optString("uid", "");
|
||||
String text = envelope.optString("text", "");
|
||||
appendLog("IN", "widget_feedback uid=" + uid + " text=\"" + text + "\"");
|
||||
if (DEBUG_DISABLE_FEEDBACK_SETTEXT) return;
|
||||
vc.runOnUiThread(() -> {
|
||||
TextUpdatable widget = vc.feedbackRegistry.get(uid);
|
||||
if (widget != null) widget.setTextFromNetwork(text);
|
||||
@@ -300,6 +319,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
|
||||
/** Activity-ring flash on the card wrapping this uid — matches iOS's BaseWidget.update(value:) side effect. */
|
||||
private void flashCard(String uid) {
|
||||
if (DEBUG_DISABLE_NETWORK_FLASH) return;
|
||||
ArrangeCardView card = vc.cardRegistry.get(uid);
|
||||
if (card != null) card.flashActivity();
|
||||
}
|
||||
@@ -330,6 +350,7 @@ public class ArrangeFunctionsWebSocket {
|
||||
* run on every call, just often enough to bound memory growth.
|
||||
*/
|
||||
private void appendLog(String direction, String message) {
|
||||
if (DEBUG_DISABLE_APPEND_LOG) return;
|
||||
String line = "[" + direction + "] " + message + "\n";
|
||||
vc.runOnUiThread(() -> {
|
||||
vc.outputTextView.append(line);
|
||||
|
||||
+5
-1
@@ -81,7 +81,11 @@ public class ContextMenuViewLogic {
|
||||
@Override
|
||||
public void onColorSelected(int colorIndex) {
|
||||
int colorValue = ContextMenuView.COLORS[colorIndex];
|
||||
if (controlView instanceof android.widget.TextView) {
|
||||
// FocusFeedbackWidgetView is a bare canvas View (drawText), not a
|
||||
// TextView — it needs its own branch or the instanceof misses it.
|
||||
if (controlView instanceof FocusFeedbackWidgetView) {
|
||||
((FocusFeedbackWidgetView) controlView).setTextColor(colorValue);
|
||||
} else if (controlView instanceof android.widget.TextView) {
|
||||
((android.widget.TextView) controlView).setTextColor(colorValue);
|
||||
}
|
||||
String colorHex = String.format("#%06X", (0xFFFFFF & colorValue));
|
||||
|
||||
+26
-9
@@ -33,6 +33,16 @@ public class FaderWidgetView extends FrameLayout implements UpdatableWidget {
|
||||
private static final int PAD_HEIGHT_DP = 90;
|
||||
private static final float FINE_SENSITIVITY = 0.10f;
|
||||
|
||||
// Stutter-bisect suspect #6 (see BISECT_LOG.md): setShadowLayer glow forces
|
||||
// LAYER_TYPE_SOFTWARE — full CPU re-raster of the fader every drag frame.
|
||||
// true = no glow, hardware rendering; false = original glow look.
|
||||
private static final boolean DEBUG_DISABLE_GLOW = false;
|
||||
|
||||
// Stutter-bisect suspect #7 (see BISECT_LOG.md): a new LinearGradient is
|
||||
// allocated on every onDraw — GC churn during drags. true = flat translucent
|
||||
// fill, zero allocations; false = original gradient fill.
|
||||
private static final boolean DEBUG_DISABLE_GRADIENT_FILL = false;
|
||||
|
||||
private final String uid;
|
||||
private final Paint bgPaint = new Paint();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
@@ -69,13 +79,15 @@ public class FaderWidgetView extends FrameLayout implements UpdatableWidget {
|
||||
|
||||
glowLinePaint.setColor(colorArgb);
|
||||
glowLinePaint.setStrokeWidth(6f);
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
|
||||
dotPaint.setColor(Color.WHITE);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
|
||||
// Shadow layers only render on a software-rendered layer.
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
if (!DEBUG_DISABLE_GLOW) {
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
// Shadow layers only render on a software-rendered layer.
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUid() {
|
||||
@@ -131,11 +143,16 @@ public class FaderWidgetView extends FrameLayout implements UpdatableWidget {
|
||||
|
||||
// Transparent at the bottom of the fill, full color+alpha at the value
|
||||
// line — analog of iOS's CAGradientLayer fill.
|
||||
fillPaint.setShader(new LinearGradient(
|
||||
0, h, 0, fillTop,
|
||||
Color.argb(0, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Color.argb(115, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Shader.TileMode.CLAMP));
|
||||
if (DEBUG_DISABLE_GRADIENT_FILL) {
|
||||
fillPaint.setShader(null);
|
||||
fillPaint.setColor(Color.argb(60, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)));
|
||||
} else {
|
||||
fillPaint.setShader(new LinearGradient(
|
||||
0, h, 0, fillTop,
|
||||
Color.argb(0, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Color.argb(115, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Shader.TileMode.CLAMP));
|
||||
}
|
||||
canvas.drawRect(0, fillTop, w, h, fillPaint);
|
||||
|
||||
// Glowing value line + a small glowing dot at its center — analog of the
|
||||
|
||||
+26
-8
@@ -36,6 +36,16 @@ public class FocusFaderWidgetView extends FrameLayout implements UpdatableWidget
|
||||
private static final float FINE_SENSITIVITY = 0.10f;
|
||||
private static final int PAD_HEIGHT_DP = 90;
|
||||
|
||||
// Stutter-bisect suspect #6 (see BISECT_LOG.md): setShadowLayer glow forces
|
||||
// LAYER_TYPE_SOFTWARE — full CPU re-raster of the fader every drag frame.
|
||||
// true = no glow, hardware rendering; false = original glow look.
|
||||
private static final boolean DEBUG_DISABLE_GLOW = false;
|
||||
|
||||
// Stutter-bisect suspect #7 (see BISECT_LOG.md): a new LinearGradient is
|
||||
// allocated on every onDraw — GC churn during drags. true = flat translucent
|
||||
// fill, zero allocations; false = original gradient fill.
|
||||
private static final boolean DEBUG_DISABLE_GRADIENT_FILL = false;
|
||||
|
||||
private final String uid;
|
||||
private final Paint bgPaint = new Paint();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
@@ -70,10 +80,13 @@ public class FocusFaderWidgetView extends FrameLayout implements UpdatableWidget
|
||||
borderPaint.setStrokeWidth(4f);
|
||||
glowLinePaint.setColor(colorArgb);
|
||||
glowLinePaint.setStrokeWidth(6f);
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
dotPaint.setColor(Color.WHITE);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
if (!DEBUG_DISABLE_GLOW) {
|
||||
glowLinePaint.setShadowLayer(18f, 0f, 0f, colorArgb);
|
||||
dotPaint.setShadowLayer(10f, 0f, 0f, colorArgb);
|
||||
// Shadow layers only render on a software-rendered layer.
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUid() {
|
||||
@@ -118,11 +131,16 @@ public class FocusFaderWidgetView extends FrameLayout implements UpdatableWidget
|
||||
float fillHeight = Math.max(8f, h * value);
|
||||
float fillTop = h - fillHeight;
|
||||
|
||||
fillPaint.setShader(new LinearGradient(
|
||||
0, h, 0, fillTop,
|
||||
Color.argb(0, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Color.argb(115, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Shader.TileMode.CLAMP));
|
||||
if (DEBUG_DISABLE_GRADIENT_FILL) {
|
||||
fillPaint.setShader(null);
|
||||
fillPaint.setColor(Color.argb(60, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)));
|
||||
} else {
|
||||
fillPaint.setShader(new LinearGradient(
|
||||
0, h, 0, fillTop,
|
||||
Color.argb(0, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Color.argb(115, Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor)),
|
||||
Shader.TileMode.CLAMP));
|
||||
}
|
||||
canvas.drawRect(0, fillTop, w, h, fillPaint);
|
||||
|
||||
canvas.drawLine(0, fillTop, w, fillTop, glowLinePaint);
|
||||
|
||||
+43
-21
@@ -1,14 +1,14 @@
|
||||
package com.virtualcontroller.remote.widgets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.text.TextPaint;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.widget.TextView;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* Read-only Channel Focus DAW-state readout — updated live via
|
||||
@@ -21,8 +21,16 @@ import android.widget.TextView;
|
||||
* drives the font size and lets the frame follow, rather than scaling the
|
||||
* frame like every other widget type — the aspect ratio then can't drift out
|
||||
* of sync with the text it's wrapping.
|
||||
*
|
||||
* Draws via canvas.drawText on a bare View rather than extending TextView:
|
||||
* this readout repaints at up to 60hz (timecode), and TextView.setText builds
|
||||
* a new internal text Layout on every call — measurement, line-breaking, span
|
||||
* bookkeeping — all for one line of digits that never wraps. drawText skips
|
||||
* every bit of that: a network update is now just a String field assign +
|
||||
* invalidate, and the draw is a single glyph run. (Same conclusion
|
||||
* FEEDBACK_STUTTER_BUG.md guessed TouchOSC reached.)
|
||||
*/
|
||||
public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
public class FocusFeedbackWidgetView extends View implements TextUpdatable {
|
||||
|
||||
public static final float DEFAULT_FONT_SP = 32f;
|
||||
public static final float MIN_FONT_SP = 12f;
|
||||
@@ -36,6 +44,8 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
void onFitRequest();
|
||||
}
|
||||
|
||||
private final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
|
||||
private String text;
|
||||
private float fontSp = DEFAULT_FONT_SP;
|
||||
private OnFitRequestListener fitListener;
|
||||
|
||||
@@ -51,14 +61,9 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
*/
|
||||
public FocusFeedbackWidgetView(Context context, String initialText, int savedHeightPx) {
|
||||
super(context);
|
||||
setText(initialText);
|
||||
setTextColor(TEXT_COLOR);
|
||||
setBackgroundColor(Color.TRANSPARENT);
|
||||
setGravity(Gravity.CENTER);
|
||||
setSingleLine(true);
|
||||
|
||||
int pad = dp(context, PADDING_DP);
|
||||
setPadding(pad, pad, pad, pad);
|
||||
this.text = initialText != null ? initialText : "";
|
||||
paint.setColor(TEXT_COLOR);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
|
||||
int defaultHeight = fittedSize(context, initialText, DEFAULT_FONT_SP)[1];
|
||||
if (savedHeightPx > 0 && defaultHeight > 0) {
|
||||
@@ -72,7 +77,8 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
}
|
||||
|
||||
public void setTextColor(int color) {
|
||||
super.setTextColor(color);
|
||||
paint.setColor(color);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setFontWeight(String weight) {
|
||||
@@ -83,14 +89,14 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
tf = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
|
||||
} else if ("super_bold".equalsIgnoreCase(weight)) {
|
||||
tf = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD);
|
||||
getPaint().setStrokeWidth(2.0f);
|
||||
paint.setStrokeWidth(2.0f);
|
||||
} else if ("thin".equalsIgnoreCase(weight)) {
|
||||
tf = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL);
|
||||
getPaint().setStrokeWidth(0.5f);
|
||||
paint.setStrokeWidth(0.5f);
|
||||
} else {
|
||||
tf = semiboldMono();
|
||||
}
|
||||
setTypeface(tf);
|
||||
paint.setTypeface(tf);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@@ -109,12 +115,12 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
|
||||
/** Box size for the current text at the current font size. */
|
||||
public int[] fittedSizePx() {
|
||||
return fittedSize(getContext(), getText().toString(), fontSp);
|
||||
return fittedSize(getContext(), text, fontSp);
|
||||
}
|
||||
|
||||
/** Box size for the current text at DEFAULT_FONT_SP — what "Reset size" converges to. */
|
||||
public int[] defaultFittedSizePx() {
|
||||
return fittedSize(getContext(), getText().toString(), DEFAULT_FONT_SP);
|
||||
return fittedSize(getContext(), text, DEFAULT_FONT_SP);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,15 +148,31 @@ public class FocusFeedbackWidgetView extends TextView implements TextUpdatable {
|
||||
|
||||
@Override
|
||||
public void setTextFromNetwork(String text) {
|
||||
if (text.equals(getText().toString())) return;
|
||||
setText(text);
|
||||
if (text == null || text.equals(this.text)) return;
|
||||
this.text = text;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// MARK: - Drawing
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (text.isEmpty()) return;
|
||||
// Centered both ways: CENTER-aligned paint handles x; y places the
|
||||
// baseline so the glyph block's vertical middle sits at the view's.
|
||||
Paint.FontMetrics fm = paint.getFontMetrics();
|
||||
float y = getHeight() / 2f - (fm.ascent + fm.descent) / 2f;
|
||||
canvas.drawText(text, getWidth() / 2f, y, paint);
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
private void applyFont() {
|
||||
setTypeface(semiboldMono());
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, fontSp);
|
||||
paint.setTypeface(semiboldMono());
|
||||
paint.setTextSize(TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_SP, fontSp, getResources().getDisplayMetrics()));
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private void requestFit() {
|
||||
|
||||
Reference in New Issue
Block a user