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