1<!-- Copyright (C) 2017 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 <div id="app"> 17 <md-app> 18 <md-app-toolbar md-tag="md-toolbar" class="top-toolbar"> 19 <h1 class="md-title" style="flex: 1">{{title}}</h1> 20 <md-button 21 class="md-primary md-theme-default download-all-btn" 22 @click="generateTags()" 23 v-if="dataLoaded && canGenerateTags" 24 >Generate Tags</md-button> 25 <md-button 26 class="md-primary md-theme-default" 27 @click="downloadAsZip(files)" 28 v-if="dataLoaded" 29 >Download All</md-button> 30 <md-button 31 class="md-accent md-raised md-theme-default clear-btn" 32 style="box-shadow: none;" 33 @click="clear()" 34 v-if="dataLoaded" 35 >Clear</md-button> 36 </md-app-toolbar> 37 38 <md-app-content class="main-content" :style="mainContentStyle"> 39 <section class="data-inputs" v-if="!dataLoaded"> 40 <div class="input"> 41 <dataadb class="adbinput" ref="adb" :store="store" 42 @dataReady="onDataReady" @statusChange="setStatus" /> 43 </div> 44 <div class="input" @dragover.prevent @drop.prevent> 45 <datainput class="fileinput" ref="input" :store="store" 46 @dataReady="onDataReady" @statusChange="setStatus" /> 47 </div> 48 </section> 49 50 <section class="data-view"> 51 <div 52 class="data-view-container" 53 v-for="file in dataViewFiles" 54 :key="file.type" 55 > 56 <dataview 57 :ref="file.type" 58 :store="store" 59 :file="file" 60 :presentTags="Object.freeze(presentTags)" 61 :presentErrors="Object.freeze(presentErrors)" 62 :dataViewFiles="dataViewFiles" 63 @click="onDataViewFocus(file)" 64 /> 65 </div> 66 67 <overlay 68 :presentTags="Object.freeze(presentTags)" 69 :presentErrors="Object.freeze(presentErrors)" 70 :store="store" 71 :ref="overlayRef" 72 :searchTypes="searchTypes" 73 v-if="dataLoaded" 74 v-on:bottom-nav-height-change="handleBottomNavHeightChange" 75 /> 76 </section> 77 </md-app-content> 78 </md-app> 79 </div> 80</template> 81<script> 82import Overlay from './Overlay.vue'; 83import DataView from './DataView.vue'; 84import DataInput from './DataInput.vue'; 85import LocalStore from './localstore.js'; 86import DataAdb from './DataAdb.vue'; 87import FileType from './mixins/FileType.js'; 88import SaveAsZip from './mixins/SaveAsZip'; 89import FocusedDataViewFinder from './mixins/FocusedDataViewFinder'; 90import {DIRECTION} from './utils/utils'; 91import Searchbar from './Searchbar.vue'; 92import {NAVIGATION_STYLE, SEARCH_TYPE} from './utils/consts'; 93import {TRACE_TYPES, FILE_TYPES, dataFile} from './decode.js'; 94import { TaggingEngine } from './flickerlib/common'; 95 96const APP_NAME = 'Winscope'; 97 98const CONTENT_BOTTOM_PADDING = 25; 99 100export default { 101 name: 'app', 102 mixins: [FileType, SaveAsZip, FocusedDataViewFinder], 103 data() { 104 return { 105 title: APP_NAME, 106 activeDataView: null, 107 // eslint-disable-next-line new-cap 108 store: LocalStore('app', { 109 flattened: false, 110 onlyVisible: false, 111 simplifyNames: true, 112 displayDefaults: true, 113 navigationStyle: NAVIGATION_STYLE.GLOBAL, 114 flickerTraceView: false, 115 showFileTypes: [], 116 isInputMode: false, 117 }), 118 overlayRef: 'overlay', 119 mainContentStyle: { 120 'padding-bottom': `${CONTENT_BOTTOM_PADDING}px`, 121 }, 122 tagFile: null, 123 presentTags: [], 124 presentErrors: [], 125 searchTypes: [SEARCH_TYPE.TIMESTAMP], 126 hasTagOrErrorTraces: false, 127 }; 128 }, 129 created() { 130 window.addEventListener('keydown', this.onKeyDown); 131 window.addEventListener('scroll', this.onScroll); 132 document.title = this.title; 133 }, 134 destroyed() { 135 window.removeEventListener('keydown', this.onKeyDown); 136 window.removeEventListener('scroll', this.onScroll); 137 }, 138 139 methods: { 140 /** Get states from either tag files or error files */ 141 getUpdatedStates(files) { 142 var states = []; 143 for (const file of files) { 144 states.push(...file.data); 145 } 146 return states; 147 }, 148 /** Get tags from all uploaded tag files*/ 149 getUpdatedTags() { 150 if (this.tagFile === null) return []; 151 const tagStates = this.getUpdatedStates([this.tagFile]); 152 var tags = []; 153 tagStates.forEach(tagState => { 154 tagState.tags.forEach(tag => { 155 tag.timestamp = Number(tagState.timestamp); 156 // tags generated on frontend have transition.name due to kotlin enum 157 tag.transition = tag.transition.name ?? tag.transition; 158 tags.push(tag); 159 }); 160 }); 161 return tags; 162 }, 163 /** Get tags from all uploaded error files*/ 164 getUpdatedErrors() { 165 var errorStates = this.getUpdatedStates(this.errorFiles); 166 var errors = []; 167 //TODO (b/196201487) add check if errors empty 168 errorStates.forEach(errorState => { 169 errorState.errors.forEach(error => { 170 error.timestamp = Number(errorState.timestamp); 171 errors.push(error); 172 }); 173 }); 174 return errors; 175 }, 176 /** Set flicker mode check for if there are tag/error traces uploaded*/ 177 updateHasTagOrErrorTraces() { 178 return this.hasTagTrace() || this.hasErrorTrace(); 179 }, 180 hasTagTrace() { 181 return this.tagFile !== null; 182 }, 183 hasErrorTrace() { 184 return this.errorFiles.length > 0; 185 }, 186 /** Activate flicker search tab if tags/errors uploaded*/ 187 updateSearchTypes() { 188 this.searchTypes = [SEARCH_TYPE.TIMESTAMP]; 189 if (this.hasTagTrace()) { 190 this.searchTypes.push(SEARCH_TYPE.TRANSITIONS); 191 } 192 if (this.hasErrorTrace()) { 193 this.searchTypes.push(SEARCH_TYPE.ERRORS); 194 } 195 }, 196 /** Filter data view files by current show settings*/ 197 updateShowFileTypes() { 198 this.store.showFileTypes = this.dataViewFiles 199 .filter((file) => file.show) 200 .map(file => file.type); 201 }, 202 clear() { 203 this.store.showFileTypes = []; 204 this.tagFile = null; 205 this.$store.commit('clearFiles'); 206 this.buttonClicked("Clear") 207 }, 208 onDataViewFocus(file) { 209 this.$store.commit('setActiveFile', file); 210 this.activeDataView = file.type; 211 }, 212 onKeyDown(event) { 213 event = event || window.event; 214 if (this.store.isInputMode) return false; 215 if (event.keyCode == 37 /* left */ ) { 216 this.$store.dispatch('advanceTimeline', DIRECTION.BACKWARD); 217 } else if (event.keyCode == 39 /* right */ ) { 218 this.$store.dispatch('advanceTimeline', DIRECTION.FORWARD); 219 } else if (event.keyCode == 38 /* up */ ) { 220 this.$refs[this.activeView][0].arrowUp(); 221 } else if (event.keyCode == 40 /* down */ ) { 222 this.$refs[this.activeView][0].arrowDown(); 223 } else { 224 return false; 225 } 226 event.preventDefault(); 227 return true; 228 }, 229 onDataReady(files) { 230 this.$store.dispatch('setFiles', files); 231 232 this.tagFile = this.tagFiles[0] ?? null; 233 this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces(); 234 this.presentTags = this.getUpdatedTags(); 235 this.presentErrors = this.getUpdatedErrors(); 236 this.updateSearchTypes(); 237 this.updateFocusedView(); 238 this.updateShowFileTypes(); 239 }, 240 setStatus(status) { 241 if (status) { 242 this.title = status; 243 } else { 244 this.title = APP_NAME; 245 } 246 }, 247 handleBottomNavHeightChange(newHeight) { 248 this.$set( 249 this.mainContentStyle, 250 'padding-bottom', 251 `${ CONTENT_BOTTOM_PADDING + newHeight }px`, 252 ); 253 }, 254 generateTags() { 255 // generate tag file 256 this.buttonClicked("Generate Tags"); 257 const engine = new TaggingEngine( 258 this.$store.getters.tagGenerationWmTrace, 259 this.$store.getters.tagGenerationSfTrace, 260 (text) => { console.log(text) } 261 ); 262 const tagTrace = engine.run(); 263 const tagFile = this.generateTagFile(tagTrace); 264 265 // update tag trace in set files, update flicker mode 266 this.tagFile = tagFile; 267 this.hasTagOrErrorTraces = this.updateHasTagOrErrorTraces(); 268 this.presentTags = this.getUpdatedTags(); 269 this.presentErrors = this.getUpdatedErrors(); 270 this.updateSearchTypes(); 271 }, 272 273 generateTagFile(tagTrace) { 274 const data = tagTrace.entries; 275 const blobUrl = URL.createObjectURL(new Blob([], {type: undefined})); 276 return dataFile( 277 "GeneratedTagTrace.winscope", 278 data.map((x) => x.timestamp), 279 data, 280 blobUrl, 281 FILE_TYPES.TAG_TRACE 282 ); 283 }, 284 }, 285 computed: { 286 files() { 287 return this.$store.getters.sortedFiles.map(file => { 288 if (this.hasDataView(file)) { 289 file.show = true; 290 } 291 return file; 292 }); 293 }, 294 prettyDump() { 295 return JSON.stringify(this.dump, null, 2); 296 }, 297 dataLoaded() { 298 return this.files.length > 0; 299 }, 300 activeView() { 301 if (!this.activeDataView && this.files.length > 0) { 302 // eslint-disable-next-line vue/no-side-effects-in-computed-properties 303 this.activeDataView = this.files[0].type; 304 } 305 return this.activeDataView; 306 }, 307 dataViewFiles() { 308 return this.files.filter((file) => this.hasDataView(file)); 309 }, 310 tagFiles() { 311 return this.$store.getters.tagFiles; 312 }, 313 errorFiles() { 314 return this.$store.getters.errorFiles; 315 }, 316 timelineFiles() { 317 return this.$store.getters.timelineFiles; 318 }, 319 canGenerateTags() { 320 const fileTypes = this.dataViewFiles.map((file) => file.type); 321 return fileTypes.includes(TRACE_TYPES.WINDOW_MANAGER) 322 && fileTypes.includes(TRACE_TYPES.SURFACE_FLINGER); 323 }, 324 }, 325 watch: { 326 title() { 327 document.title = this.title; 328 }, 329 }, 330 components: { 331 overlay: Overlay, 332 dataview: DataView, 333 datainput: DataInput, 334 dataadb: DataAdb, 335 searchbar: Searchbar, 336 }, 337}; 338</script> 339<style> 340@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@600&display=swap'); 341 342#app .md-app-container { 343 /* Get rid of transforms which prevent fixed position from being used */ 344 transform: none!important; 345 min-height: 100vh; 346} 347 348#app .top-toolbar { 349 box-shadow: none; 350 background-color: #fff; 351 background-color: var(--md-theme-default-background, #fff); 352 border-bottom: thin solid rgba(0,0,0,.12); 353 padding: 0 40px; 354} 355 356#app .top-toolbar .md-title { 357 font-family: 'Open Sans', sans-serif; 358 white-space: nowrap; 359 color: #5f6368; 360 margin: 0; 361 padding: 0; 362 font-size: 22px; 363 letter-spacing: 0; 364 font-weight: 600; 365} 366 367.data-view { 368 display: flex; 369 flex-direction: column; 370} 371 372.card-toolbar { 373 border-bottom: 1px solid rgba(0, 0, 0, .12); 374} 375 376.timeline { 377 margin: 16px; 378} 379 380.container { 381 display: flex; 382 flex-wrap: wrap; 383} 384 385.md-button { 386 margin-top: 1em 387} 388 389h1 { 390 font-weight: normal; 391} 392 393.data-inputs { 394 display: flex; 395 flex-wrap: wrap; 396 height: 100%; 397 width: 100%; 398 align-self: center; 399 /* align-items: center; */ 400 align-content: center; 401 justify-content: center; 402} 403 404.data-inputs .input { 405 padding: 15px; 406 flex: 1 1 0; 407 max-width: 840px; 408 /* align-self: center; */ 409} 410 411.data-inputs .input > div { 412 height: 100%; 413} 414 415.data-view-container { 416 padding: 25px 20px 0 20px; 417} 418 419.snackbar-break-words { 420 /* These are technically the same, but use both */ 421 overflow-wrap: break-word; 422 word-wrap: break-word; 423 -ms-word-break: break-all; 424 word-break: break-word; 425 /* Adds a hyphen where the word breaks, if supported (No Blink) */ 426 -ms-hyphens: auto; 427 -moz-hyphens: auto; 428 -webkit-hyphens: auto; 429 hyphens: auto; 430 padding: 10px 10px 10px 10px; 431} 432</style>