• 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'
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