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