1 /* 2 * Copyright (C) 2018 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.server.wm.flicker; 18 19 import androidx.annotation.Nullable; 20 21 import android.graphics.Rect; 22 import android.surfaceflinger.nano.Layers.LayerProto; 23 import android.surfaceflinger.nano.Layers.RectProto; 24 import android.surfaceflinger.nano.Layers.RegionProto; 25 import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto; 26 import android.surfaceflinger.nano.Layerstrace.LayersTraceProto; 27 import android.util.SparseArray; 28 29 import com.android.server.wm.flicker.Assertions.Result; 30 31 import java.nio.file.Path; 32 import java.util.ArrayList; 33 import java.util.Collection; 34 import java.util.List; 35 import java.util.Optional; 36 import java.util.function.Consumer; 37 import java.util.stream.Collectors; 38 39 /** 40 * Contains a collection of parsed Layers trace entries and assertions to apply over a single entry. 41 * 42 * <p>Each entry is parsed into a list of {@link LayersTrace.Entry} objects. 43 */ 44 public class LayersTrace { 45 private final List<Entry> mEntries; 46 @Nullable private final Path mSource; 47 @Nullable private final String mSourceChecksum; 48 LayersTrace(List<Entry> entries, Path source, String sourceChecksum)49 private LayersTrace(List<Entry> entries, Path source, String sourceChecksum) { 50 this.mEntries = entries; 51 this.mSource = source; 52 this.mSourceChecksum = sourceChecksum; 53 } 54 55 /** 56 * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list 57 * of trace entries, storing the flattened layers into its hierarchical structure. 58 * 59 * @param data binary proto data 60 * @param source Path to source of data for additional debug information 61 * @param orphanLayerCallback a callback to handle any unexpected orphan layers 62 */ parseFrom( byte[] data, Path source, Consumer<Layer> orphanLayerCallback)63 public static LayersTrace parseFrom( 64 byte[] data, Path source, Consumer<Layer> orphanLayerCallback) { 65 return parseFrom(data, source, null /* sourceChecksum */, orphanLayerCallback); 66 } 67 68 /** 69 * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list 70 * of trace entries, storing the flattened layers into its hierarchical structure. 71 * 72 * @param data binary proto data 73 * @param source Path to source of data for additional debug information 74 * @param orphanLayerCallback a callback to handle any unexpected orphan layers 75 */ parseFrom( byte[] data, Path source, String sourceChecksum, Consumer<Layer> orphanLayerCallback)76 public static LayersTrace parseFrom( 77 byte[] data, Path source, String sourceChecksum, Consumer<Layer> orphanLayerCallback) { 78 List<Entry> entries = new ArrayList<>(); 79 LayersTraceFileProto fileProto; 80 try { 81 fileProto = LayersTraceFileProto.parseFrom(data); 82 } catch (Exception e) { 83 throw new RuntimeException(e); 84 } 85 for (LayersTraceProto traceProto : fileProto.entry) { 86 Entry entry = 87 Entry.fromFlattenedLayers( 88 traceProto.elapsedRealtimeNanos, traceProto.layers.layers, 89 orphanLayerCallback); 90 entries.add(entry); 91 } 92 return new LayersTrace(entries, source, sourceChecksum); 93 } 94 95 /** 96 * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list 97 * of trace entries, storing the flattened layers into its hierarchical structure. 98 * 99 * @param data binary proto data 100 * @param source Path to source of data for additional debug information 101 */ parseFrom(byte[] data, Path source, String sourceChecksum)102 public static LayersTrace parseFrom(byte[] data, Path source, String sourceChecksum) { 103 return parseFrom(data, source, sourceChecksum, null /* orphanLayerCallback */); 104 } 105 106 /** 107 * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list 108 * of trace entries, storing the flattened layers into its hierarchical structure. 109 * 110 * @param data binary proto data 111 * @param source Path to source of data for additional debug information 112 */ parseFrom(byte[] data, Path source)113 public static LayersTrace parseFrom(byte[] data, Path source) { 114 return parseFrom(data, source, null /* sourceChecksum */, null /* orphanLayerCallback */); 115 } 116 117 /** 118 * Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list 119 * of trace entries, storing the flattened layers into its hierarchical structure. 120 * 121 * @param data binary proto data 122 */ parseFrom(byte[] data)123 public static LayersTrace parseFrom(byte[] data) { 124 return parseFrom(data, null /* source */); 125 } 126 getEntries()127 public List<Entry> getEntries() { 128 return mEntries; 129 } 130 getEntry(long timestamp)131 public Entry getEntry(long timestamp) { 132 Optional<Entry> entry = 133 mEntries.stream().filter(e -> e.getTimestamp() == timestamp).findFirst(); 134 if (!entry.isPresent()) { 135 throw new RuntimeException("Entry does not exist for timestamp " + timestamp); 136 } 137 return entry.get(); 138 } 139 getSource()140 public Optional<Path> getSource() { 141 return Optional.ofNullable(mSource); 142 } 143 getSourceChecksum()144 public String getSourceChecksum() { 145 return mSourceChecksum; 146 } 147 148 /** Represents a single Layer trace entry. */ 149 public static class Entry implements ITraceEntry { 150 private long mTimestamp; 151 private List<Layer> mRootLayers; // hierarchical representation of layers 152 private List<Layer> mFlattenedLayers = null; 153 Entry(long timestamp, List<Layer> rootLayers)154 private Entry(long timestamp, List<Layer> rootLayers) { 155 this.mTimestamp = timestamp; 156 this.mRootLayers = rootLayers; 157 } 158 159 /** 160 * Determines the id of the root element. 161 * 162 * <p>On some files, such as the ones used in the FlickerLib testdata, the root nodes are 163 * those that have parent=0, on newer traces, the root nodes are those that have parent=-1 164 * 165 * <p>This function keeps compatibility with both new and older traces by searching for a 166 * known root layer (Display Root) and considering its parent Id as overall root. 167 */ getRootLayer(SparseArray<Layer> layerMap)168 private static Layer getRootLayer(SparseArray<Layer> layerMap) { 169 Layer knownRoot = null; 170 int numKeys = layerMap.size(); 171 for (int i = 0; i < numKeys; ++i) { 172 Layer currentLayer = layerMap.valueAt(i); 173 if (currentLayer.isRootLayer()) { 174 knownRoot = currentLayer; 175 break; 176 } 177 } 178 179 if (knownRoot == null) { 180 throw new IllegalStateException("Display root layer not found."); 181 } 182 183 return layerMap.get(knownRoot.getParentId()); 184 } 185 186 /** Constructs the layer hierarchy from a flattened list of layers. */ fromFlattenedLayers(long timestamp, LayerProto[] protos, Consumer<Layer> orphanLayerCallback)187 public static Entry fromFlattenedLayers(long timestamp, LayerProto[] protos, 188 Consumer<Layer> orphanLayerCallback) { 189 SparseArray<Layer> layerMap = new SparseArray<>(); 190 ArrayList<Layer> orphans = new ArrayList<>(); 191 for (LayerProto proto : protos) { 192 int id = proto.id; 193 int parentId = proto.parent; 194 195 Layer newLayer = layerMap.get(id); 196 if (newLayer == null) { 197 newLayer = new Layer(proto); 198 layerMap.append(id, newLayer); 199 } else if (newLayer.mProto != null) { 200 throw new RuntimeException("Duplicate layer id found:" + id); 201 } else { 202 newLayer.mProto = proto; 203 orphans.remove(newLayer); 204 } 205 206 // add parent placeholder 207 if (layerMap.get(parentId) == null) { 208 Layer orphanLayer = new Layer(null); 209 layerMap.append(parentId, orphanLayer); 210 orphans.add(orphanLayer); 211 } 212 layerMap.get(parentId).addChild(newLayer); 213 newLayer.addParent(layerMap.get(parentId)); 214 } 215 216 // Remove root node 217 Layer rootLayer = getRootLayer(layerMap); 218 orphans.remove(rootLayer); 219 // Fail if we find orphan layers. 220 orphans.forEach( 221 orphan -> { 222 if (orphanLayerCallback != null) { 223 // Workaround for b/141326137, ignore the existence of an orphan layer 224 orphanLayerCallback.accept(orphan); 225 return; 226 } 227 String childNodes = 228 orphan.mChildren 229 .stream() 230 .map(node -> Integer.toString(node.getId())) 231 .collect(Collectors.joining(", ")); 232 int orphanId = orphan.mChildren.get(0).getParentId(); 233 throw new RuntimeException( 234 "Failed to parse layers trace. Found orphan layers with parent " 235 + "layer id:" 236 + orphanId 237 + " : " 238 + childNodes); 239 }); 240 241 return new Entry(timestamp, rootLayer.mChildren); 242 } 243 244 /** Extracts {@link Rect} from {@link RectProto}. */ extract(RectProto proto)245 private static Rect extract(RectProto proto) { 246 return new Rect(proto.left, proto.top, proto.right, proto.bottom); 247 } 248 249 /** 250 * Extracts {@link Rect} from {@link RegionProto} by returning a rect that encompasses all 251 * the rects making up the region. 252 */ extract(RegionProto regionProto)253 private static Rect extract(RegionProto regionProto) { 254 Rect region = new Rect(); 255 for (RectProto proto : regionProto.rect) { 256 region.union(proto.left, proto.top, proto.right, proto.bottom); 257 } 258 return region; 259 } 260 261 /** Checks if a region specified by {@code testRect} is covered by all visible layers. */ coversRegion(Rect testRect)262 public Result coversRegion(Rect testRect) { 263 String assertionName = "coversRegion"; 264 Collection<Layer> layers = asFlattenedLayers(); 265 266 for (int x = testRect.left; x < testRect.right; x++) { 267 for (int y = testRect.top; y < testRect.bottom; y++) { 268 boolean emptyRegionFound = true; 269 for (Layer layer : layers) { 270 if (layer.isInvisible() || layer.isHiddenByParent()) { 271 continue; 272 } 273 for (RectProto rectProto : layer.mProto.visibleRegion.rect) { 274 Rect r = extract(rectProto); 275 if (r.contains(x, y)) { 276 y = r.bottom; 277 emptyRegionFound = false; 278 } 279 } 280 } 281 if (emptyRegionFound) { 282 String reason = 283 "Region to test: " 284 + testRect 285 + "\nfirst empty point: " 286 + x 287 + ", " 288 + y; 289 reason += "\nvisible regions:"; 290 for (Layer layer : layers) { 291 if (layer.isInvisible() || layer.isHiddenByParent()) { 292 continue; 293 } 294 Rect r = extract(layer.mProto.visibleRegion); 295 reason += "\n" + layer.mProto.name + r.toString(); 296 } 297 return new Result( 298 false /* success */, this.mTimestamp, assertionName, reason); 299 } 300 } 301 } 302 String info = "Region covered: " + testRect; 303 return new Result(true /* success */, this.mTimestamp, assertionName, info); 304 } 305 306 /** 307 * Checks if a layer with name {@code layerName} has a visible region {@code 308 * expectedVisibleRegion}. 309 */ hasVisibleRegion(String layerName, Rect expectedVisibleRegion)310 public Result hasVisibleRegion(String layerName, Rect expectedVisibleRegion) { 311 String assertionName = "hasVisibleRegion"; 312 String reason = "Could not find " + layerName; 313 for (Layer layer : asFlattenedLayers()) { 314 if (layer.mProto.name.contains(layerName)) { 315 if (layer.isHiddenByParent()) { 316 reason = layer.getHiddenByParentReason(); 317 continue; 318 } 319 if (layer.isInvisible()) { 320 reason = layer.getVisibilityReason(); 321 continue; 322 } 323 Rect visibleRegion = extract(layer.mProto.visibleRegion); 324 if (visibleRegion.equals(expectedVisibleRegion)) { 325 return new Result( 326 true /* success */, 327 this.mTimestamp, 328 assertionName, 329 layer.mProto.name + "has visible region " + expectedVisibleRegion); 330 } 331 reason = 332 layer.mProto.name 333 + " has visible region:" 334 + visibleRegion 335 + " " 336 + "expected:" 337 + expectedVisibleRegion; 338 } 339 } 340 return new Result(false /* success */, this.mTimestamp, assertionName, reason); 341 } 342 343 /** Checks if a layer with name {@code layerName} exists in the hierarchy. */ exists(String layerName)344 public Result exists(String layerName) { 345 String assertionName = "exists"; 346 String reason = "Could not find " + layerName; 347 for (Layer layer : asFlattenedLayers()) { 348 if (layer.mProto.name.contains(layerName)) { 349 return new Result( 350 true /* success */, 351 this.mTimestamp, 352 assertionName, 353 layer.mProto.name + " exists"); 354 } 355 } 356 return new Result(false /* success */, this.mTimestamp, assertionName, reason); 357 } 358 359 /** Checks if a layer with name {@code layerName} is visible. */ isVisible(String layerName)360 public Result isVisible(String layerName) { 361 String assertionName = "isVisible"; 362 String reason = "Could not find " + layerName; 363 for (Layer layer : asFlattenedLayers()) { 364 if (layer.mProto.name.contains(layerName)) { 365 if (layer.isHiddenByParent()) { 366 reason = layer.getHiddenByParentReason(); 367 continue; 368 } 369 if (layer.isInvisible()) { 370 reason = layer.getVisibilityReason(); 371 continue; 372 } 373 return new Result( 374 true /* success */, 375 this.mTimestamp, 376 assertionName, 377 layer.mProto.name + " is visible"); 378 } 379 } 380 return new Result(false /* success */, this.mTimestamp, assertionName, reason); 381 } 382 383 @Override getTimestamp()384 public long getTimestamp() { 385 return mTimestamp; 386 } 387 getRootLayers()388 public List<Layer> getRootLayers() { 389 return mRootLayers; 390 } 391 392 /** Returns all layers as a flattened list using a depth first traversal. */ asFlattenedLayers()393 public List<Layer> asFlattenedLayers() { 394 if (mFlattenedLayers == null) { 395 mFlattenedLayers = new ArrayList<>(); 396 ArrayList<Layer> pendingLayers = new ArrayList<>(this.mRootLayers); 397 while (!pendingLayers.isEmpty()) { 398 Layer layer = pendingLayers.remove(0); 399 mFlattenedLayers.add(layer); 400 pendingLayers.addAll(layer.mChildren); 401 } 402 } 403 return mFlattenedLayers; 404 } 405 getVisibleBounds(String layerName)406 public Rect getVisibleBounds(String layerName) { 407 List<Layer> layers = asFlattenedLayers(); 408 for (Layer layer : layers) { 409 if (layer.mProto.name.contains(layerName) && layer.isVisible()) { 410 return extract(layer.mProto.visibleRegion); 411 } 412 } 413 return new Rect(0, 0, 0, 0); 414 } 415 } 416 417 /** Represents a single layer with links to its parent and child layers. */ 418 public static class Layer { 419 @Nullable public LayerProto mProto; 420 public List<Layer> mChildren; 421 @Nullable public Layer mParent = null; 422 Layer(LayerProto proto)423 private Layer(LayerProto proto) { 424 this.mProto = proto; 425 this.mChildren = new ArrayList<>(); 426 } 427 addChild(Layer childLayer)428 private void addChild(Layer childLayer) { 429 this.mChildren.add(childLayer); 430 } 431 addParent(Layer parentLayer)432 private void addParent(Layer parentLayer) { 433 this.mParent = parentLayer; 434 } 435 getId()436 public int getId() { 437 return mProto.id; 438 } 439 getParentId()440 public int getParentId() { 441 return mProto.parent; 442 } 443 getName()444 public String getName() { 445 if (mProto != null) { 446 return mProto.name; 447 } 448 449 return ""; 450 } 451 isActiveBufferEmpty()452 public boolean isActiveBufferEmpty() { 453 return this.mProto.activeBuffer == null 454 || this.mProto.activeBuffer.height == 0 455 || this.mProto.activeBuffer.width == 0; 456 } 457 isVisibleRegionEmpty()458 public boolean isVisibleRegionEmpty() { 459 if (this.mProto.visibleRegion == null) { 460 return true; 461 } 462 Rect visibleRect = Entry.extract(this.mProto.visibleRegion); 463 return visibleRect.height() == 0 || visibleRect.width() == 0; 464 } 465 isHidden()466 public boolean isHidden() { 467 return (this.mProto.flags & /* FLAG_HIDDEN */ 0x1) != 0x0; 468 } 469 isVisible()470 public boolean isVisible() { 471 return (!isActiveBufferEmpty() || isColorLayer()) 472 && !isHidden() 473 && this.mProto.color != null 474 && this.mProto.color.a > 0 475 && !isVisibleRegionEmpty(); 476 } 477 isColorLayer()478 public boolean isColorLayer() { 479 return this.mProto.type.equals("ColorLayer"); 480 } 481 isRootLayer()482 public boolean isRootLayer() { 483 return mParent != null && mParent.mProto == null; 484 } 485 isInvisible()486 public boolean isInvisible() { 487 return !isVisible(); 488 } 489 isHiddenByParent()490 public boolean isHiddenByParent() { 491 return !isRootLayer() && (mParent.isHidden() || mParent.isHiddenByParent()); 492 } 493 getHiddenByParentReason()494 public String getHiddenByParentReason() { 495 String reason = "Layer " + mProto.name; 496 if (isHiddenByParent()) { 497 reason += " is hidden by parent: " + mParent.mProto.name; 498 } else { 499 reason += " is not hidden by parent: " + mParent.mProto.name; 500 } 501 return reason; 502 } 503 getVisibilityReason()504 public String getVisibilityReason() { 505 String reason = "Layer " + mProto.name; 506 if (isVisible()) { 507 reason += " is visible:"; 508 } else { 509 reason += " is invisible:"; 510 if (this.mProto.activeBuffer == null) { 511 reason += " activeBuffer=null"; 512 } else if (this.mProto.activeBuffer.height == 0) { 513 reason += " activeBuffer.height=0"; 514 } else if (this.mProto.activeBuffer.width == 0) { 515 reason += " activeBuffer.width=0"; 516 } 517 if (!isColorLayer()) { 518 reason += " type != ColorLayer"; 519 } 520 if (isHidden()) { 521 reason += " flags=" + this.mProto.flags + " (FLAG_HIDDEN set)"; 522 } 523 if (this.mProto.color == null || this.mProto.color.a == 0) { 524 reason += " color.a=0"; 525 } 526 if (isVisibleRegionEmpty()) { 527 reason += " visible region is empty"; 528 } 529 } 530 return reason; 531 } 532 } 533 } 534