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' 20 21import App from './App.vue' 22import { TRACE_TYPES, DUMP_TYPES, TRACE_INFO, DUMP_INFO } from './decode.js' 23import { DIRECTION, findLastMatchingSorted, stableIdCompatibilityFixup } from './utils/utils.js' 24 25import 'style-loader!css-loader!vue-material/dist/vue-material.css' 26import 'style-loader!css-loader!vue-material/dist/theme/default.css' 27 28Vue.use(Vuex) 29Vue.use(VueMaterial) 30 31// Used to determine the order in which files or displayed 32const fileOrder = { 33 [TRACE_TYPES.WINDOW_MANAGER]: 1, 34 [TRACE_TYPES.SURFACE_FLINGER]: 2, 35 [TRACE_TYPES.TRANSACTION]: 3, 36 [TRACE_TYPES.PROTO_LOG]: 4, 37 [TRACE_TYPES.IME_CLIENTS]: 5, 38 [TRACE_TYPES.IME_SERVICE]: 6, 39 [TRACE_TYPES.IME_MANAGERSERVICE]: 7, 40}; 41 42function sortFiles(files) { 43 return files.sort( 44 (a, b) => (fileOrder[a.type] ?? Infinity) - (fileOrder[b.type] ?? Infinity)); 45}; 46 47/** 48 * Find the smallest timeline timestamp in a list of files 49 * @return undefined if not timestamp exists in the timelines of the files 50 */ 51function findSmallestTimestamp(files) { 52 let timestamp = Infinity; 53 for (const file of files) { 54 if (file.timeline[0] && file.timeline[0] < timestamp) { 55 timestamp = file.timeline[0]; 56 } 57 } 58 59 return timestamp === Infinity ? undefined : timestamp; 60} 61 62const store = new Vuex.Store({ 63 state: { 64 currentTimestamp: 0, 65 traces: {}, 66 dumps: {}, 67 excludeFromTimeline: [ 68 TRACE_TYPES.PROTO_LOG, 69 ], 70 activeFile: null, 71 focusedFile: null, 72 mergedTimeline: null, 73 navigationFilesFilter: f => true, 74 // obj -> bool, identifies whether or not an item is collapsed in a treeView 75 collapsedStateStore: {}, 76 }, 77 getters: { 78 collapsedStateStoreFor: (state) => (item) => { 79 if (item.stableId === undefined || item.stableId === null) { 80 console.error("Missing stable ID for item", item); 81 throw new Error("Failed to get collapse state of item — missing a stableId"); 82 } 83 84 return state.collapsedStateStore[stableIdCompatibilityFixup(item)]; 85 }, 86 files(state) { 87 return Object.values(state.traces).concat(Object.values(state.dumps)); 88 }, 89 sortedFiles(state, getters) { 90 return sortFiles(getters.files); 91 }, 92 timelineFiles(state, getters) { 93 return Object.values(state.traces) 94 .filter(file => !state.excludeFromTimeline.includes(file.type)); 95 }, 96 sortedTimelineFiles(state, getters) { 97 return sortFiles(getters.timelineFiles); 98 }, 99 video(state) { 100 return state.traces[TRACE_TYPES.SCREEN_RECORDING]; 101 }, 102 }, 103 mutations: { 104 setCurrentTimestamp(state, timestamp) { 105 state.currentTimestamp = timestamp; 106 }, 107 setFileEntryIndex(state, { type, entryIndex }) { 108 if (state.traces[type]) { 109 state.traces[type].selectedIndex = entryIndex; 110 } else { 111 throw new Error("Unexpected type — not a trace..."); 112 } 113 }, 114 setFiles(state, files) { 115 const filesByType = {}; 116 for (const file of files) { 117 if (!filesByType[file.type]) { 118 filesByType[file.type] = []; 119 } 120 filesByType[file.type].push(file); 121 } 122 123 // TODO: Extract into smaller functions 124 const traces = {}; 125 for (const traceType of Object.values(TRACE_TYPES)) { 126 const traceFiles = {}; 127 const typeInfo = TRACE_INFO[traceType]; 128 129 for (const traceDataFile of typeInfo.files) { 130 131 const files = filesByType[traceDataFile.type]; 132 133 if (!files) { 134 continue; 135 } 136 137 if (traceDataFile.oneOf) { 138 if (files.length > 1) { 139 throw new Error(`More than one file of type ${traceDataFile.type} has been provided`); 140 } 141 142 traceFiles[traceDataFile.type] = files[0]; 143 } else if (traceDataFile.manyOf) { 144 traceFiles[traceDataFile.type] = files; 145 } else { 146 throw new Error("Missing oneOf or manyOf property..."); 147 } 148 } 149 150 if (Object.keys(traceFiles).length > 0 && typeInfo.constructor) { 151 traces[traceType] = new typeInfo.constructor(traceFiles); 152 } 153 } 154 155 state.traces = traces; 156 157 // TODO: Refactor common code out 158 const dumps = {}; 159 for (const dumpType of Object.values(DUMP_TYPES)) { 160 const dumpFiles = {}; 161 const typeInfo = DUMP_INFO[dumpType]; 162 163 for (const dumpDataFile of typeInfo.files) { 164 const files = filesByType[dumpDataFile.type]; 165 166 if (!files) { 167 continue; 168 } 169 170 if (dumpDataFile.oneOf) { 171 if (files.length > 1) { 172 throw new Error(`More than one file of type ${dumpDataFile.type} has been provided`); 173 } 174 175 dumpFiles[dumpDataFile.type] = files[0]; 176 } else if (dumpDataFile.manyOf) { 177 178 } else { 179 throw new Error("Missing oneOf or manyOf property..."); 180 } 181 } 182 183 if (Object.keys(dumpFiles).length > 0 && typeInfo.constructor) { 184 dumps[dumpType] = new typeInfo.constructor(dumpFiles); 185 } 186 187 } 188 189 state.dumps = dumps; 190 191 if (!state.activeFile && Object.keys(traces).length > 0) { 192 state.activeFile = sortFiles(Object.values(traces))[0]; 193 } 194 195 // TODO: Add same for dumps 196 }, 197 clearFiles(state) { 198 for (const traceType in state.traces) { 199 if (state.traces.hasOwnProperty(traceType)) { 200 Vue.delete(state.traces, traceType); 201 } 202 } 203 204 for (const dumpType in state.dumps) { 205 if (state.dumps.hasOwnProperty(dumpType)) { 206 Vue.delete(state.dumps, dumpType); 207 } 208 } 209 210 state.activeFile = null; 211 state.mergedTimeline = null; 212 }, 213 setActiveFile(state, file) { 214 state.activeFile = file; 215 }, 216 setMergedTimeline(state, timeline) { 217 state.mergedTimeline = timeline; 218 }, 219 removeMergedTimeline(state, timeline) { 220 state.mergedTimeline = null; 221 }, 222 setMergedTimelineIndex(state, newIndex) { 223 state.mergedTimeline.selectedIndex = newIndex; 224 }, 225 setCollapsedState(state, { item, isCollapsed }) { 226 if (item.stableId === undefined || item.stableId === null) { 227 return; 228 } 229 230 Vue.set( 231 state.collapsedStateStore, 232 stableIdCompatibilityFixup(item), 233 isCollapsed 234 ); 235 }, 236 setFocusedFile(state, file) { 237 state.focusedFile = file; 238 }, 239 setNavigationFilesFilter(state, filter) { 240 state.navigationFilesFilter = filter; 241 }, 242 }, 243 actions: { 244 setFiles(context, files) { 245 context.commit('clearFiles'); 246 context.commit('setFiles', files); 247 248 const timestamp = findSmallestTimestamp(files); 249 if (timestamp !== undefined) { 250 context.commit('setCurrentTimestamp', timestamp); 251 } 252 }, 253 updateTimelineTime(context, timestamp) { 254 for (const file of context.getters.files) { 255 const type = file.type; 256 const entryIndex = findLastMatchingSorted( 257 file.timeline, 258 (array, idx) => parseInt(array[idx]) <= timestamp, 259 ); 260 261 context.commit('setFileEntryIndex', { type, entryIndex }); 262 } 263 264 if (context.state.mergedTimeline) { 265 const newIndex = findLastMatchingSorted( 266 context.state.mergedTimeline.timeline, 267 (array, idx) => parseInt(array[idx]) <= timestamp, 268 ); 269 270 context.commit('setMergedTimelineIndex', newIndex); 271 } 272 273 context.commit('setCurrentTimestamp', timestamp); 274 }, 275 advanceTimeline(context, direction) { 276 // NOTE: MergedTimeline is never considered to find the next closest index 277 // MergedTimeline only represented the timelines overlapped together and 278 // isn't considered an actual timeline. 279 280 if (direction !== DIRECTION.FORWARD && direction !== DIRECTION.BACKWARD) { 281 throw new Error("Unsupported direction provided."); 282 } 283 284 const consideredFiles = context.getters.timelineFiles 285 .filter(context.state.navigationFilesFilter); 286 287 let fileIndex = -1; 288 let timelineIndex; 289 let minTimeDiff = Infinity; 290 291 for (let idx = 0; idx < consideredFiles.length; idx++) { 292 const file = consideredFiles[idx]; 293 294 let candidateTimestampIndex = file.selectedIndex; 295 let candidateTimestamp = file.timeline[candidateTimestampIndex]; 296 297 let candidateCondition; 298 switch (direction) { 299 case DIRECTION.BACKWARD: 300 candidateCondition = () => candidateTimestamp < context.state.currentTimestamp; 301 break; 302 case DIRECTION.FORWARD: 303 candidateCondition = () => candidateTimestamp > context.state.currentTimestamp; 304 break; 305 } 306 307 if (!candidateCondition()) { 308 // Not a candidate — find a valid candidate 309 let noCandidate = false; 310 while (!candidateCondition()) { 311 candidateTimestampIndex += direction; 312 if (candidateTimestampIndex < 0 || candidateTimestampIndex >= file.timeline.length) { 313 noCandidate = true; 314 break; 315 } 316 candidateTimestamp = file.timeline[candidateTimestampIndex]; 317 } 318 319 if (noCandidate) { 320 continue; 321 } 322 } 323 324 const timeDiff = Math.abs(candidateTimestamp - context.state.currentTimestamp); 325 if (minTimeDiff > timeDiff) { 326 minTimeDiff = timeDiff; 327 fileIndex = idx; 328 timelineIndex = candidateTimestampIndex; 329 } 330 } 331 332 if (fileIndex >= 0) { 333 const closestFile = consideredFiles[fileIndex]; 334 const timestamp = parseInt(closestFile.timeline[timelineIndex]); 335 336 context.dispatch('updateTimelineTime', timestamp); 337 } 338 } 339 } 340}) 341 342new Vue({ 343 el: '#app', 344 store, // inject the Vuex store into all components 345 render: h => h(App) 346}) 347