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