• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 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 {assertDefined} from 'common/assert_utils';
18import {TimeUtils} from 'common/time_utils';
19import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
20import {Timestamp, TimestampType} from 'trace/timestamp';
21import {TraceEntry} from 'trace/trace';
22import {Traces} from 'trace/traces';
23import {TraceEntryFinder} from 'trace/trace_entry_finder';
24import {TracePosition} from 'trace/trace_position';
25import {TraceType} from 'trace/trace_type';
26
27export interface TimeRange {
28  from: Timestamp;
29  to: Timestamp;
30}
31const INVALID_TIMESTAMP = 0n
32
33export class TimelineData {
34  private traces = new Traces();
35  private screenRecordingVideo?: Blob;
36  private timestampType?: TimestampType;
37  private firstEntry?: TraceEntry<{}>;
38  private lastEntry?: TraceEntry<{}>;
39  private explicitlySetPosition?: TracePosition;
40  private explicitlySetSelection?: TimeRange;
41  private activeViewTraceTypes: TraceType[] = []; // dependencies of current active view
42
43  initialize(traces: Traces, screenRecordingVideo: Blob | undefined) {
44    this.clear();
45
46    this.traces = new Traces();
47    traces.forEachTrace((trace, type) => {
48      if (type === TraceType.WINDOW_MANAGER) {
49        // Filter out WindowManager dumps with no timestamp from timeline
50        if (
51          trace.lengthEntries === 1 &&
52          trace.getEntry(0).getTimestamp().getValueNs() === INVALID_TIMESTAMP
53        ) {
54          return;
55        }
56      }
57
58      this.traces.setTrace(type, trace);
59    });
60
61    this.screenRecordingVideo = screenRecordingVideo;
62    this.firstEntry = this.findFirstEntry();
63    this.lastEntry = this.findLastEntry();
64    this.timestampType = this.firstEntry?.getTimestamp().getType();
65  }
66
67  getCurrentPosition(): TracePosition | undefined {
68    if (this.explicitlySetPosition) {
69      return this.explicitlySetPosition;
70    }
71    const firstActiveEntry = this.getFirstEntryOfActiveViewTraces();
72    if (firstActiveEntry) {
73      return TracePosition.fromTraceEntry(firstActiveEntry);
74    }
75    if (this.firstEntry) {
76      return TracePosition.fromTraceEntry(this.firstEntry);
77    }
78    return undefined;
79  }
80
81  setPosition(position: TracePosition | undefined) {
82    if (!this.hasTimestamps()) {
83      console.warn('Attempted to set position on traces with no timestamps/entries...');
84      return;
85    }
86
87    if (position) {
88      if (this.timestampType === undefined) {
89        throw Error('Attempted to set explicit position but no timestamp type is available');
90      }
91      if (position.timestamp.getType() !== this.timestampType) {
92        throw Error('Attempted to set explicit position with incompatible timestamp type');
93      }
94    }
95
96    this.explicitlySetPosition = position;
97  }
98
99  setActiveViewTraceTypes(types: TraceType[]) {
100    this.activeViewTraceTypes = types;
101  }
102
103  getTimestampType(): TimestampType | undefined {
104    return this.timestampType;
105  }
106
107  getFullTimeRange(): TimeRange {
108    if (!this.firstEntry || !this.lastEntry) {
109      throw Error('Trying to get full time range when there are no timestamps');
110    }
111    return {
112      from: this.firstEntry.getTimestamp(),
113      to: this.lastEntry.getTimestamp(),
114    };
115  }
116
117  getSelectionTimeRange(): TimeRange {
118    if (this.explicitlySetSelection === undefined) {
119      return this.getFullTimeRange();
120    } else {
121      return this.explicitlySetSelection;
122    }
123  }
124
125  setSelectionTimeRange(selection: TimeRange) {
126    this.explicitlySetSelection = selection;
127  }
128
129  getTraces(): Traces {
130    return this.traces;
131  }
132
133  getScreenRecordingVideo(): Blob | undefined {
134    return this.screenRecordingVideo;
135  }
136
137  searchCorrespondingScreenRecordingTimeSeconds(position: TracePosition): number | undefined {
138    const trace = this.traces.getTrace(TraceType.SCREEN_RECORDING);
139    if (!trace || trace.lengthEntries === 0) {
140      return undefined;
141    }
142
143    const firstTimestamp = trace.getEntry(0).getTimestamp();
144    const entry = TraceEntryFinder.findCorrespondingEntry(trace, position);
145    if (!entry) {
146      return undefined;
147    }
148
149    return ScreenRecordingUtils.timestampToVideoTimeSeconds(firstTimestamp, entry.getTimestamp());
150  }
151
152  hasTimestamps(): boolean {
153    return this.firstEntry !== undefined;
154  }
155
156  hasMoreThanOneDistinctTimestamp(): boolean {
157    return (
158      this.hasTimestamps() &&
159      this.firstEntry?.getTimestamp().getValueNs() !== this.lastEntry?.getTimestamp().getValueNs()
160    );
161  }
162
163  getPreviousEntryFor(type: TraceType): TraceEntry<{}> | undefined {
164    const trace = assertDefined(this.traces.getTrace(type));
165    if (trace.lengthEntries === 0) {
166      return undefined;
167    }
168
169    const currentIndex = this.findCurrentEntryFor(type)?.getIndex();
170    if (currentIndex === undefined || currentIndex === 0) {
171      return undefined;
172    }
173
174    return trace.getEntry(currentIndex - 1);
175  }
176
177  getNextEntryFor(type: TraceType): TraceEntry<{}> | undefined {
178    const trace = assertDefined(this.traces.getTrace(type));
179    if (trace.lengthEntries === 0) {
180      return undefined;
181    }
182
183    const currentIndex = this.findCurrentEntryFor(type)?.getIndex();
184    if (currentIndex === undefined) {
185      return trace.getEntry(0);
186    }
187
188    if (currentIndex + 1 >= trace.lengthEntries) {
189      return undefined;
190    }
191
192    return trace.getEntry(currentIndex + 1);
193  }
194
195  findCurrentEntryFor(type: TraceType): TraceEntry<{}> | undefined {
196    const position = this.getCurrentPosition();
197    if (!position) {
198      return undefined;
199    }
200    return TraceEntryFinder.findCorrespondingEntry(
201      assertDefined(this.traces.getTrace(type)),
202      position
203    );
204  }
205
206  moveToPreviousEntryFor(type: TraceType) {
207    const prevEntry = this.getPreviousEntryFor(type);
208    if (prevEntry !== undefined) {
209      this.setPosition(TracePosition.fromTraceEntry(prevEntry));
210    }
211  }
212
213  moveToNextEntryFor(type: TraceType) {
214    const nextEntry = this.getNextEntryFor(type);
215    if (nextEntry !== undefined) {
216      this.setPosition(TracePosition.fromTraceEntry(nextEntry));
217    }
218  }
219
220  clear() {
221    this.traces = new Traces();
222    this.firstEntry = undefined;
223    this.lastEntry = undefined;
224    this.explicitlySetPosition = undefined;
225    this.timestampType = undefined;
226    this.explicitlySetSelection = undefined;
227    this.screenRecordingVideo = undefined;
228    this.activeViewTraceTypes = [];
229  }
230
231  private findFirstEntry(): TraceEntry<{}> | undefined {
232    let first: TraceEntry<{}> | undefined = undefined;
233
234    this.traces.forEachTrace((trace) => {
235      if (trace.lengthEntries === 0) {
236        return;
237      }
238      const candidate = trace.getEntry(0);
239      if (!first || candidate.getTimestamp() < first.getTimestamp()) {
240        first = candidate;
241      }
242    });
243
244    return first;
245  }
246
247  private findLastEntry(): TraceEntry<{}> | undefined {
248    let last: TraceEntry<{}> | undefined = undefined;
249
250    this.traces.forEachTrace((trace) => {
251      if (trace.lengthEntries === 0) {
252        return;
253      }
254      const candidate = trace.getEntry(trace.lengthEntries - 1);
255      if (!last || candidate.getTimestamp() > last.getTimestamp()) {
256        last = candidate;
257      }
258    });
259
260    return last;
261  }
262
263  private getFirstEntryOfActiveViewTraces(): TraceEntry<{}> | undefined {
264    const activeEntries = this.activeViewTraceTypes
265      .filter((it) => this.traces.getTrace(it) !== undefined)
266      .map((traceType) => assertDefined(this.traces.getTrace(traceType)))
267      .filter((trace) => trace.lengthEntries > 0)
268      .map((trace) => trace.getEntry(0))
269      .sort((a, b) => {
270        return TimeUtils.compareFn(a.getTimestamp(), b.getTimestamp());
271      });
272    if (activeEntries.length === 0) {
273      return undefined;
274    }
275    return activeEntries[0];
276  }
277}
278