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 {TimeRange, Timestamp} from 'common/time';
18import {ComponentTimestampConverter} from 'common/timestamp_converter';
19import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
20import {Trace, TraceEntry} from 'trace/trace';
21import {Traces} from 'trace/traces';
22import {TraceEntryFinder} from 'trace/trace_entry_finder';
23import {TracePosition} from 'trace/trace_position';
24import {TraceType, TraceTypeUtils} from 'trace/trace_type';
25import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
26
27export class TimelineData {
28  private traces = new Traces();
29  private screenRecordingVideo?: Blob;
30  private firstEntry?: TraceEntry<object>;
31  private lastEntry?: TraceEntry<object>;
32  private explicitlySetPosition?: TracePosition;
33  private explicitlySetSelection?: TimeRange;
34  private explicitlySetZoomRange?: TimeRange;
35  private lastReturnedCurrentPosition?: TracePosition;
36  private lastReturnedFullTimeRange?: TimeRange;
37  private lastReturnedCurrentEntries = new Map<
38    Trace<object>,
39    TraceEntry<any> | undefined
40  >();
41  private activeTrace: Trace<object> | undefined;
42  private transitions: PropertyTreeNode[] = []; // cached trace entries to avoid TP and object creation latencies each time transition timeline is redrawn
43  private timestampConverter: ComponentTimestampConverter | undefined;
44
45  async initialize(
46    traces: Traces,
47    screenRecordingVideo: Blob | undefined,
48    timestampConverter: ComponentTimestampConverter,
49  ) {
50    this.clear();
51
52    this.timestampConverter = timestampConverter;
53
54    this.traces = new Traces();
55    traces.forEachTrace((trace, type) => {
56      // Filter out dumps with invalid timestamp (would mess up the timeline)
57      if (trace.isDumpWithoutTimestamp()) {
58        return;
59      }
60
61      this.traces.addTrace(trace);
62    });
63
64    const transitionTrace = this.traces.getTrace(TraceType.TRANSITION);
65    if (transitionTrace) {
66      this.transitions = await Promise.all(
67        transitionTrace.mapEntry(async (entry) => await entry.getValue()),
68      );
69    }
70
71    this.screenRecordingVideo = screenRecordingVideo;
72    this.firstEntry = this.findFirstEntry();
73    this.lastEntry = this.findLastEntry();
74
75    const tracesSortedByDisplayOrder = traces
76      .mapTrace((trace) => trace)
77      .filter(
78        (trace) =>
79          TraceTypeUtils.isTraceTypeWithViewer(trace.type) &&
80          trace.type !== TraceType.SCREEN_RECORDING,
81      )
82      .sort((a, b) => {
83        return TraceTypeUtils.compareByDisplayOrder(a.type, b.type);
84      });
85    if (tracesSortedByDisplayOrder.length > 0) {
86      this.trySetActiveTrace(tracesSortedByDisplayOrder[0]);
87    }
88  }
89
90  getTransitions(): PropertyTreeNode[] {
91    return this.transitions;
92  }
93
94  getTimestampConverter(): ComponentTimestampConverter | undefined {
95    return this.timestampConverter;
96  }
97
98  getCurrentPosition(): TracePosition | undefined {
99    if (this.explicitlySetPosition) {
100      return this.explicitlySetPosition;
101    }
102
103    let currentPosition: TracePosition | undefined = undefined;
104    if (this.firstEntry) {
105      currentPosition = TracePosition.fromTraceEntry(this.firstEntry);
106    }
107
108    const firstActiveEntry = this.getFirstEntryOfActiveViewTrace();
109    if (firstActiveEntry) {
110      currentPosition = TracePosition.fromTraceEntry(firstActiveEntry);
111    }
112
113    if (
114      this.lastReturnedCurrentPosition === undefined ||
115      currentPosition === undefined ||
116      !this.lastReturnedCurrentPosition.isEqual(currentPosition)
117    ) {
118      this.lastReturnedCurrentPosition = currentPosition;
119    }
120
121    return this.lastReturnedCurrentPosition;
122  }
123
124  setPosition(position: TracePosition | undefined) {
125    if (!this.hasTimestamps()) {
126      console.warn(
127        'Attempted to set position on traces with no timestamps/entries...',
128      );
129      return;
130    }
131
132    this.explicitlySetPosition = position;
133  }
134
135  makePositionFromActiveTrace(timestamp: Timestamp): TracePosition {
136    if (!this.activeTrace) {
137      return TracePosition.fromTimestamp(timestamp);
138    }
139
140    const entry = this.activeTrace.findClosestEntry(timestamp);
141    if (!entry) {
142      return TracePosition.fromTimestamp(timestamp);
143    }
144
145    return TracePosition.fromTraceEntry(entry, timestamp);
146  }
147
148  trySetActiveTrace(trace: Trace<object>): boolean {
149    const isTraceWithValidTimestamps = this.traces.hasTrace(trace);
150    if (this.activeTrace !== trace && isTraceWithValidTimestamps) {
151      this.activeTrace = trace;
152      return true;
153    }
154    return false;
155  }
156
157  getActiveTrace() {
158    return this.activeTrace;
159  }
160
161  getFullTimeRange(): TimeRange {
162    if (!this.firstEntry || !this.lastEntry) {
163      throw Error('Trying to get full time range when there are no timestamps');
164    }
165
166    const fullTimeRange = new TimeRange(
167      this.firstEntry.getTimestamp(),
168      this.lastEntry.getTimestamp(),
169    );
170
171    if (
172      this.lastReturnedFullTimeRange === undefined ||
173      this.lastReturnedFullTimeRange.from.getValueNs() !==
174        fullTimeRange.from.getValueNs() ||
175      this.lastReturnedFullTimeRange.to.getValueNs() !==
176        fullTimeRange.to.getValueNs()
177    ) {
178      this.lastReturnedFullTimeRange = fullTimeRange;
179    }
180
181    return this.lastReturnedFullTimeRange;
182  }
183
184  getSelectionTimeRange(): TimeRange {
185    if (this.explicitlySetSelection === undefined) {
186      return this.getFullTimeRange();
187    } else {
188      return this.explicitlySetSelection;
189    }
190  }
191
192  setSelectionTimeRange(selection: TimeRange) {
193    this.explicitlySetSelection = selection;
194  }
195
196  getZoomRange(): TimeRange {
197    if (this.explicitlySetZoomRange === undefined) {
198      return this.getFullTimeRange();
199    } else {
200      return this.explicitlySetZoomRange;
201    }
202  }
203
204  setZoom(zoomRange: TimeRange) {
205    this.explicitlySetZoomRange = zoomRange;
206  }
207
208  getTraces(): Traces {
209    return this.traces;
210  }
211
212  getScreenRecordingVideo(): Blob | undefined {
213    return this.screenRecordingVideo;
214  }
215
216  searchCorrespondingScreenRecordingTimeSeconds(
217    position: TracePosition,
218  ): number | undefined {
219    const trace = this.traces.getTrace(TraceType.SCREEN_RECORDING);
220    if (!trace || trace.lengthEntries === 0) {
221      return undefined;
222    }
223
224    const firstTimestamp = trace.getEntry(0).getTimestamp();
225    const entry = TraceEntryFinder.findCorrespondingEntry(trace, position);
226    if (!entry) {
227      return undefined;
228    }
229
230    return ScreenRecordingUtils.timestampToVideoTimeSeconds(
231      firstTimestamp.getValueNs(),
232      entry.getTimestamp().getValueNs(),
233    );
234  }
235
236  hasTimestamps(): boolean {
237    return this.firstEntry !== undefined;
238  }
239
240  hasMoreThanOneDistinctTimestamp(): boolean {
241    return (
242      this.hasTimestamps() &&
243      this.firstEntry?.getTimestamp().getValueNs() !==
244        this.lastEntry?.getTimestamp().getValueNs()
245    );
246  }
247
248  getPreviousEntryFor(trace: Trace<object>): TraceEntry<object> | undefined {
249    if (trace.lengthEntries === 0) {
250      return undefined;
251    }
252
253    const currentIndex = this.findCurrentEntryFor(trace)?.getIndex();
254    if (currentIndex === undefined || currentIndex === 0) {
255      return undefined;
256    }
257
258    return trace.getEntry(currentIndex - 1);
259  }
260
261  getNextEntryFor(trace: Trace<object>): TraceEntry<object> | undefined {
262    if (trace.lengthEntries === 0) {
263      return undefined;
264    }
265
266    const currentIndex = this.findCurrentEntryFor(trace)?.getIndex();
267    if (currentIndex === undefined) {
268      return trace.getEntry(0);
269    }
270
271    if (currentIndex + 1 >= trace.lengthEntries) {
272      return undefined;
273    }
274
275    return trace.getEntry(currentIndex + 1);
276  }
277
278  findCurrentEntryFor(trace: Trace<object>): TraceEntry<object> | undefined {
279    const position = this.getCurrentPosition();
280    if (!position) {
281      return undefined;
282    }
283
284    const entry = TraceEntryFinder.findCorrespondingEntry(trace, position);
285
286    if (
287      this.lastReturnedCurrentEntries.get(trace)?.getIndex() !==
288      entry?.getIndex()
289    ) {
290      this.lastReturnedCurrentEntries.set(trace, entry);
291    }
292
293    return this.lastReturnedCurrentEntries.get(trace);
294  }
295
296  moveToPreviousEntryFor(trace: Trace<object>) {
297    const prevEntry = this.getPreviousEntryFor(trace);
298    if (prevEntry !== undefined) {
299      this.setPosition(TracePosition.fromTraceEntry(prevEntry));
300    }
301  }
302
303  moveToNextEntryFor(trace: Trace<object>) {
304    const nextEntry = this.getNextEntryFor(trace);
305    if (nextEntry !== undefined) {
306      this.setPosition(TracePosition.fromTraceEntry(nextEntry));
307    }
308  }
309
310  clear() {
311    this.traces = new Traces();
312    this.firstEntry = undefined;
313    this.lastEntry = undefined;
314    this.explicitlySetPosition = undefined;
315    this.explicitlySetSelection = undefined;
316    this.lastReturnedCurrentPosition = undefined;
317    this.screenRecordingVideo = undefined;
318    this.lastReturnedFullTimeRange = undefined;
319    this.lastReturnedCurrentEntries.clear();
320    this.activeTrace = undefined;
321  }
322
323  private findFirstEntry(): TraceEntry<{}> | undefined {
324    let first: TraceEntry<{}> | undefined = undefined;
325
326    this.traces.forEachTrace((trace) => {
327      if (trace.lengthEntries === 0) {
328        return;
329      }
330      const candidate = trace.getEntry(0);
331      if (!first || candidate.getTimestamp() < first.getTimestamp()) {
332        first = candidate;
333      }
334    });
335
336    return first;
337  }
338
339  private findLastEntry(): TraceEntry<{}> | undefined {
340    let last: TraceEntry<{}> | undefined = undefined;
341
342    this.traces.forEachTrace((trace) => {
343      if (trace.lengthEntries === 0) {
344        return;
345      }
346      const candidate = trace.getEntry(trace.lengthEntries - 1);
347      if (!last || candidate.getTimestamp() > last.getTimestamp()) {
348        last = candidate;
349      }
350    });
351
352    return last;
353  }
354
355  private getFirstEntryOfActiveViewTrace(): TraceEntry<{}> | undefined {
356    if (!this.activeTrace || this.activeTrace.lengthEntries === 0) {
357      return undefined;
358    }
359    return this.activeTrace.getEntry(0);
360  }
361}
362