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]: 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 traces[traceType] = new typeInfo.constructor(traceFiles); 169 } 170 } 171 172 state.traces = traces; 173 174 // TODO: Refactor common code out 175 const dumps = {}; 176 for (const dumpType of Object.values(DUMP_TYPES)) { 177 const dumpFiles = {}; 178 const typeInfo = DUMP_INFO[dumpType]; 179 180 for (const dumpDataFile of typeInfo.files) { 181 const files = filesByType[dumpDataFile.type]; 182 183 if (!files) { 184 continue; 185 } 186 187 if (dumpDataFile.oneOf) { 188 if (files.length > 1) { 189 throw new Error(`More than one file of type ${dumpDataFile.type} has been provided`); 190 } 191 192 dumpFiles[dumpDataFile.type] = files[0]; 193 } else if (dumpDataFile.manyOf) { 194 195 } else { 196 throw new Error("Missing oneOf or manyOf property..."); 197 } 198 } 199 200 if (Object.keys(dumpFiles).length > 0 && typeInfo.constructor) { 201 dumps[dumpType] = new typeInfo.constructor(dumpFiles); 202 } 203 204 } 205 206 state.dumps = dumps; 207 208 if (!state.activeFile && Object.keys(traces).length > 0) { 209 state.activeFile = sortFiles(Object.values(traces))[0]; 210 } 211 212 // TODO: Add same for dumps 213 }, 214 clearFiles(state) { 215 for (const traceType in state.traces) { 216 if (state.traces.hasOwnProperty(traceType)) { 217 Vue.delete(state.traces, traceType); 218 } 219 } 220 221 for (const dumpType in state.dumps) { 222 if (state.dumps.hasOwnProperty(dumpType)) { 223 Vue.delete(state.dumps, dumpType); 224 } 225 } 226 227 state.activeFile = null; 228 state.mergedTimeline = null; 229 }, 230 setActiveFile(state, file) { 231 state.activeFile = file; 232 }, 233 setMergedTimeline(state, timeline) { 234 state.mergedTimeline = timeline; 235 }, 236 removeMergedTimeline(state, timeline) { 237 state.mergedTimeline = null; 238 }, 239 setMergedTimelineIndex(state, newIndex) { 240 state.mergedTimeline.selectedIndex = newIndex; 241 }, 242 setCollapsedState(state, { item, isCollapsed }) { 243 if (item.stableId === undefined || item.stableId === null) { 244 return; 245 } 246 247 Vue.set( 248 state.collapsedStateStore, 249 stableIdCompatibilityFixup(item), 250 isCollapsed 251 ); 252 }, 253 setFocusedFile(state, file) { 254 state.focusedFile = file; 255 }, 256 setNavigationFilesFilter(state, filter) { 257 state.navigationFilesFilter = filter; 258 }, 259 }, 260 actions: { 261 setFiles(context, files) { 262 context.commit('clearFiles'); 263 context.commit('setFiles', files); 264 265 const timestamp = findSmallestTimestamp(files); 266 if (timestamp !== undefined) { 267 context.commit('setCurrentTimestamp', timestamp); 268 } 269 }, 270 updateTimelineTime(context, timestamp) { 271 for (const file of context.getters.files) { 272 //dumps do not have a timeline, so only look at files with timelines to update the timestamp 273 if (!file.timeline) continue; 274 275 const type = file.type; 276 const entryIndex = findLastMatchingSorted( 277 file.timeline, 278 (array, idx) => parseInt(array[idx]) <= timestamp, 279 ); 280 281 context.commit('setFileEntryIndex', { type, entryIndex }); 282 } 283 284 if (context.state.mergedTimeline) { 285 const newIndex = findLastMatchingSorted( 286 context.state.mergedTimeline.timeline, 287 (array, idx) => parseInt(array[idx]) <= timestamp, 288 ); 289 290 context.commit('setMergedTimelineIndex', newIndex); 291 } 292 293 context.commit('setCurrentTimestamp', timestamp); 294 }, 295 advanceTimeline(context, direction) { 296 // NOTE: MergedTimeline is never considered to find the next closest index 297 // MergedTimeline only represented the timelines overlapped together and 298 // isn't considered an actual timeline. 299 300 if (direction !== DIRECTION.FORWARD && direction !== DIRECTION.BACKWARD) { 301 throw new Error("Unsupported direction provided."); 302 } 303 304 const consideredFiles = context.getters.timelineFiles 305 .filter(context.state.navigationFilesFilter); 306 307 let fileIndex = -1; 308 let timelineIndex; 309 let minTimeDiff = Infinity; 310 311 for (let idx = 0; idx < consideredFiles.length; idx++) { 312 const file = consideredFiles[idx]; 313 314 let candidateTimestampIndex = file.selectedIndex; 315 let candidateTimestamp = file.timeline[candidateTimestampIndex]; 316 317 let candidateCondition; 318 switch (direction) { 319 case DIRECTION.BACKWARD: 320 candidateCondition = () => candidateTimestamp < context.state.currentTimestamp; 321 break; 322 case DIRECTION.FORWARD: 323 candidateCondition = () => candidateTimestamp > context.state.currentTimestamp; 324 break; 325 } 326 327 if (!candidateCondition()) { 328 // Not a candidate — find a valid candidate 329 let noCandidate = false; 330 while (!candidateCondition()) { 331 candidateTimestampIndex += direction; 332 if (candidateTimestampIndex < 0 || candidateTimestampIndex >= file.timeline.length) { 333 noCandidate = true; 334 break; 335 } 336 candidateTimestamp = file.timeline[candidateTimestampIndex]; 337 } 338 339 if (noCandidate) { 340 continue; 341 } 342 } 343 344 const timeDiff = Math.abs(candidateTimestamp - context.state.currentTimestamp); 345 if (minTimeDiff > timeDiff) { 346 minTimeDiff = timeDiff; 347 fileIndex = idx; 348 timelineIndex = candidateTimestampIndex; 349 } 350 } 351 352 if (fileIndex >= 0) { 353 const closestFile = consideredFiles[fileIndex]; 354 const timestamp = parseInt(closestFile.timeline[timelineIndex]); 355 356 context.dispatch('updateTimelineTime', timestamp); 357 } 358 } 359 } 360}) 361 362/** 363 * Make Google analytics functionalities available for recording events. 364 */ 365Vue.use(VueGtag, { 366 config: { id: 'G-RRV0M08Y76'} 367}) 368 369Vue.mixin({ 370 methods: { 371 buttonClicked(button) { 372 const string = "Clicked " + button + " Button"; 373 this.$gtag.event(string, { 374 'event_category': 'Button Clicked', 375 'event_label': "Winscope Interactions", 376 'value': button, 377 }); 378 }, 379 draggedAndDropped(val) { 380 this.$gtag.event("Dragged And DroppedFile", { 381 'event_category': 'Uploaded file', 382 'event_label': "Winscope Interactions", 383 'value': val, 384 }); 385 }, 386 uploadedFileThroughFilesystem(val) { 387 this.$gtag.event("Uploaded File From Filesystem", { 388 'event_category': 'Uploaded file', 389 'event_label': "Winscope Interactions", 390 'value': val, 391 }); 392 }, 393 newEventOccurred(event) { 394 this.$gtag.event(event, { 395 'event_category': event, 396 'event_label': "Winscope Interactions", 397 'value': 1, 398 }); 399 }, 400 seeingNewScreen(screenname) { 401 this.$gtag.screenview({ 402 app_name: "Winscope", 403 screen_name: screenname, 404 }) 405 }, 406 openedToSeeAttributeField(field) { 407 const string = "Opened field " + field; 408 this.$gtag.event(string, { 409 'event_category': "Opened attribute field", 410 'event_label': "Winscope Interactions", 411 'value': field, 412 }); 413 }, 414 } 415}); 416 417new Vue({ 418 el: '#app', 419 store, // inject the Vuex store into all components 420 render: h => h(App) 421}) 422