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
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 5ee9aa8..05536d3 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1505,7 +1505,7 @@
                 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 cf9ea69..cf4e84a 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.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 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 @@
 
     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<ExportedData> task = new FutureTask<>(this::dumpToProtoUI);
-        if (Looper.myLooper() == handler.getLooper()) {
-            task.run();
-        } else {
-            handler.post(task);
-        }
+        Future<ExportedData> 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 @@
         }
     }
 
-    @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();
         }
-        return result;
+        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;
+        }
     }
 
-    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 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 @@
             } 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 @@
                     .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;
     }
 }