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