• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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