1<!-- Copyright (C) 2019 The Android Open Source Project 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14--> 15<template> 16 <md-card-content class="container"> 17 <div class="rects" v-if="hasScreenView"> 18 <rects 19 :bounds="bounds" 20 :rects="rects" 21 :highlight="highlight" 22 @rect-click="onRectClick" 23 /> 24 </div> 25 26 <div class="hierarchy"> 27 <flat-card> 28 <md-content 29 md-tag="md-toolbar" 30 md-elevation="0" 31 class="card-toolbar md-transparent md-dense" 32 > 33 <h2 class="md-title" style="flex: 1;">Hierarchy</h2> 34 <md-checkbox 35 v-model="showHierarchyDiff" 36 v-if="diffVisualizationAvailable" 37 > 38 Show Diff 39 </md-checkbox> 40 <md-checkbox v-model="store.simplifyNames"> 41 Simplify names 42 </md-checkbox> 43 <md-checkbox v-model="store.onlyVisible">Only visible</md-checkbox> 44 <md-checkbox v-model="store.flattened">Flat</md-checkbox> 45 <md-checkbox v-if="hasTagsOrErrors" v-model="store.flickerTraceView">Flicker</md-checkbox> 46 <md-field md-inline class="filter"> 47 <label>Filter...</label> 48 <md-input 49 v-model="hierarchyPropertyFilterString" 50 v-on:focus="updateInputMode(true)" 51 v-on:blur="updateInputMode(false)" 52 /> 53 </md-field> 54 </md-content> 55 <div class="tree-view-wrapper"> 56 <tree-view 57 class="treeview" 58 :item="tree" 59 @item-selected="itemSelected" 60 :selected="hierarchySelected" 61 :filter="hierarchyFilter" 62 :flattened="store.flattened" 63 :onlyVisible="store.onlyVisible" 64 :flickerTraceView="store.flickerTraceView" 65 :presentTags="presentTags" 66 :presentErrors="presentErrors" 67 :items-clickable="true" 68 :useGlobalCollapsedState="true" 69 :simplify-names="store.simplifyNames" 70 ref="hierarchy" 71 /> 72 </div> 73 </flat-card> 74 </div> 75 76 <div class="properties"> 77 <flat-card> 78 <md-content 79 md-tag="md-toolbar" 80 md-elevation="0" 81 class="card-toolbar md-transparent md-dense" 82 > 83 <h2 class="md-title" style="flex: 1">Properties</h2> 84 <div> 85 <md-checkbox 86 v-model="displayDefaults" 87 @change="checkboxChange" 88 > 89 Show Defaults 90 </md-checkbox> 91 <md-tooltip md-direction="bottom"> 92 If checked, shows the value of all properties. 93 Otherwise, hides all properties whose value is 94 the default for its data type. 95 </md-tooltip> 96 </div> 97 <md-checkbox 98 v-model="showPropertiesDiff" 99 v-if="diffVisualizationAvailable" 100 > 101 Show Diff 102 </md-checkbox> 103 <md-field md-inline class="filter"> 104 <label>Filter...</label> 105 <md-input 106 v-model="propertyFilterString" 107 v-on:focus="updateInputMode(true)" 108 v-on:blur="updateInputMode(false)" 109 /> 110 </md-field> 111 </md-content> 112 <div class="properties-content"> 113 <div v-if="elementSummary" class="element-summary"> 114 <div v-for="elem in elementSummary" v-bind:key="elem.key"> 115 <!-- eslint-disable-next-line max-len --> 116 <span class="key">{{ elem.key }}:</span> <span class="value">{{ elem.value }}</span> 117 </div> 118 </div> 119 <div v-if="selectedTree" class="tree-view-wrapper"> 120 <tree-view 121 class="treeview" 122 :item="selectedTree" 123 :filter="propertyFilter" 124 :collapseChildren="true" 125 :elementView="PropertiesTreeElement" 126 /> 127 </div> 128 <div class="no-properties" v-else> 129 <i class="material-icons none-icon"> 130 filter_none 131 </i> 132 <span>No element selected in the hierarchy.</span> 133 </div> 134 </div> 135 </flat-card> 136 </div> 137 138 </md-card-content> 139</template> 140<script> 141import TreeView from './TreeView.vue'; 142import Rects from './Rects.vue'; 143import FlatCard from './components/FlatCard.vue'; 144import PropertiesTreeElement from './PropertiesTreeElement.vue'; 145 146import {ObjectTransformer} from './transform.js'; 147import {DiffGenerator, defaultModifiedCheck} from './utils/diff.js'; 148import {TRACE_TYPES, DUMP_TYPES} from './decode.js'; 149import {isPropertyMatch, stableIdCompatibilityFixup} from './utils/utils.js'; 150import {CompatibleFeatures} from './utils/compatibility.js'; 151import {getPropertiesForDisplay} from './flickerlib/mixin'; 152import ObjectFormatter from './flickerlib/ObjectFormatter'; 153 154function formatProto(obj) { 155 if (obj?.prettyPrint) { 156 return obj.prettyPrint(); 157 } 158} 159 160function findEntryInTree(tree, id) { 161 if (tree.stableId === id) { 162 return tree; 163 } 164 165 if (!tree.children) { 166 return null; 167 } 168 169 for (const child of tree.children) { 170 const foundEntry = findEntryInTree(child, id); 171 if (foundEntry) { 172 return foundEntry; 173 } 174 } 175 176 return null; 177} 178 179export default { 180 name: 'traceview', 181 props: ['store', 'file', 'summarizer', 'presentTags', 'presentErrors'], 182 data() { 183 return { 184 propertyFilterString: '', 185 hierarchyPropertyFilterString: '', 186 selectedTree: null, 187 hierarchySelected: null, 188 lastSelectedStableId: null, 189 bounds: {}, 190 rects: [], 191 item: null, 192 tree: null, 193 highlight: null, 194 showHierarchyDiff: false, 195 displayDefaults: false, 196 showPropertiesDiff: false, 197 PropertiesTreeElement, 198 }; 199 }, 200 methods: { 201 checkboxChange(checked) { 202 this.itemSelected(this.item); 203 }, 204 itemSelected(item) { 205 this.hierarchySelected = item; 206 this.selectedTree = this.getTransformedProperties(item); 207 this.highlight = item.rect; 208 this.lastSelectedStableId = item.stableId; 209 this.$emit('focus'); 210 }, 211 getTransformedProperties(item) { 212 ObjectFormatter.displayDefaults = this.displayDefaults; 213 const transformer = new ObjectTransformer( 214 getPropertiesForDisplay(item), 215 item.name, 216 stableIdCompatibilityFixup(item), 217 ).setOptions({ 218 skip: item.skip, 219 formatter: formatProto, 220 }); 221 222 if (this.showPropertiesDiff && this.diffVisualizationAvailable) { 223 const prevItem = this.getItemFromPrevTree(item); 224 transformer.withDiff(getPropertiesForDisplay(prevItem)); 225 } 226 227 return transformer.transform(); 228 }, 229 onRectClick(item) { 230 if (item) { 231 this.itemSelected(item); 232 } 233 }, 234 generateTreeFromItem(item) { 235 if (!this.showHierarchyDiff || !this.diffVisualizationAvailable) { 236 return item; 237 } 238 239 const thisItem = this.item; 240 const prevItem = this.getDataWithOffset(-1); 241 return new DiffGenerator(thisItem) 242 .compareWith(prevItem) 243 .withUniqueNodeId((node) => { 244 return node.stableId; 245 }) 246 .withModifiedCheck(defaultModifiedCheck) 247 .generateDiffTree(); 248 }, 249 setData(item) { 250 this.item = item; 251 this.tree = this.generateTreeFromItem(item); 252 253 const rects = item.rects; // .toArray() 254 this.rects = [...rects].reverse(); 255 this.bounds = item.bounds; 256 257 this.hierarchySelected = null; 258 this.selectedTree = null; 259 this.highlight = null; 260 261 function findItem(item, stableId) { 262 if (item.stableId === stableId) { 263 return item; 264 } 265 if (Array.isArray(item.children)) { 266 for (const child of item.children) { 267 const found = findItem(child, stableId); 268 if (found) { 269 return found; 270 } 271 } 272 } 273 return null; 274 } 275 276 if (this.lastSelectedStableId) { 277 const found = findItem(item, this.lastSelectedStableId); 278 if (found) { 279 this.itemSelected(found); 280 } 281 } 282 }, 283 arrowUp() { 284 return this.$refs.hierarchy.selectPrev(); 285 }, 286 arrowDown() { 287 return this.$refs.hierarchy.selectNext(); 288 }, 289 getDataWithOffset(offset) { 290 const index = this.file.selectedIndex + offset; 291 292 if (index < 0 || index >= this.file.data.length) { 293 return null; 294 } 295 296 return this.file.data[index]; 297 }, 298 getItemFromPrevTree(entry) { 299 if (!this.showPropertiesDiff || !this.hierarchySelected) { 300 return null; 301 } 302 303 const id = entry.stableId; 304 if (!id) { 305 throw new Error('Entry has no stableId...'); 306 } 307 308 const prevTree = this.getDataWithOffset(-1); 309 if (!prevTree) { 310 console.warn('No previous entry'); 311 return null; 312 } 313 314 const prevEntry = findEntryInTree(prevTree, id); 315 if (!prevEntry) { 316 console.warn('Didn\'t exist in last entry'); 317 // TODO: Maybe handle this in some way. 318 } 319 320 return prevEntry; 321 }, 322 323 /** Performs check for id match between entry and present tags/errors 324 * must be carried out for every present tag/error 325 */ 326 matchItems(flickerItems, entryItem) { 327 var match = false; 328 flickerItems.forEach(flickerItem => { 329 if (isPropertyMatch(flickerItem, entryItem)) match = true; 330 }); 331 return match; 332 }, 333 /** Returns check for id match between entry and present tags/errors */ 334 isEntryTagMatch(entryItem) { 335 return this.matchItems(this.presentTags, entryItem) || this.matchItems(this.presentErrors, entryItem); 336 }, 337 338 /** determines whether left/right arrow keys should move cursor in input field */ 339 updateInputMode(isInputMode) { 340 this.store.isInputMode = isInputMode; 341 }, 342 }, 343 created() { 344 this.setData(this.file.data[this.file.selectedIndex ?? 0]); 345 }, 346 destroyed() { 347 this.store.flickerTraceView = false; 348 }, 349 watch: { 350 selectedIndex() { 351 this.setData(this.file.data[this.file.selectedIndex ?? 0]); 352 }, 353 showHierarchyDiff() { 354 this.tree = this.generateTreeFromItem(this.item); 355 }, 356 showPropertiesDiff() { 357 if (this.hierarchySelected) { 358 this.selectedTree = 359 this.getTransformedProperties(this.hierarchySelected); 360 } 361 }, 362 }, 363 computed: { 364 diffVisualizationAvailable() { 365 return CompatibleFeatures.DiffVisualization && ( 366 this.file.type == TRACE_TYPES.WINDOW_MANAGER || 367 this.file.type == TRACE_TYPES.SURFACE_FLINGER 368 ); 369 }, 370 selectedIndex() { 371 return this.file.selectedIndex; 372 }, 373 hierarchyFilter() { 374 const hierarchyPropertyFilter = 375 getFilter(this.hierarchyPropertyFilterString); 376 var fil = this.store.onlyVisible ? (c) => { 377 return c.isVisible && hierarchyPropertyFilter(c); 378 } : hierarchyPropertyFilter; 379 return this.store.flickerTraceView ? (c) => { 380 return this.isEntryTagMatch(c); 381 } : fil; 382 }, 383 propertyFilter() { 384 return getFilter(this.propertyFilterString); 385 }, 386 hasScreenView() { 387 return this.file.type == TRACE_TYPES.WINDOW_MANAGER || 388 this.file.type == TRACE_TYPES.SURFACE_FLINGER || 389 this.file.type == DUMP_TYPES.WINDOW_MANAGER || 390 this.file.type == DUMP_TYPES.SURFACE_FLINGER; 391 }, 392 elementSummary() { 393 if (!this.hierarchySelected || !this.summarizer) { 394 return null; 395 } 396 397 const summary = this.summarizer(this.hierarchySelected); 398 399 if (summary?.length === 0) { 400 return null; 401 } 402 403 return summary; 404 }, 405 hasTagsOrErrors() { 406 return this.presentTags.length > 0 || this.presentErrors.length > 0; 407 }, 408 }, 409 components: { 410 'tree-view': TreeView, 411 'rects': Rects, 412 'flat-card': FlatCard, 413 }, 414}; 415 416function getFilter(filterString) { 417 const filterStrings = filterString.split(','); 418 const positive = []; 419 const negative = []; 420 filterStrings.forEach((f) => { 421 if (f.startsWith('!')) { 422 const str = f.substring(1); 423 negative.push((s) => s.indexOf(str) === -1); 424 } else { 425 const str = f; 426 positive.push((s) => s.indexOf(str) !== -1); 427 } 428 }); 429 const filter = (item) => { 430 const apply = (f) => f(String(item.name)); 431 return (positive.length === 0 || positive.some(apply)) && 432 (negative.length === 0 || negative.every(apply)); 433 }; 434 return filter; 435} 436 437</script> 438<style scoped> 439.container { 440 display: flex; 441 flex-wrap: wrap; 442} 443 444.rects { 445 flex: none; 446 margin: 8px; 447} 448 449.hierarchy, 450.properties { 451 flex: 1; 452 margin: 8px; 453 min-width: 400px; 454 min-height: 50rem; 455} 456 457.rects, 458.hierarchy, 459.properties { 460 padding: 5px; 461} 462 463.flat-card { 464 display: flex; 465 flex-direction: column; 466 height: 100%; 467} 468 469.hierarchy>.tree-view, 470.properties>.tree-view { 471 margin: 16px; 472} 473 474.treeview { 475 overflow: auto; 476 white-space: pre-line; 477} 478 479.no-properties { 480 display: flex; 481 flex: 1; 482 flex-direction: column; 483 align-self: center; 484 align-items: center; 485 justify-content: center; 486 padding: 50px 25px; 487} 488 489.no-properties .none-icon { 490 font-size: 35px; 491 margin-bottom: 10px; 492} 493 494.no-properties span { 495 font-weight: 100; 496} 497 498.filter { 499 width: auto; 500} 501 502.element-summary { 503 padding: 1rem; 504 border-bottom: thin solid rgba(0,0,0,.12); 505} 506 507.element-summary .key { 508 font-weight: 500; 509} 510 511.element-summary .value { 512 color: rgba(0, 0, 0, 0.75); 513} 514 515.properties-content { 516 display: flex; 517 flex-direction: column; 518 flex: 1; 519} 520 521.tree-view-wrapper { 522 display: flex; 523 flex-direction: column; 524 flex: 1; 525} 526 527.treeview { 528 flex: 1 0 0; 529} 530</style> 531