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