1/* 2 * Copyright 2017, 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 17import Vue from 'vue' 18import Vuex from 'vuex' 19import VueMaterial from 'vue-material' 20import VueGtag from "vue-gtag"; 21 22import App from './App.vue' 23import { TRACE_TYPES, DUMP_TYPES, TRACE_INFO, DUMP_INFO } from './decode.js' 24import { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup } from './utils/utils.js' 25 26import 'style-loader!css-loader!vue-material/dist/vue-material.css' 27import 'style-loader!css-loader!vue-material/dist/theme/default.css' 28 29Vue.use(Vuex) 30Vue.use(VueMaterial) 31 32// Used to determine the order in which files or displayed 33const fileOrder = { 34 [TRACE_TYPES.WINDOW_MANAGER]: 1, 35 [TRACE_TYPES.SURFACE_FLINGER]: 2, 36 [TRACE_TYPES.TRANSACTION_LEGACY]: 3, 37 [TRACE_TYPES.PROTO_LOG]: 4, 38 [TRACE_TYPES.IME_CLIENTS]: 5, 39 [TRACE_TYPES.IME_SERVICE]: 6, 40 [TRACE_TYPES.IME_MANAGERSERVICE]: 7, 41}; 42 43function sortFiles(files) { 44 return files.sort( 45 (a, b) => (fileOrder[a.type] ?? Infinity) - (fileOrder[b.type] ?? Infinity)); 46}; 47 48/** 49 * Find the smallest timeline timestamp in a list of files 50 * @return undefined if not timestamp exists in the timelines of the files 51 */ 52function findSmallestTimestamp(files) { 53 let timestamp = Infinity; 54 for (const file of files) { 55 if (file.timeline[0] && file.timeline[0] < timestamp) { 56 timestamp = file.timeline[0]; 57 } 58 } 59 60 return timestamp === Infinity ? undefined : timestamp; 61} 62 63const store = new Vuex.Store({ 64 state: { 65 currentTimestamp: 0, 66 traces: {}, 67 dumps: {}, 68 excludeFromTimeline: [ 69 TRACE_TYPES.PROTO_LOG, 70 TRACE_TYPES.TAG, 71 TRACE_TYPES.ERROR 72 ], 73 activeFile: null, 74 focusedFile: null, 75 mergedTimeline: null, 76 navigationFilesFilter: f => true, 77 // obj -> bool, identifies whether or not an item is collapsed in a treeView 78 collapsedStateStore: {}, 79 }, 80 getters: { 81 collapsedStateStoreFor: (state) => (item) => { 82 if (item.stableId === undefined || item.stableId === null) { 83 console.error("Missing stable ID for item", item); 84 throw new Error("Failed to get collapse state of item — missing a stableId"); 85 } 86 87 return state.collapsedStateStore[stableIdCompatibilityFixup(item)]; 88 }, 89 files(state) { 90 return Object.values(state.traces).concat(Object.values(state.dumps)); 91 }, 92 sortedFiles(state, getters) { 93 return sortFiles(getters.files); 94 }, 95 timelineFiles(state, getters) { 96 return Object.values(state.traces) 97 .filter(file => !state.excludeFromTimeline.includes(file.type)); 98 }, 99 tagFiles(state, getters) { 100 return Object.values(state.traces) 101 .filter(file => file.type === TRACE_TYPES.TAG); 102 }, 103 errorFiles(state, getters) { 104 return Object.values(state.traces) 105 .filter(file => file.type === TRACE_TYPES.ERROR); 106 }, 107 sortedTimelineFiles(state, getters) { 108 return sortFiles(getters.timelineFiles); 109 }, 110 video(state) { 111 return state.traces[TRACE_TYPES.SCREEN_RECORDING]; 112 }, 113 tagGenerationWmTrace(state, getters) { 114 return state.traces[TRACE_TYPES.WINDOW_MANAGER].tagGenerationTrace; 115 }, 116 tagGenerationSfTrace(state, getters) { 117 return state.traces[TRACE_TYPES.SURFACE_FLINGER].tagGenerationTrace; 118 } 119 }, 120 mutations: { 121 setCurrentTimestamp(state, timestamp) { 122 state.currentTimestamp = timestamp; 123 }, 124 setFileEntryIndex(state, { type, entryIndex }) { 125 if (state.traces[type]) { 126 state.traces[type].selectedIndex = entryIndex; 127 } else { 128 throw new Error("Unexpected type — not a trace..."); 129 } 130 }, 131 setFiles(state, files) { 132 const filesByType = {}; 133 for (const file of files) { 134 if (!filesByType[file.type]) { 135 filesByType[file.type] = []; 136 } 137 filesByType[file.type].push(file); 138 } 139 140 // TODO: Extract into smaller functions 141 const traces = {}; 142 for (const traceType of Object.values(TRACE_TYPES)) { 143 const traceFiles = {}; 144 const typeInfo = TRACE_INFO[traceType]; 145 146 for (const traceDataFile of typeInfo.files) { 147 148 const files = filesByType[traceDataFile.type]; 149 150 if (!files) { 151 continue; 152 } 153 154 if (traceDataFile.oneOf) { 155 if (files.length > 1) { 156 throw new Error(`More than one file of type ${traceDataFile.type} has been provided`); 157 } 158 159 traceFiles[traceDataFile.type] = files[0]; 160 } else if (traceDataFile.manyOf) { 161 traceFiles[traceDataFile.type] = files; 162 } else { 163 throw new Error("Missing oneOf or manyOf property..."); 164 } 165 } 166 167 if (Object.keys(traceFiles).length > 0 && typeInfo.constructor) { 168 const newObj = new typeInfo.constructor(traceFiles); 169 newObj.data = Object.freeze(newObj.data); 170 traces[traceType] = newObj; 171 } 172 } 173 174 state.traces = traces; 175 176 // TODO: Refactor common code out 177 const dumps = {}; 178 for (const dumpType of Object.values(DUMP_TYPES)) { 179 const dumpFiles = {}; 180 const typeInfo = DUMP_INFO[dumpType]; 181 182 for (const dumpDataFile of typeInfo.files) { 183 const files = filesByType[dumpDataFile.type]; 184 185 if (!files) { 186 continue; 187 } 188 189 if (dumpDataFile.oneOf) { 190 if (files.length > 1) { 191 throw new Error(`More than one file of type ${dumpDataFile.type} has been provided`); 192 } 193 194 dumpFiles[dumpDataFile.type] = files[0]; 195 } else if (dumpDataFile.manyOf) { 196 197 } else { 198 throw new Error("Missing oneOf or manyOf property..."); 199 } 200 } 201 202 if (Object.keys(dumpFiles).length > 0 && typeInfo.constructor) { 203 const newObj = new typeInfo.constructor(dumpFiles); 204 newObj.data = Object.freeze(newObj.data); 205 dumps[dumpType] = newObj; 206 } 207 208 } 209 210 state.dumps = dumps; 211 212 if (!state.activeFile && Object.keys(traces).length > 0) { 213 state.activeFile = sortFiles(Object.values(traces))[0]; 214 } 215 216 // TODO: Add same for dumps 217 }, 218 clearFiles(state) { 219 for (const traceType in state.traces) { 220 if (state.traces.hasOwnProperty(traceType)) { 221 Vue.delete(state.traces, traceType); 222 } 223 } 224 225 for (const dumpType in state.dumps) { 226 if (state.dumps.hasOwnProperty(dumpType)) { 227 Vue.delete(state.dumps, dumpType); 228 } 229 } 230 231 state.activeFile = null; 232 state.mergedTimeline = null; 233 }, 234 setActiveFile(state, file) { 235 state.activeFile = file; 236 }, 237 setMergedTimeline(state, timeline) { 238 state.mergedTimeline = timeline; 239 }, 240 removeMergedTimeline(state, timeline) { 241 state.mergedTimeline = null; 242 }, 243 setMergedTimelineIndex(state, newIndex) { 244 state.mergedTimeline.selectedIndex = newIndex; 245 }, 246 setCollapsedState(state, { item, isCollapsed }) { 247 if (item.stableId === undefined || item.stableId === null) { 248 return; 249 } 250 251 Vue.set( 252 state.collapsedStateStore, 253 stableIdCompatibilityFixup(item), 254 isCollapsed 255 ); 256 }, 257 setFocusedFile(state, file) { 258 state.focusedFile = file; 259 }, 260 setNavigationFilesFilter(state, filter) { 261 state.navigationFilesFilter = filter; 262 }, 263 }, 264 actions: { 265 setFiles(context, files) { 266 context.commit('clearFiles'); 267 context.commit('setFiles', files); 268 269 const timestamp = findSmallestTimestamp(files); 270 if (timestamp !== undefined) { 271 context.commit('setCurrentTimestamp', timestamp); 272 } 273 }, 274 updateTimelineTime(context, timestamp) { 275 for (const file of context.getters.files) { 276 //dumps do not have a timeline, so only look at files with timelines to update the timestamp 277 if (!file.timeline) continue; 278 279 const type = file.type; 280 const entryIndex = findLastMatchingSorted( 281 file.timeline, 282 (array, idx) => parseInt(array[idx]) <= timestamp, 283 ); 284 285 context.commit('setFileEntryIndex', { type, entryIndex }); 286 } 287 288 if (context.state.mergedTimeline) { 289 const newIndex = findLastMatchingSorted( 290 context.state.mergedTimeline.timeline, 291 (array, idx) => parseInt(array[idx]) <= timestamp, 292 ); 293 294 context.commit('setMergedTimelineIndex', newIndex); 295 } 296 297 context.commit('setCurrentTimestamp', timestamp); 298 }, 299 advanceTimeline(context, direction) { 300 // NOTE: MergedTimeline is never considered to find the next closest index 301 // MergedTimeline only represented the timelines overlapped together and 302 // isn't considered an actual timeline. 303 304 if (direction !== DIRECTION.FORWARD && direction !== DIRECTION.BACKWARD) { 305 throw new Error("Unsupported direction provided."); 306 } 307 308 const consideredFiles = context.getters.timelineFiles 309 .filter(context.state.navigationFilesFilter); 310 311 let fileIndex = -1; 312 let timelineIndex; 313 let minTimeDiff = Infinity; 314 315 for (let idx = 0; idx < consideredFiles.length; idx++) { 316 const file = consideredFiles[idx]; 317 318 let candidateTimestampIndex = file.selectedIndex; 319 let candidateTimestamp = file.timeline[candidateTimestampIndex]; 320 321 let candidateCondition; 322 switch (direction) { 323 case DIRECTION.BACKWARD: 324 candidateCondition = () => candidateTimestamp < context.state.currentTimestamp; 325 break; 326 case DIRECTION.FORWARD: 327 candidateCondition = () => candidateTimestamp > context.state.currentTimestamp; 328 break; 329 } 330 331 if (!candidateCondition()) { 332 // Not a candidate — find a valid candidate 333 let noCandidate = false; 334 while (!candidateCondition()) { 335 candidateTimestampIndex += direction; 336 if (candidateTimestampIndex < 0 || candidateTimestampIndex >= file.timeline.length) { 337 noCandidate = true; 338 break; 339 } 340 candidateTimestamp = file.timeline[candidateTimestampIndex]; 341 } 342 343 if (noCandidate) { 344 continue; 345 } 346 } 347 348 const timeDiff = Math.abs(candidateTimestamp - context.state.currentTimestamp); 349 if (minTimeDiff > timeDiff) { 350 minTimeDiff = timeDiff; 351 fileIndex = idx; 352 timelineIndex = candidateTimestampIndex; 353 } 354 } 355 356 if (fileIndex >= 0) { 357 const closestFile = consideredFiles[fileIndex]; 358 const timestamp = parseInt(closestFile.timeline[timelineIndex]); 359 360 context.dispatch('updateTimelineTime', timestamp); 361 } 362 } 363 } 364}) 365 366/** 367 * Make Google analytics functionalities available for recording events. 368 */ 369Vue.use(VueGtag, { 370 config: { id: 'G-RRV0M08Y76'} 371}) 372 373Vue.mixin({ 374 methods: { 375 recordButtonClickedEvent(button) { 376 const txt = "Clicked " + button + " Button"; 377 this.$gtag.event(txt, { 378 'event_category': 'Button Clicked', 379 'event_label': "Winscope Interactions", 380 'value': button, 381 }); 382 }, 383 recordDragAndDropFileEvent(val) { 384 this.$gtag.event("Dragged And DroppedFile", { 385 'event_category': 'Uploaded file', 386 'event_label': "Winscope Interactions", 387 'value': val, 388 }); 389 }, 390 recordFileUploadEvent(val) { 391 this.$gtag.event("Uploaded File From Filesystem", { 392 'event_category': 'Uploaded file', 393 'event_label': "Winscope Interactions", 394 'value': val, 395 }); 396 }, 397 recordNewEvent(event) { 398 this.$gtag.event(event, { 399 'event_category': event, 400 'event_label': "Winscope Interactions", 401 'value': 1, 402 }); 403 }, 404 recordOpenTraceEvent(traceType) { 405 this.$gtag.screenview({ 406 app_name: "Winscope", 407 screen_name: traceType, 408 }) 409 }, 410 recordExpandedPropertyEvent(field) { 411 const string = "Property: " + field; 412 this.$gtag.event(string, { 413 'event_category': "Expanded property", 414 'event_label': "Winscope Interactions", 415 'value': field, 416 }); 417 }, 418 recordOpenedEntryEvent(entryType) { 419 const string = "Trace: " + entryType; 420 this.$gtag.event(string, { 421 'event_category': "Opened trace", 422 'event_label': "Winscope Interactions", 423 'value': entryType, 424 }); 425 }, 426 recordChangedNavigationStyleEvent(field) { 427 this.$gtag.event("Navigation mode changed", { 428 'event_category': "Timeline Navigation", 429 'event_label': "Winscope Interactions", 430 'value': field, 431 }); 432 }, 433 } 434}); 435 436new Vue({ 437 el: '#app', 438 store, // inject the Vuex store into all components 439 render: h => h(App) 440}) 441