1<!-- Copyright (C) 2020 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 18 <flat-card class="changes card"> 19 <md-content 20 md-tag="md-toolbar" 21 md-elevation="0" 22 class="card-toolbar md-transparent md-dense" 23 > 24 <h2 class="md-title" style="flex: 1">Transactions</h2> 25 </md-content> 26 <div class="filters"> 27 <div class="input"> 28 <md-field> 29 <label>Transaction Type</label> 30 <md-select v-model="selectedTransactionTypes" multiple> 31 <md-option 32 v-for="type in transactionTypes" 33 :value="type" 34 v-bind:key="type"> 35 {{ type }} 36 </md-option> 37 </md-select> 38 </md-field> 39 </div> 40 41 <div class="input"> 42 <div> 43 <md-autocomplete 44 v-model="selectedProperty" 45 :md-options="properties" 46 > 47 <label>Changed property</label> 48 </md-autocomplete> 49 <!-- TODO(b/159582192): Add way to select value a property has 50 changed to, figure out how to handle properties that are 51 objects... --> 52 </div> 53 </div> 54 55 <div class="input"> 56 <md-field> 57 <label>Origin PID</label> 58 <md-select v-model="selectedPids" multiple> 59 <md-option v-for="pid in pids" :value="pid" v-bind:key="pid"> 60 {{ pid }} 61 </md-option> 62 </md-select> 63 </md-field> 64 </div> 65 66 <div class="input"> 67 <md-field> 68 <label>Origin UID</label> 69 <md-select v-model="selectedUids" multiple> 70 <md-option v-for="uid in uids" :value="uid" v-bind:key="uid"> 71 {{ uid }} 72 </md-option> 73 </md-select> 74 </md-field> 75 </div> 76 77 <div class="input"> 78 <md-chips 79 v-model="filters" 80 md-placeholder="Add surface id or name..." 81 > 82 <div class="md-helper-text">Press enter to add</div> 83 </md-chips> 84 </div> 85 86 <md-checkbox v-model="trace.simplifyNames"> 87 Simplify names 88 </md-checkbox> 89 90 </div> 91 92 <virtual-list style="height: 600px; overflow-y: auto;" 93 :data-key="'timestamp'" 94 :data-sources="filteredData" 95 :data-component="transactionEntryComponent" 96 :extra-props="{ 97 onClick: transactionSelected, 98 selectedTransaction, 99 transactionsTrace, 100 prettifyTransactionId, 101 simplifyNames: trace.simplifyNames, 102 }" 103 ref="loglist" 104 /> 105 </flat-card> 106 107 <flat-card class="changes card"> 108 <md-content 109 md-tag="md-toolbar" 110 md-elevation="0" 111 class="card-toolbar md-transparent md-dense" 112 > 113 <h2 class="md-title" style="flex: 1">Changes</h2> 114 </md-content> 115 <div class="changes-content" v-if="selectedTree"> 116 <div 117 v-if="selectedTransaction.type === 'transaction'" 118 class="transaction-events" 119 > 120 <div 121 v-for="(event, i) in transactionHistory(selectedTransaction)" 122 v-bind:key="`${selectedTransaction.identifier}-${i}`" 123 class="transaction-event" 124 > 125 <div v-if="event.type === 'apply'" class="applied-event"> 126 applied 127 </div> 128 <div v-if="event.type === 'merge'" class="merged-event"> 129 <!-- eslint-disable-next-line max-len --> 130 {{ prettifyTransactionId(event.mergedId) }} 131 </div> 132 </div> 133 </div> 134 <tree-view 135 :item="selectedTree" 136 :collapseChildren="true" 137 :useGlobalCollapsedState="true" 138 /> 139 </div> 140 <div class="no-properties" v-else> 141 <i class="material-icons none-icon"> 142 filter_none 143 </i> 144 <span>No transaction selected.</span> 145 </div> 146 </flat-card> 147 148 </md-card-content> 149</template> 150<script> 151import TreeView from './TreeView.vue'; 152import VirtualList from '../libs/virtualList/VirtualList'; 153import TransactionEntry from './TransactionEntry.vue'; 154import FlatCard from './components/FlatCard.vue'; 155 156import {ObjectTransformer} from './transform.js'; 157import {expandTransactionId} from '@/traces/Transactions.ts'; 158 159export default { 160 name: 'transactionsview', 161 props: ['trace'], 162 data() { 163 const transactionTypes = new Set(); 164 const properties = new Set(); 165 const pids = new Set(); 166 const uids = new Set(); 167 const transactionsTrace = this.trace; 168 for (const entry of transactionsTrace.data) { 169 if (entry.type == 'transaction') { 170 for (const transaction of entry.transactions) { 171 transactionTypes.add(transaction.type); 172 Object.keys(transaction.obj).forEach((item) => properties.add(item)); 173 } 174 } else { 175 transactionTypes.add(entry.type); 176 Object.keys(entry.obj).forEach((item) => properties.add(item)); 177 } 178 179 if (entry.origin) { 180 pids.add(entry.origin.pid); 181 uids.add(entry.origin.uid); 182 } 183 } 184 185 // Remove vsync from being transaction types that can be filtered 186 // We want to always show vsyncs 187 transactionTypes.delete('vsyncEvent'); 188 189 return { 190 transactionTypes: Array.from(transactionTypes), 191 properties: Array.from(properties), 192 pids: Array.from(pids), 193 uids: Array.from(uids), 194 selectedTransactionTypes: [], 195 selectedPids: [], 196 selectedUids: [], 197 searchInput: '', 198 selectedTree: null, 199 filters: [], 200 selectedProperty: null, 201 selectedTransaction: null, 202 transactionEntryComponent: TransactionEntry, 203 transactionsTrace, 204 expandTransactionId, 205 }; 206 }, 207 computed: { 208 data() { 209 return this.transactionsTrace.data; 210 }, 211 filteredData() { 212 let filteredData = this.data; 213 214 if (this.selectedTransactionTypes.length > 0) { 215 filteredData = filteredData.filter( 216 this.filterTransactions((transaction) => 217 transaction.type === 'vsyncEvent' || 218 this.selectedTransactionTypes.includes(transaction.type))); 219 } 220 221 if (this.selectedPids.length > 0) { 222 filteredData = filteredData.filter((entry) => 223 this.selectedPids.includes(entry.origin?.pid)); 224 } 225 226 if (this.selectedUids.length > 0) { 227 filteredData = filteredData.filter((entry) => 228 this.selectedUids.includes(entry.origin?.uid)); 229 } 230 231 if (this.filters.length > 0) { 232 filteredData = filteredData.filter( 233 this.filterTransactions((transaction) => { 234 for (const filter of this.filters) { 235 if (isNaN(filter) && transaction.layerName?.includes(filter)) { 236 // If filter isn't a number then check if the transaction's 237 // target surface's name matches the filter — if so keep it. 238 return true; 239 } 240 if (filter == transaction.obj.id) { 241 // If filteter is a number then check if the filter matches 242 // the transaction's target surface id — if so keep it. 243 return true; 244 } 245 } 246 247 // Exclude transaction if it fails to match filter. 248 return false; 249 }), 250 ); 251 } 252 253 if (this.selectedProperty) { 254 filteredData = filteredData.filter( 255 this.filterTransactions((transaction) => { 256 for (const key in transaction.obj) { 257 if (this.isMeaningfulChange(transaction.obj, key) && 258 key === this.selectedProperty) { 259 return true; 260 } 261 } 262 263 return false; 264 }), 265 ); 266 } 267 268 // We quish vsyncs because otherwise the lazy list will not load enough 269 // elements if there are many vsyncs in a row since vsyncs take up no 270 // space. 271 return this.squishVSyncs(filteredData); 272 }, 273 274 }, 275 methods: { 276 removeNullFields(changeObject) { 277 for (const key in changeObject) { 278 if (changeObject[key] === null) { 279 delete changeObject[key]; 280 } 281 } 282 283 return changeObject; 284 }, 285 transactionSelected(transaction) { 286 this.selectedTransaction = transaction; 287 288 const META_DATA_KEY = 'metadata'; 289 290 let obj; 291 let name; 292 if (transaction.type == 'transaction') { 293 name = 'changes'; 294 obj = {}; 295 296 const [surfaceChanges, displayChanges] = 297 this.aggregateTransactions(transaction.transactions); 298 299 // Prepare the surface and display changes to be passed through 300 // the ObjectTransformer — in particular, remove redundant properties 301 // and add metadata that can be accessed post transformation 302 const perpareForTreeViewTransform = (change) => { 303 this.removeNullFields(change); 304 change[META_DATA_KEY] = { 305 layerName: change.layerName, 306 }; 307 // remove redundant properties 308 delete change.layerName; 309 delete change.id; 310 }; 311 312 for (const changeId in surfaceChanges) { 313 if (surfaceChanges.hasOwnProperty(changeId)) { 314 perpareForTreeViewTransform(surfaceChanges[changeId]); 315 } 316 } 317 for (const changeId in displayChanges) { 318 if (displayChanges.hasOwnProperty(changeId)) { 319 perpareForTreeViewTransform(displayChanges[changeId]); 320 } 321 } 322 323 if (Object.keys(surfaceChanges).length > 0) { 324 obj.surfaceChanges = surfaceChanges; 325 } 326 327 if (Object.keys(displayChanges).length > 0) { 328 obj.displayChanges = displayChanges; 329 } 330 } else { 331 obj = this.removeNullFields(transaction.obj); 332 name = transaction.type; 333 } 334 335 // Transform the raw JS object to be TreeView compatible 336 const transactionUniqueId = transaction.timestamp; 337 let tree = new ObjectTransformer( 338 obj, 339 name, 340 transactionUniqueId, 341 ).setOptions({ 342 formatter: () => {}, 343 }).transform({ 344 keepOriginal: true, 345 metadataKey: META_DATA_KEY, 346 freeze: false, 347 }); 348 349 // Add the layer name as the kind of the object to be shown in the 350 // TreeView 351 const addLayerNameAsKind = (tree) => { 352 for (const layerChanges of tree.children) { 353 layerChanges.kind = layerChanges.metadata.layerName; 354 } 355 }; 356 357 if (transaction.type == 'transaction') { 358 for (const child of tree.children) { 359 // child = surfaceChanges or displayChanges tree node 360 addLayerNameAsKind(child); 361 } 362 } 363 364 // If there are only surfaceChanges or only displayChanges and not both 365 // remove the extra top layer node which is meant to hold both types of 366 // changes when both are present 367 if (tree.name == 'changes' && tree.children.length === 1) { 368 tree = tree.children[0]; 369 } 370 371 this.selectedTree = tree; 372 }, 373 filterTransactions(condition) { 374 return (entry) => { 375 if (entry.type == 'transaction') { 376 for (const transaction of entry.transactions) { 377 if (condition(transaction)) { 378 return true; 379 } 380 } 381 382 return false; 383 } else { 384 return condition(entry); 385 } 386 }; 387 }, 388 isMeaningfulChange(object, key) { 389 // TODO (b/159799733): Handle cases of non null objects but meaningless 390 // change 391 return object[key] !== null && object.hasOwnProperty(key); 392 }, 393 mergeChanges(a, b) { 394 const res = {}; 395 396 for (const key in a) { 397 if (this.isMeaningfulChange(a, key)) { 398 res[key] = a[key]; 399 } 400 } 401 402 for (const key in b) { 403 if (this.isMeaningfulChange(b, key)) { 404 if (res.hasOwnProperty(key) && key != 'id') { 405 throw new Error(`Merge failed – key '${key}' already present`); 406 } 407 res[key] = b[key]; 408 } 409 } 410 411 return res; 412 }, 413 aggregateTransactions(transactions) { 414 const surfaceChanges = {}; 415 const displayChanges = {}; 416 417 for (const transaction of transactions) { 418 const obj = transaction.obj; 419 420 // Create a new base object to merge all changes into 421 const newBaseObj = () => { 422 return { 423 layerName: transaction.layerName, 424 }; 425 }; 426 427 switch (transaction.type) { 428 case 'surfaceChange': 429 surfaceChanges[obj.id] = 430 this.mergeChanges(surfaceChanges[obj.id] ?? newBaseObj(), obj); 431 break; 432 433 case 'displayChange': 434 displayChanges[obj.id] = 435 this.mergeChanges(displayChanges[obj.id] ?? newBaseObj(), obj); 436 break; 437 438 default: 439 throw new Error(`Unhandled transaction type ${transaction.type}`); 440 } 441 } 442 443 return [surfaceChanges, displayChanges]; 444 }, 445 446 transactionHistory(selectedTransaction) { 447 const transactionId = selectedTransaction.identifier; 448 const history = this.transactionsTrace.transactionHistory 449 .generateHistoryTreesOf(transactionId); 450 451 return history; 452 }, 453 454 prettifyTransactionId(transactionId) { 455 const expandedId = expandTransactionId(transactionId); 456 return `${expandedId.pid}.${expandedId.id}`; 457 }, 458 459 squishVSyncs(data) { 460 return data.filter((event, i) => { 461 return !(event.type === 'vsyncEvent' && 462 data[i + 1]?.type === 'vsyncEvent'); 463 }); 464 }, 465 }, 466 components: { 467 'virtual-list': VirtualList, 468 'tree-view': TreeView, 469 'flat-card': FlatCard, 470 }, 471}; 472 473</script> 474<style scoped> 475.container { 476 display: flex; 477 flex-wrap: wrap; 478} 479 480.transaction-table, 481.changes { 482 flex: 1 1 0; 483 width: 0; 484 margin: 8px; 485} 486 487.scrollBody { 488 width: 100%; 489 height: 100%; 490 overflow: scroll; 491} 492 493.filters { 494 margin-bottom: 15px; 495 width: 100%; 496 padding: 15px 5px; 497 display: flex; 498 flex-wrap: wrap; 499} 500 501.filters .input { 502 max-width: 300px; 503 margin: 0 10px; 504 flex-grow: 1; 505} 506 507.changes-content { 508 padding: 18px; 509 height: 550px; 510 overflow: auto; 511} 512 513.no-properties { 514 display: flex; 515 flex-direction: column; 516 align-self: center; 517 align-items: center; 518 justify-content: center; 519 height: calc(100% - 50px); 520 padding: 50px 25px; 521} 522 523.no-properties .none-icon { 524 font-size: 35px; 525 margin-bottom: 10px; 526} 527 528.no-properties span { 529 font-weight: 100; 530} 531 532.transaction-event { 533 display: inline-flex; 534} 535</style> 536