Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a38aa060 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

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
parent fa13629d
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -1505,7 +1505,7 @@ public class Launcher extends StatefulActivity<LauncherState>
                root.getViewTreeObserver().removeOnDrawListener(mViewCapture);
            }
            mViewCapture = new ViewCapture(root);
            root.getViewTreeObserver().addOnDrawListener(mViewCapture);
            mViewCapture.attach();
        }
    }

+184 −80
Original line number Diff line number Diff line
@@ -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 final Handler mHandler;
    private final ViewRef mViewRef = new ViewRef();

    private int mFrameIndex = -1;
    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();
    }

    /**
     * Creates a proto of all the data captured so far.
     * Captures the View property on the background thread, and transfer all the ViewRef objects
     * back to the pool
     */
    public void dump(FileDescriptor out) {
        Handler handler = mRoot.getHandler();
        if (handler == null) {
            handler = Executors.MAIN_EXECUTOR.getHandler();
    @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;
            }
        FutureTask<ExportedData> task = new FutureTask<>(this::dumpToProtoUI);
        if (Looper.myLooper() == handler.getLooper()) {
            task.run();

            propertyRef.transfer(current);
            last = current;
            current = current.next;

            if (result == null) {
                result = propertyRef;
                resultEnd = result;
            } else {
            handler.post(task);
                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) {
        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 @@ 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;
    private ViewRef captureViewTree(View view, ViewRef start) {
        ViewRef ref;
        if (mPool != null) {
            ref = mPool;
            mPool = mPool.next;
            ref.next = 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;
            ref = new ViewRef();
        }
                lastChild.sibling = null;
        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 {
            result.clipChildren = false;
            result.children = null;
            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 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();
        }

        public ViewNode toProto(Resources res) {
        /**
         * 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;

            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;
        }
            return result.build();
    }

    private static class ViewRef {
        public View view;
        public int childCount = 0;
        public ViewRef next;
    }
}