From a38aa060ebf14ef7081202d88a8f8e02d556ef56 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Tue, 9 Aug 2022 16:23:26 -0700 Subject: [PATCH] Optimizing View capture logic Doing view capture in two passes 1) UI thread: creating a flat copy of the full view tree. Since view structure can change on the UI thread, this needs to be captured synchronously on UI thread. 2) BG thread: We capture the properties of the View on background thread using the flat tree created in the previous step. Since reading the properties is atomic, there is no synchronization issued. One down side of this approach is that the properties might change while the background-tep is underway. So all the properties of a of a node may not represent the frame-state. But for the purpose of animations, we can just refer a few continous frames. Bug: 242095405 Test: Verified on device, frame capture reduced by at least 5x every time. Change-Id: I0a61fb24669940b3b3533c0471e42e476709da55 --- src/com/android/launcher3/Launcher.java | 2 +- .../android/launcher3/util/ViewCapture.java | 276 ++++++++++++------ 2 files changed, 191 insertions(+), 87 deletions(-) diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 5ee9aa8668..05536d30f4 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1505,7 +1505,7 @@ public class Launcher extends StatefulActivity root.getViewTreeObserver().removeOnDrawListener(mViewCapture); } mViewCapture = new ViewCapture(root); - root.getViewTreeObserver().addOnDrawListener(mViewCapture); + mViewCapture.attach(); } } diff --git a/src/com/android/launcher3/util/ViewCapture.java b/src/com/android/launcher3/util/ViewCapture.java index cf9ea699d9..cf4e84a84b 100644 --- a/src/com/android/launcher3/util/ViewCapture.java +++ b/src/com/android/launcher3/util/ViewCapture.java @@ -15,10 +15,11 @@ */ package com.android.launcher3.util; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + import android.content.res.Resources; import android.os.Handler; -import android.os.Looper; -import android.os.SystemClock; +import android.os.Message; import android.os.Trace; import android.util.Base64; import android.util.Base64OutputStream; @@ -28,6 +29,7 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver.OnDrawListener; import androidx.annotation.UiThread; +import androidx.annotation.WorkerThread; import com.android.launcher3.view.ViewCaptureData.ExportedData; import com.android.launcher3.view.ViewCaptureData.FrameData; @@ -36,7 +38,7 @@ import com.android.launcher3.view.ViewCaptureData.ViewNode; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.OutputStream; -import java.util.concurrent.FutureTask; +import java.util.concurrent.Future; /** * Utility class for capturing view data every frame @@ -45,49 +47,132 @@ public class ViewCapture implements OnDrawListener { private static final String TAG = "ViewCapture"; + // Number of frames to keep in memory private static final int MEMORY_SIZE = 2000; + // Initial size of the reference pool. This is at least be 5 * total number of views in + // Launcher. This allows the first free frames avoid object allocation during view capture. + private static final int INIT_POOL_SIZE = 300; private final View mRoot; - private final long[] mFrameTimes = new long[MEMORY_SIZE]; - private final Node[] mNodes = new Node[MEMORY_SIZE]; + private final Resources mResources; - private int mFrameIndex = -1; + private final Handler mHandler; + private final ViewRef mViewRef = new ViewRef(); + + private int mFrameIndexBg = -1; + private final long[] mFrameTimesBg = new long[MEMORY_SIZE]; + private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE]; + + // Pool used for capturing view tree on the UI thread. + private ViewRef mPool = new ViewRef(); /** * @param root the root view for the capture data */ public ViewCapture(View root) { mRoot = root; + mResources = root.getResources(); + mHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::captureViewPropertiesBg); + } + + /** + * Attaches the ViewCapture to the root + */ + public void attach() { + mHandler.post(this::initPool); } @Override public void onDraw() { Trace.beginSection("view_capture"); - long now = SystemClock.elapsedRealtimeNanos(); - - mFrameIndex++; - if (mFrameIndex >= MEMORY_SIZE) { - mFrameIndex = 0; - } - mFrameTimes[mFrameIndex] = now; - mNodes[mFrameIndex] = captureView(mRoot, mNodes[mFrameIndex]); + captureViewTree(mRoot, mViewRef); + Message m = Message.obtain(mHandler); + m.obj = mViewRef.next; + mHandler.sendMessage(m); Trace.endSection(); } + /** + * Captures the View property on the background thread, and transfer all the ViewRef objects + * back to the pool + */ + @WorkerThread + private boolean captureViewPropertiesBg(Message msg) { + ViewRef start = (ViewRef) msg.obj; + long time = msg.getWhen(); + if (start == null) { + return false; + } + mFrameIndexBg++; + if (mFrameIndexBg >= MEMORY_SIZE) { + mFrameIndexBg = 0; + } + mFrameTimesBg[mFrameIndexBg] = time; + + ViewPropertyRef recycle = mNodesBg[mFrameIndexBg]; + + ViewPropertyRef result = null; + ViewPropertyRef resultEnd = null; + + ViewRef current = start; + ViewRef last = start; + while (current != null) { + ViewPropertyRef propertyRef = recycle; + if (propertyRef == null) { + propertyRef = new ViewPropertyRef(); + } else { + recycle = recycle.next; + propertyRef.next = null; + } + + propertyRef.transfer(current); + last = current; + current = current.next; + + if (result == null) { + result = propertyRef; + resultEnd = result; + } else { + resultEnd.next = propertyRef; + resultEnd = propertyRef; + } + } + mNodesBg[mFrameIndexBg] = result; + ViewRef end = last; + Executors.MAIN_EXECUTOR.execute(() -> addToPool(start, end)); + return true; + } + + @UiThread + private void addToPool(ViewRef start, ViewRef end) { + end.next = mPool; + mPool = start; + } + + @WorkerThread + private void initPool() { + ViewRef start = new ViewRef(); + ViewRef current = start; + + for (int i = 0; i < INIT_POOL_SIZE; i++) { + current.next = new ViewRef(); + current = current.next; + } + + ViewRef end = current; + Executors.MAIN_EXECUTOR.execute(() -> { + addToPool(start, end); + if (mRoot.isAttachedToWindow()) { + mRoot.getViewTreeObserver().addOnDrawListener(this); + } + }); + } + /** * Creates a proto of all the data captured so far. */ public void dump(FileDescriptor out) { - Handler handler = mRoot.getHandler(); - if (handler == null) { - handler = Executors.MAIN_EXECUTOR.getHandler(); - } - FutureTask task = new FutureTask<>(this::dumpToProtoUI); - if (Looper.myLooper() == handler.getLooper()) { - task.run(); - } else { - handler.post(task); - } + Future task = UI_HELPER_EXECUTOR.submit(this::dumpToProto); try (OutputStream os = new FileOutputStream(out)) { ExportedData data = task.get(); Base64OutputStream encodedOS = new Base64OutputStream(os, @@ -100,70 +185,53 @@ public class ViewCapture implements OnDrawListener { } } - @UiThread - private ExportedData dumpToProtoUI() { + @WorkerThread + private ExportedData dumpToProto() { ExportedData.Builder dataBuilder = ExportedData.newBuilder(); - Resources res = mRoot.getResources(); + Resources res = mResources; - int size = (mNodes[MEMORY_SIZE - 1] == null) ? mFrameIndex + 1 : MEMORY_SIZE; + int size = (mNodesBg[MEMORY_SIZE - 1] == null) ? mFrameIndexBg + 1 : MEMORY_SIZE; for (int i = size - 1; i >= 0; i--) { - int index = (MEMORY_SIZE + mFrameIndex - i) % MEMORY_SIZE; + int index = (MEMORY_SIZE + mFrameIndexBg - i) % MEMORY_SIZE; + ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); + mNodesBg[index].toProto(res, nodeBuilder); dataBuilder.addFrameData(FrameData.newBuilder() - .setNode(mNodes[index].toProto(res)) - .setTimestamp(mFrameTimes[index])); + .setNode(nodeBuilder) + .setTimestamp(mFrameTimesBg[index])); } return dataBuilder.build(); } - private Node captureView(View view, Node recycle) { - Node result = recycle == null ? new Node() : recycle; - - result.clazz = view.getClass(); - result.hashCode = view.hashCode(); - result.id = view.getId(); - result.left = view.getLeft(); - result.top = view.getTop(); - result.right = view.getRight(); - result.bottom = view.getBottom(); - result.scrollX = view.getScrollX(); - result.scrollY = view.getScrollY(); - - result.translateX = view.getTranslationX(); - result.translateY = view.getTranslationY(); - result.scaleX = view.getScaleX(); - result.scaleY = view.getScaleY(); - result.alpha = view.getAlpha(); - - result.visibility = view.getVisibility(); - result.willNotDraw = view.willNotDraw(); - - if (view instanceof ViewGroup) { - ViewGroup parent = (ViewGroup) view; - result.clipChildren = parent.getClipChildren(); - int childCount = parent.getChildCount(); - if (childCount == 0) { - result.children = null; - } else { - result.children = captureView(parent.getChildAt(0), result.children); - Node lastChild = result.children; - for (int i = 1; i < childCount; i++) { - lastChild.sibling = captureView(parent.getChildAt(i), lastChild.sibling); - lastChild = lastChild.sibling; - } - lastChild.sibling = null; - } + private ViewRef captureViewTree(View view, ViewRef start) { + ViewRef ref; + if (mPool != null) { + ref = mPool; + mPool = mPool.next; + ref.next = null; } else { - result.clipChildren = false; - result.children = null; + ref = new ViewRef(); + } + ref.view = view; + start.next = ref; + if (view instanceof ViewGroup) { + ViewRef result = ref; + ViewGroup parent = (ViewGroup) view; + int childCount = ref.childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + result = captureViewTree(parent.getChildAt(i), result); + } + return result; + } else { + ref.childCount = 0; + return ref; } - return result; } - private static class Node { - + private static class ViewPropertyRef { // We store reference in memory to avoid generating and storing too many strings public Class clazz; public int hashCode; + public int childCount = 0; public int id; public int left, top, right, bottom; @@ -177,10 +245,41 @@ public class ViewCapture implements OnDrawListener { public boolean willNotDraw; public boolean clipChildren; - public Node sibling; - public Node children; + public ViewPropertyRef next; - public ViewNode toProto(Resources res) { + public void transfer(ViewRef viewRef) { + childCount = viewRef.childCount; + + View view = viewRef.view; + viewRef.view = null; + + clazz = view.getClass(); + hashCode = view.hashCode(); + id = view.getId(); + left = view.getLeft(); + top = view.getTop(); + right = view.getRight(); + bottom = view.getBottom(); + scrollX = view.getScrollX(); + scrollY = view.getScrollY(); + + translateX = view.getTranslationX(); + translateY = view.getTranslationY(); + scaleX = view.getScaleX(); + scaleY = view.getScaleY(); + alpha = view.getAlpha(); + + visibility = view.getVisibility(); + willNotDraw = view.willNotDraw(); + } + + /** + * Converts the data to the proto representation and returns the next property ref + * at the end of the iteration. + * @param res + * @return + */ + public ViewPropertyRef toProto(Resources res, ViewNode.Builder outBuilder) { String resolvedId; if (id >= 0) { try { @@ -191,9 +290,7 @@ public class ViewCapture implements OnDrawListener { } else { resolvedId = "NO_ID"; } - - ViewNode.Builder result = ViewNode.newBuilder() - .setClassname(clazz.getName() + "@" + hashCode) + outBuilder.setClassname(clazz.getName() + "@" + hashCode) .setId(resolvedId) .setLeft(left) .setTop(top) @@ -207,13 +304,20 @@ public class ViewCapture implements OnDrawListener { .setVisibility(visibility) .setWillNotDraw(willNotDraw) .setClipChildren(clipChildren); - Node child = children; - while (child != null) { - result.addChildren(child.toProto(res)); - child = child.sibling; - } - return result.build(); - } + ViewPropertyRef result = next; + for (int i = 0; (i < childCount) && (result != null); i++) { + ViewNode.Builder childBuilder = ViewNode.newBuilder(); + result = result.toProto(res, childBuilder); + outBuilder.addChildren(childBuilder); + } + return result; + } + } + + private static class ViewRef { + public View view; + public int childCount = 0; + public ViewRef next; } }