Loading protos/view_capture.proto 0 → 100644 +54 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ syntax = "proto2"; package com.android.launcher3.view; option java_outer_classname = "ViewCaptureData"; message ExportedData { repeated FrameData frameData = 1; } message FrameData { optional int64 timestamp = 1; optional ViewNode node = 2; } message ViewNode { optional string classname = 1; optional string id = 2; optional int32 left = 3; optional int32 top = 4; optional int32 width = 5; optional int32 height = 6; optional int32 scrollX = 7; optional int32 scrollY = 8; optional float translationX = 9; optional float translationY = 10; optional float scaleX = 11 [default = 1]; optional float scaleY = 12 [default = 1]; optional float alpha = 13 [default = 1]; optional bool willNotDraw = 14; optional bool clipChildren = 15; optional int32 visibility = 16; repeated ViewNode children = 17; } src/com/android/launcher3/Launcher.java +14 −0 Original line number Diff line number Diff line Loading @@ -194,6 +194,7 @@ import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.FloatingIconView; Loading Loading @@ -388,6 +389,7 @@ public class Launcher extends StatefulActivity<LauncherState> private LauncherState mPrevLauncherState; private StringCache mStringCache; private ViewCapture mViewCapture; @Override @TargetApi(Build.VERSION_CODES.S) Loading Loading @@ -1478,6 +1480,14 @@ public class Launcher extends StatefulActivity<LauncherState> public void onAttachedToWindow() { super.onAttachedToWindow(); mOverlayManager.onAttachedToWindow(); if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { View root = getDragLayer().getRootView(); if (mViewCapture != null) { root.getViewTreeObserver().removeOnDrawListener(mViewCapture); } mViewCapture = new ViewCapture(root); root.getViewTreeObserver().addOnDrawListener(mViewCapture); } } @Override Loading Loading @@ -2997,6 +3007,10 @@ public class Launcher extends StatefulActivity<LauncherState> writer.println(prefix + "\tmRotationHelper: " + mRotationHelper); writer.println(prefix + "\tmAppWidgetHost.isListening: " + mAppWidgetHost.isListening()); if (mViewCapture != null) { writer.println(prefix + "\tmViewCapture: " + mViewCapture.dumpToString()); } // Extra logging for general debugging mDragLayer.dump(prefix, writer); mStateManager.dump(prefix, writer); Loading src/com/android/launcher3/config/FeatureFlags.java +3 −0 Original line number Diff line number Diff line Loading @@ -285,6 +285,9 @@ public final class FeatureFlags { "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", false, "Use local overrides for search request timeout"); public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag( "CONTINUOUS_VIEW_TREE_CAPTURE", false, "Capture View tree every frame"); public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { Loading src/com/android/launcher3/util/ViewCapture.java 0 → 100644 +212 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.util; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.os.Trace; import android.util.Base64; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnDrawListener; import androidx.annotation.UiThread; import com.android.launcher3.view.ViewCaptureData.ExportedData; import com.android.launcher3.view.ViewCaptureData.FrameData; import com.android.launcher3.view.ViewCaptureData.ViewNode; import java.util.concurrent.FutureTask; /** * Utility class for capturing view data every frame */ public class ViewCapture implements OnDrawListener { private static final String TAG = "ViewCapture"; private static final int MEMORY_SIZE = 2000; private final View mRoot; private final long[] mFrameTimes = new long[MEMORY_SIZE]; private final Node[] mNodes = new Node[MEMORY_SIZE]; private int mFrameIndex = -1; /** * @param root the root view for the capture data */ public ViewCapture(View root) { mRoot = root; } @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]); Trace.endSection(); } /** * Creates a proto of all the data captured so far. */ public String dumpToString() { 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); } try { return Base64.encodeToString(task.get().toByteArray(), Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP); } catch (Exception e) { Log.e(TAG, "Error capturing proto", e); return "--error--"; } } @UiThread private ExportedData dumpToProtoUI() { ExportedData.Builder dataBuilder = ExportedData.newBuilder(); Resources res = mRoot.getResources(); int size = (mNodes[MEMORY_SIZE - 1] == null) ? mFrameIndex + 1 : MEMORY_SIZE; for (int i = size - 1; i >= 0; i--) { int index = (MEMORY_SIZE + mFrameIndex - i) % MEMORY_SIZE; dataBuilder.addFrameData(FrameData.newBuilder() .setNode(mNodes[index].toProto(res)) .setTimestamp(mFrameTimes[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; } } else { result.clipChildren = false; result.children = null; } return result; } private static class Node { // We store reference in memory to avoid generating and storing too many strings public Class clazz; public int hashCode; public int id; public int left, top, right, bottom; public int scrollX, scrollY; public float translateX, translateY; public float scaleX, scaleY; public float alpha; public int visibility; public boolean willNotDraw; public boolean clipChildren; public Node sibling; public Node children; public ViewNode toProto(Resources res) { String resolvedId; if (id >= 0) { try { resolvedId = res.getResourceTypeName(id) + '/' + res.getResourceEntryName(id); } catch (Resources.NotFoundException e) { resolvedId = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); } } else { resolvedId = "NO_ID"; } ViewNode.Builder result = ViewNode.newBuilder() .setClassname(clazz.getName() + "@" + hashCode) .setId(resolvedId) .setLeft(left) .setTop(top) .setWidth(right - left) .setHeight(bottom - top) .setTranslationX(translateX) .setTranslationY(translateY) .setScaleX(scaleX) .setScaleY(scaleY) .setAlpha(alpha) .setVisibility(visibility) .setWillNotDraw(willNotDraw) .setClipChildren(clipChildren); Node child = children; while (child != null) { result.addChildren(child.toProto(res)); child = child.sibling; } return result.build(); } } } Loading
protos/view_capture.proto 0 → 100644 +54 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ syntax = "proto2"; package com.android.launcher3.view; option java_outer_classname = "ViewCaptureData"; message ExportedData { repeated FrameData frameData = 1; } message FrameData { optional int64 timestamp = 1; optional ViewNode node = 2; } message ViewNode { optional string classname = 1; optional string id = 2; optional int32 left = 3; optional int32 top = 4; optional int32 width = 5; optional int32 height = 6; optional int32 scrollX = 7; optional int32 scrollY = 8; optional float translationX = 9; optional float translationY = 10; optional float scaleX = 11 [default = 1]; optional float scaleY = 12 [default = 1]; optional float alpha = 13 [default = 1]; optional bool willNotDraw = 14; optional bool clipChildren = 15; optional int32 visibility = 16; repeated ViewNode children = 17; }
src/com/android/launcher3/Launcher.java +14 −0 Original line number Diff line number Diff line Loading @@ -194,6 +194,7 @@ import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; import com.android.launcher3.util.TraceHelper; import com.android.launcher3.util.UiThreadHelper; import com.android.launcher3.util.ViewCapture; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.FloatingIconView; Loading Loading @@ -388,6 +389,7 @@ public class Launcher extends StatefulActivity<LauncherState> private LauncherState mPrevLauncherState; private StringCache mStringCache; private ViewCapture mViewCapture; @Override @TargetApi(Build.VERSION_CODES.S) Loading Loading @@ -1478,6 +1480,14 @@ public class Launcher extends StatefulActivity<LauncherState> public void onAttachedToWindow() { super.onAttachedToWindow(); mOverlayManager.onAttachedToWindow(); if (FeatureFlags.CONTINUOUS_VIEW_TREE_CAPTURE.get()) { View root = getDragLayer().getRootView(); if (mViewCapture != null) { root.getViewTreeObserver().removeOnDrawListener(mViewCapture); } mViewCapture = new ViewCapture(root); root.getViewTreeObserver().addOnDrawListener(mViewCapture); } } @Override Loading Loading @@ -2997,6 +3007,10 @@ public class Launcher extends StatefulActivity<LauncherState> writer.println(prefix + "\tmRotationHelper: " + mRotationHelper); writer.println(prefix + "\tmAppWidgetHost.isListening: " + mAppWidgetHost.isListening()); if (mViewCapture != null) { writer.println(prefix + "\tmViewCapture: " + mViewCapture.dumpToString()); } // Extra logging for general debugging mDragLayer.dump(prefix, writer); mStateManager.dump(prefix, writer); Loading
src/com/android/launcher3/config/FeatureFlags.java +3 −0 Original line number Diff line number Diff line Loading @@ -285,6 +285,9 @@ public final class FeatureFlags { "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", false, "Use local overrides for search request timeout"); public static final BooleanFlag CONTINUOUS_VIEW_TREE_CAPTURE = getDebugFlag( "CONTINUOUS_VIEW_TREE_CAPTURE", false, "Capture View tree every frame"); public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { Loading
src/com/android/launcher3/util/ViewCapture.java 0 → 100644 +212 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.util; import android.content.res.Resources; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.os.Trace; import android.util.Base64; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver.OnDrawListener; import androidx.annotation.UiThread; import com.android.launcher3.view.ViewCaptureData.ExportedData; import com.android.launcher3.view.ViewCaptureData.FrameData; import com.android.launcher3.view.ViewCaptureData.ViewNode; import java.util.concurrent.FutureTask; /** * Utility class for capturing view data every frame */ public class ViewCapture implements OnDrawListener { private static final String TAG = "ViewCapture"; private static final int MEMORY_SIZE = 2000; private final View mRoot; private final long[] mFrameTimes = new long[MEMORY_SIZE]; private final Node[] mNodes = new Node[MEMORY_SIZE]; private int mFrameIndex = -1; /** * @param root the root view for the capture data */ public ViewCapture(View root) { mRoot = root; } @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]); Trace.endSection(); } /** * Creates a proto of all the data captured so far. */ public String dumpToString() { 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); } try { return Base64.encodeToString(task.get().toByteArray(), Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP); } catch (Exception e) { Log.e(TAG, "Error capturing proto", e); return "--error--"; } } @UiThread private ExportedData dumpToProtoUI() { ExportedData.Builder dataBuilder = ExportedData.newBuilder(); Resources res = mRoot.getResources(); int size = (mNodes[MEMORY_SIZE - 1] == null) ? mFrameIndex + 1 : MEMORY_SIZE; for (int i = size - 1; i >= 0; i--) { int index = (MEMORY_SIZE + mFrameIndex - i) % MEMORY_SIZE; dataBuilder.addFrameData(FrameData.newBuilder() .setNode(mNodes[index].toProto(res)) .setTimestamp(mFrameTimes[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; } } else { result.clipChildren = false; result.children = null; } return result; } private static class Node { // We store reference in memory to avoid generating and storing too many strings public Class clazz; public int hashCode; public int id; public int left, top, right, bottom; public int scrollX, scrollY; public float translateX, translateY; public float scaleX, scaleY; public float alpha; public int visibility; public boolean willNotDraw; public boolean clipChildren; public Node sibling; public Node children; public ViewNode toProto(Resources res) { String resolvedId; if (id >= 0) { try { resolvedId = res.getResourceTypeName(id) + '/' + res.getResourceEntryName(id); } catch (Resources.NotFoundException e) { resolvedId = "id/" + "0x" + Integer.toHexString(id).toUpperCase(); } } else { resolvedId = "NO_ID"; } ViewNode.Builder result = ViewNode.newBuilder() .setClassname(clazz.getName() + "@" + hashCode) .setId(resolvedId) .setLeft(left) .setTop(top) .setWidth(right - left) .setHeight(bottom - top) .setTranslationX(translateX) .setTranslationY(translateY) .setScaleX(scaleX) .setScaleY(scaleY) .setAlpha(alpha) .setVisibility(visibility) .setWillNotDraw(willNotDraw) .setClipChildren(clipChildren); Node child = children; while (child != null) { result.addChildren(child.toProto(res)); child = child.sibling; } return result.build(); } } }