• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.app.viewcapture;
18 
19 import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_H;
20 import static com.android.app.viewcapture.data.ExportedData.MagicNumber.MAGIC_NUMBER_L;
21 
22 import android.content.Context;
23 import android.content.res.Resources;
24 import android.media.permission.SafeCloseable;
25 import android.os.HandlerThread;
26 import android.os.Looper;
27 import android.os.SystemClock;
28 import android.os.Trace;
29 import android.text.TextUtils;
30 import android.util.SparseArray;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewTreeObserver;
34 import android.view.Window;
35 
36 import androidx.annotation.AnyThread;
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.UiThread;
40 import androidx.annotation.VisibleForTesting;
41 import androidx.annotation.WorkerThread;
42 
43 import com.android.app.viewcapture.data.ExportedData;
44 import com.android.app.viewcapture.data.FrameData;
45 import com.android.app.viewcapture.data.MotionWindowData;
46 import com.android.app.viewcapture.data.ViewNode;
47 import com.android.app.viewcapture.data.WindowData;
48 
49 import java.io.IOException;
50 import java.io.OutputStream;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Optional;
54 import java.util.concurrent.CompletableFuture;
55 import java.util.concurrent.ExecutionException;
56 import java.util.concurrent.Executor;
57 import java.util.concurrent.TimeUnit;
58 import java.util.function.Consumer;
59 import java.util.function.Predicate;
60 
61 /**
62  * Utility class for capturing view data every frame
63  */
64 public abstract class ViewCapture {
65 
66     private static final String TAG = "ViewCapture";
67 
68     // These flags are copies of two private flags in the View class.
69     private static final int PFLAG_INVALIDATED = 0x80000000;
70     private static final int PFLAG_DIRTY_MASK = 0x00200000;
71 
72     private static final long MAGIC_NUMBER_FOR_WINSCOPE =
73             ((long) MAGIC_NUMBER_H.getNumber() << 32) | MAGIC_NUMBER_L.getNumber();
74 
75     // Number of frames to keep in memory
76     private final int mMemorySize;
77     protected static final int DEFAULT_MEMORY_SIZE = 2000;
78     // Initial size of the reference pool. This is at least be 5 * total number of views in
79     // Launcher. This allows the first free frames avoid object allocation during view capture.
80     protected static final int DEFAULT_INIT_POOL_SIZE = 300;
81 
82     public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());
83 
84     private final List<WindowListener> mListeners = new ArrayList<>();
85 
86     protected final Executor mBgExecutor;
87 
88     // Pool used for capturing view tree on the UI thread.
89     private ViewRef mPool = new ViewRef();
90     private boolean mIsEnabled = true;
91 
ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor)92     protected ViewCapture(int memorySize, int initPoolSize, Executor bgExecutor) {
93         mMemorySize = memorySize;
94         mBgExecutor = bgExecutor;
95         mBgExecutor.execute(() -> initPool(initPoolSize));
96     }
97 
createAndStartNewLooperExecutor(String name, int priority)98     public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
99         HandlerThread thread = new HandlerThread(name, priority);
100         thread.start();
101         return new LooperExecutor(thread.getLooper());
102     }
103 
104     @UiThread
addToPool(ViewRef start, ViewRef end)105     private void addToPool(ViewRef start, ViewRef end) {
106         end.next = mPool;
107         mPool = start;
108     }
109 
110     @WorkerThread
initPool(int initPoolSize)111     private void initPool(int initPoolSize) {
112         ViewRef start = new ViewRef();
113         ViewRef current = start;
114 
115         for (int i = 0; i < initPoolSize; i++) {
116             current.next = new ViewRef();
117             current = current.next;
118         }
119 
120         ViewRef finalCurrent = current;
121         MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent));
122     }
123 
124     /**
125      * Attaches the ViewCapture to the provided window and returns a handle to detach the listener
126      */
127     @NonNull
startCapture(Window window)128     public SafeCloseable startCapture(Window window) {
129         String title = window.getAttributes().getTitle().toString();
130         String name = TextUtils.isEmpty(title) ? window.toString() : title;
131         return startCapture(window.getDecorView(), name);
132     }
133 
134     /**
135      * Attaches the ViewCapture to the provided window and returns a handle to detach the listener.
136      * Verifies that ViewCapture is enabled before actually attaching an onDrawListener.
137      */
138     @NonNull
startCapture(View view, String name)139     public SafeCloseable startCapture(View view, String name) {
140         WindowListener listener = new WindowListener(view, name);
141         if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot);
142         mListeners.add(listener);
143         return () -> {
144             mListeners.remove(listener);
145             listener.detachFromRoot();
146         };
147     }
148 
149     /**
150      * Launcher checks for leaks in many spots during its instrumented tests. The WindowListeners
151      * appear to have leaks because they store mRoot views. In reality, attached views close their
152      * respective window listeners when they are destroyed.
153      * <p>
154      * This method deletes detaches and deletes mRoot views from windowListeners. This makes the
155      * WindowListeners unusable for anything except dumping previously captured information. They
156      * are still technically enabled to allow for dumping.
157      */
158     @VisibleForTesting
stopCapture(@onNull View rootView)159     public void stopCapture(@NonNull View rootView) {
160         mListeners.forEach(it -> {
161             if (rootView == it.mRoot) {
162                 it.mRoot.getViewTreeObserver().removeOnDrawListener(it);
163                 it.mRoot = null;
164             }
165         });
166     }
167 
168     @UiThread
enableOrDisableWindowListeners(boolean isEnabled)169     protected void enableOrDisableWindowListeners(boolean isEnabled) {
170         mIsEnabled = isEnabled;
171         mListeners.forEach(WindowListener::detachFromRoot);
172         if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot);
173     }
174 
175     @AnyThread
dumpTo(OutputStream os, Context context)176     public void dumpTo(OutputStream os, Context context)
177             throws InterruptedException, ExecutionException, IOException {
178         if (mIsEnabled) getExportedData(context).writeTo(os);
179     }
180 
181     @VisibleForTesting
getExportedData(Context context)182     public ExportedData getExportedData(Context context)
183             throws InterruptedException, ExecutionException {
184         ArrayList<Class> classList = new ArrayList<>();
185         return ExportedData.newBuilder()
186                 .setMagicNumber(MAGIC_NUMBER_FOR_WINSCOPE)
187                 .setPackage(context.getPackageName())
188                 .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get())
189                 .addAllClassname(toStringList(classList))
190                 .setRealToElapsedTimeOffsetNanos(TimeUnit.MILLISECONDS
191                         .toNanos(System.currentTimeMillis()) - SystemClock.elapsedRealtimeNanos())
192                 .build();
193     }
194 
toStringList(List<Class> classList)195     private static List<String> toStringList(List<Class> classList) {
196         return classList.stream().map(Class::getName).toList();
197     }
198 
getDumpTask(View view)199     public CompletableFuture<Optional<MotionWindowData>> getDumpTask(View view) {
200         ArrayList<Class> classList = new ArrayList<>();
201         return getWindowData(view.getContext().getApplicationContext(), classList,
202                 l -> l.mRoot.equals(view)).thenApply(list -> list.stream().findFirst().map(w ->
203                 MotionWindowData.newBuilder()
204                         .addAllFrameData(w.getFrameDataList())
205                         .addAllClassname(toStringList(classList))
206                         .build()));
207     }
208 
209     @AnyThread
getWindowData(Context context, ArrayList<Class> outClassList, Predicate<WindowListener> filter)210     private CompletableFuture<List<WindowData>> getWindowData(Context context,
211             ArrayList<Class> outClassList, Predicate<WindowListener> filter) {
212         ViewIdProvider idProvider = new ViewIdProvider(context.getResources());
213         return CompletableFuture.supplyAsync(() ->
214                 mListeners.stream().filter(filter).toList(), MAIN_EXECUTOR).thenApplyAsync(it ->
215                         it.stream().map(l -> l.dumpToProto(idProvider, outClassList)).toList(),
216                 mBgExecutor);
217     }
218 
219 
220     /**
221      * Once this window listener is attached to a window's root view, it traverses the entire
222      * view tree on the main thread every time onDraw is called. It then saves the state of the view
223      * tree traversed in a local list of nodes, so that this list of nodes can be processed on a
224      * background thread, and prepared for being dumped into a bugreport.
225      *
226      * Since some of the work needs to be done on the main thread after every draw, this piece of
227      * code needs to be hyper optimized. That is why we are recycling ViewRef and ViewPropertyRef
228      * objects and storing the list of nodes as a flat LinkedList, rather than as a tree. This data
229      * structure allows recycling to happen in O(1) time via pointer assignment. Without this
230      * optimization, a lot of time is wasted creating ViewRef objects, or finding ViewRef objects to
231      * recycle.
232      *
233      * Another optimization is to only traverse view nodes on the main thread that have potentially
234      * changed since the last frame was drawn. This can be determined via a combination of private
235      * flags inside the View class.
236      *
237      * Another optimization is to not store or manipulate any string objects on the main thread.
238      * While this might seem trivial, using Strings in any form causes the ViewCapture to hog the
239      * main thread for up to an additional 6-7ms. It must be avoided at all costs.
240      *
241      * Another optimization is to only store the class names of the Views in the view hierarchy one
242      * time. They are then referenced via a classNameIndex value stored in each ViewPropertyRef.
243      *
244      * TODO: b/262585897: If further memory optimization is required, an effective one would be to
245      * only store the changes between frames, rather than the entire node tree for each frame.
246      * The go/web-hv UX already does this, and has reaped significant memory improves because of it.
247      *
248      * TODO: b/262585897: Another memory optimization could be to store all integer, float, and
249      * boolean information via single integer values via the Chinese remainder theorem, or a similar
250      * algorithm, which enables multiple numerical values to be stored inside 1 number. Doing this
251      * would allow each ViewProperty / ViewRef to slim down its memory footprint significantly.
252      *
253      * One important thing to remember is that bugs related to recycling will usually only appear
254      * after at least 2000 frames have been rendered. If that code is changed, the tester can
255      * use hard-coded logs to verify that recycling is happening, and test view capturing at least
256      * ~8000 frames or so to verify the recycling functionality is working properly.
257      */
258     private class WindowListener implements ViewTreeObserver.OnDrawListener {
259 
260         @Nullable // Nullable in tests only
261         public View mRoot;
262         public final String name;
263 
264         private final ViewRef mViewRef = new ViewRef();
265 
266         private int mFrameIndexBg = -1;
267         private boolean mIsFirstFrame = true;
268         private final long[] mFrameTimesNanosBg = new long[mMemorySize];
269         private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize];
270 
271         private boolean mIsActive = true;
272         private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg;
273 
WindowListener(View view, String name)274         WindowListener(View view, String name) {
275             mRoot = view;
276             this.name = name;
277         }
278 
279         /**
280          * Every time onDraw is called, it does the minimal set of work required on the main thread,
281          * i.e. capturing potentially dirty / invalidated views, and then immediately offloads the
282          * rest of the processing work (extracting the captured view properties) to a background
283          * thread via mExecutor.
284          */
285         @Override
onDraw()286         public void onDraw() {
287             Trace.beginSection("view_capture");
288             captureViewTree(mRoot, mViewRef);
289             ViewRef captured = mViewRef.next;
290             if (captured != null) {
291                 captured.callback = mCaptureCallback;
292                 captured.elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos();
293                 mBgExecutor.execute(captured);
294             }
295             mIsFirstFrame = false;
296             Trace.endSection();
297         }
298 
299         /**
300          * Captures the View property on the background thread, and transfer all the ViewRef objects
301          * back to the pool
302          */
303         @WorkerThread
captureViewPropertiesBg(ViewRef viewRefStart)304         private void captureViewPropertiesBg(ViewRef viewRefStart) {
305             long elapsedRealtimeNanos = viewRefStart.elapsedRealtimeNanos;
306             mFrameIndexBg++;
307             if (mFrameIndexBg >= mMemorySize) {
308                 mFrameIndexBg = 0;
309             }
310             mFrameTimesNanosBg[mFrameIndexBg] = elapsedRealtimeNanos;
311 
312             ViewPropertyRef recycle = mNodesBg[mFrameIndexBg];
313 
314             ViewPropertyRef resultStart = null;
315             ViewPropertyRef resultEnd = null;
316 
317             ViewRef viewRefEnd = viewRefStart;
318             while (viewRefEnd != null) {
319                 ViewPropertyRef propertyRef = recycle;
320                 if (propertyRef == null) {
321                     propertyRef = new ViewPropertyRef();
322                 } else {
323                     recycle = recycle.next;
324                     propertyRef.next = null;
325                 }
326 
327                 ViewPropertyRef copy = null;
328                 if (viewRefEnd.childCount < 0) {
329                     copy = findInLastFrame(viewRefEnd.view.hashCode());
330                     viewRefEnd.childCount = (copy != null) ? copy.childCount : 0;
331                 }
332                 viewRefEnd.transferTo(propertyRef);
333 
334                 if (resultStart == null) {
335                     resultStart = propertyRef;
336                     resultEnd = resultStart;
337                 } else {
338                     resultEnd.next = propertyRef;
339                     resultEnd = resultEnd.next;
340                 }
341 
342                 if (copy != null) {
343                     int pending = copy.childCount;
344                     while (pending > 0) {
345                         copy = copy.next;
346                         pending = pending - 1 + copy.childCount;
347 
348                         propertyRef = recycle;
349                         if (propertyRef == null) {
350                             propertyRef = new ViewPropertyRef();
351                         } else {
352                             recycle = recycle.next;
353                             propertyRef.next = null;
354                         }
355 
356                         copy.transferTo(propertyRef);
357 
358                         resultEnd.next = propertyRef;
359                         resultEnd = resultEnd.next;
360                     }
361                 }
362 
363                 if (viewRefEnd.next == null) {
364                     // The compiler will complain about using a non-final variable from
365                     // an outer class in a lambda if we pass in viewRefEnd directly.
366                     final ViewRef finalViewRefEnd = viewRefEnd;
367                     MAIN_EXECUTOR.execute(() -> addToPool(viewRefStart, finalViewRefEnd));
368                     break;
369                 }
370                 viewRefEnd = viewRefEnd.next;
371             }
372             mNodesBg[mFrameIndexBg] = resultStart;
373         }
374 
findInLastFrame(int hashCode)375         private @Nullable ViewPropertyRef findInLastFrame(int hashCode) {
376             int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1;
377             ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex];
378             while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) {
379                 viewPropertyRef = viewPropertyRef.next;
380             }
381             return viewPropertyRef;
382         }
383 
attachToRoot()384         void attachToRoot() {
385             mIsActive = true;
386             if (mRoot.isAttachedToWindow()) {
387                 safelyEnableOnDrawListener();
388             } else {
389                 mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
390                     @Override
391                     public void onViewAttachedToWindow(View v) {
392                         if (mIsActive) {
393                             safelyEnableOnDrawListener();
394                         }
395                         mRoot.removeOnAttachStateChangeListener(this);
396                     }
397 
398                     @Override
399                     public void onViewDetachedFromWindow(View v) {
400                     }
401                 });
402             }
403         }
404 
detachFromRoot()405         void detachFromRoot() {
406             mIsActive = false;
407             if (mRoot != null) {
408                 mRoot.getViewTreeObserver().removeOnDrawListener(this);
409             }
410         }
411 
safelyEnableOnDrawListener()412         private void safelyEnableOnDrawListener() {
413             mRoot.getViewTreeObserver().removeOnDrawListener(this);
414             mRoot.getViewTreeObserver().addOnDrawListener(this);
415         }
416 
417         @WorkerThread
dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList)418         private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList) {
419             WindowData.Builder builder = WindowData.newBuilder().setTitle(name);
420             int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize;
421             for (int i = size - 1; i >= 0; i--) {
422                 int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize;
423                 ViewNode.Builder nodeBuilder = ViewNode.newBuilder();
424                 mNodesBg[index].toProto(idProvider, classList, nodeBuilder);
425                 FrameData.Builder frameDataBuilder = FrameData.newBuilder()
426                         .setNode(nodeBuilder)
427                         .setTimestamp(mFrameTimesNanosBg[index]);
428                 builder.addFrameData(frameDataBuilder);
429             }
430             return builder.build();
431         }
432 
captureViewTree(View view, ViewRef start)433         private ViewRef captureViewTree(View view, ViewRef start) {
434             ViewRef ref;
435             if (mPool != null) {
436                 ref = mPool;
437                 mPool = mPool.next;
438                 ref.next = null;
439             } else {
440                 ref = new ViewRef();
441             }
442             ref.view = view;
443             start.next = ref;
444             if (view instanceof ViewGroup) {
445                 ViewGroup parent = (ViewGroup) view;
446                 // If a view has not changed since the last frame, we will copy
447                 // its children from the last processed frame's data.
448                 if ((view.mPrivateFlags & (PFLAG_INVALIDATED | PFLAG_DIRTY_MASK)) == 0
449                         && !mIsFirstFrame) {
450                     // A negative child count is the signal to copy this view from the last frame.
451                     ref.childCount = -parent.getChildCount();
452                     return ref;
453                 }
454                 ViewRef result = ref;
455                 int childCount = ref.childCount = parent.getChildCount();
456                 for (int i = 0; i < childCount; i++) {
457                     result = captureViewTree(parent.getChildAt(i), result);
458                 }
459                 return result;
460             } else {
461                 ref.childCount = 0;
462                 return ref;
463             }
464         }
465     }
466 
467     private static class ViewPropertyRef {
468         // We store reference in memory to avoid generating and storing too many strings
469         public Class clazz;
470         public int hashCode;
471         public int childCount = 0;
472 
473         public int id;
474         public int left, top, right, bottom;
475         public int scrollX, scrollY;
476 
477         public float translateX, translateY;
478         public float scaleX, scaleY;
479         public float alpha;
480         public float elevation;
481 
482         public int visibility;
483         public boolean willNotDraw;
484         public boolean clipChildren;
485 
486         public ViewPropertyRef next;
487 
transferTo(ViewPropertyRef out)488         public void transferTo(ViewPropertyRef out) {
489             out.clazz = this.clazz;
490             out.hashCode = this.hashCode;
491             out.childCount = this.childCount;
492             out.id = this.id;
493             out.left = this.left;
494             out.top = this.top;
495             out.right = this.right;
496             out.bottom = this.bottom;
497             out.scrollX = this.scrollX;
498             out.scrollY = this.scrollY;
499             out.scaleX = this.scaleX;
500             out.scaleY = this.scaleY;
501             out.translateX = this.translateX;
502             out.translateY = this.translateY;
503             out.alpha = this.alpha;
504             out.visibility = this.visibility;
505             out.willNotDraw = this.willNotDraw;
506             out.clipChildren = this.clipChildren;
507             out.elevation = this.elevation;
508         }
509 
510         /**
511          * Converts the data to the proto representation and returns the next property ref
512          * at the end of the iteration.
513          */
toProto(ViewIdProvider idProvider, ArrayList<Class> classList, ViewNode.Builder viewNode)514         public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList,
515                 ViewNode.Builder viewNode) {
516             int classnameIndex = classList.indexOf(clazz);
517             if (classnameIndex < 0) {
518                 classnameIndex = classList.size();
519                 classList.add(clazz);
520             }
521 
522             viewNode.setClassnameIndex(classnameIndex)
523                     .setHashcode(hashCode)
524                     .setId(idProvider.getName(id))
525                     .setLeft(left)
526                     .setTop(top)
527                     .setWidth(right - left)
528                     .setHeight(bottom - top)
529                     .setTranslationX(translateX)
530                     .setTranslationY(translateY)
531                     .setScrollX(scrollX)
532                     .setScrollY(scrollY)
533                     .setScaleX(scaleX)
534                     .setScaleY(scaleY)
535                     .setAlpha(alpha)
536                     .setVisibility(visibility)
537                     .setWillNotDraw(willNotDraw)
538                     .setElevation(elevation)
539                     .setClipChildren(clipChildren);
540 
541             ViewPropertyRef result = next;
542             for (int i = 0; (i < childCount) && (result != null); i++) {
543                 ViewNode.Builder childViewNode = ViewNode.newBuilder();
544                 result = result.toProto(idProvider, classList, childViewNode);
545                 viewNode.addChildren(childViewNode);
546             }
547             return result;
548         }
549     }
550 
551 
552     private static class ViewRef implements Runnable {
553         public View view;
554         public int childCount = 0;
555         public ViewRef next;
556 
557         public Consumer<ViewRef> callback = null;
558         public long elapsedRealtimeNanos = 0;
559 
transferTo(ViewPropertyRef out)560         public void transferTo(ViewPropertyRef out) {
561             out.childCount = this.childCount;
562 
563             View view = this.view;
564             this.view = null;
565 
566             out.clazz = view.getClass();
567             out.hashCode = view.hashCode();
568             out.id = view.getId();
569             out.left = view.getLeft();
570             out.top = view.getTop();
571             out.right = view.getRight();
572             out.bottom = view.getBottom();
573             out.scrollX = view.getScrollX();
574             out.scrollY = view.getScrollY();
575 
576             out.translateX = view.getTranslationX();
577             out.translateY = view.getTranslationY();
578             out.scaleX = view.getScaleX();
579             out.scaleY = view.getScaleY();
580             out.alpha = view.getAlpha();
581             out.elevation = view.getElevation();
582 
583             out.visibility = view.getVisibility();
584             out.willNotDraw = view.willNotDraw();
585         }
586 
587         @Override
run()588         public void run() {
589             Consumer<ViewRef> oldCallback = callback;
590             callback = null;
591             if (oldCallback != null) {
592                 oldCallback.accept(this);
593             }
594         }
595     }
596 
597     private static final class ViewIdProvider {
598 
599         private final SparseArray<String> mNames = new SparseArray<>();
600         private final Resources mRes;
601 
ViewIdProvider(Resources res)602         ViewIdProvider(Resources res) {
603             mRes = res;
604         }
605 
getName(int id)606         String getName(int id) {
607             String name = mNames.get(id);
608             if (name == null) {
609                 if (id >= 0) {
610                     try {
611                         name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id);
612                     } catch (Resources.NotFoundException e) {
613                         name = "id/" + "0x" + Integer.toHexString(id).toUpperCase();
614                     }
615                 } else {
616                     name = "NO_ID";
617                 }
618                 mNames.put(id, name);
619             }
620             return name;
621         }
622     }
623 }
624