xref: /aosp_15_r20/development/tools/winscope/src/trace/trace.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 2023 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 {ArrayUtils} from 'common/array_utils';
18import {assertDefined} from 'common/assert_utils';
19import {INVALID_TIME_NS, Timestamp} from 'common/time';
20import {TimestampUtils} from 'common/timestamp_utils';
21import {TracesParserInput} from 'parsers/input/perfetto/traces_parser_input';
22import {AbstractParser as AbstractPerfettoParser} from 'parsers/perfetto/abstract_parser';
23import {
24  CustomQueryParamTypeMap,
25  CustomQueryParserResultTypeMap,
26  CustomQueryResultTypeMap,
27  CustomQueryType,
28  ProcessParserResult,
29} from './custom_query';
30import {FrameMap} from './frame_map';
31import {
32  AbsoluteEntryIndex,
33  AbsoluteFrameIndex,
34  EntriesRange,
35  FramesRange,
36  RelativeEntryIndex,
37} from './index_types';
38import {Parser} from './parser';
39import {TRACE_INFO} from './trace_info';
40import {TraceType} from './trace_type';
41
42export {
43  AbsoluteEntryIndex,
44  AbsoluteFrameIndex,
45  EntriesRange,
46  FramesRange,
47  RelativeEntryIndex,
48} from './index_types';
49
50export abstract class TraceEntry<T> {
51  constructor(
52    protected readonly fullTrace: Trace<T>,
53    protected readonly parser: Parser<T>,
54    protected readonly index: AbsoluteEntryIndex,
55    protected readonly timestamp: Timestamp,
56    protected readonly framesRange: FramesRange | undefined,
57  ) {}
58
59  getFullTrace(): Trace<T> {
60    return this.fullTrace;
61  }
62
63  getIndex(): AbsoluteEntryIndex {
64    return this.index;
65  }
66
67  getTimestamp(): Timestamp {
68    return this.timestamp;
69  }
70
71  hasValidTimestamp() {
72    return this.timestamp.getValueNs() !== INVALID_TIME_NS;
73  }
74
75  getFramesRange(): FramesRange | undefined {
76    if (!this.fullTrace.hasFrameInfo()) {
77      throw new Error(
78        `Trace ${
79          TRACE_INFO[this.fullTrace.type].name
80        } can't be accessed in frame domain (no frame info available)`,
81      );
82    }
83    return this.framesRange;
84  }
85
86  abstract getValue(): any;
87}
88
89export class TraceEntryLazy<T> extends TraceEntry<T> {
90  constructor(
91    fullTrace: Trace<T>,
92    parser: Parser<T>,
93    index: AbsoluteEntryIndex,
94    timestamp: Timestamp,
95    framesRange: FramesRange | undefined,
96  ) {
97    super(fullTrace, parser, index, timestamp, framesRange);
98  }
99
100  override async getValue(): Promise<T> {
101    try {
102      return await this.parser.getEntry(this.index);
103    } catch (e) {
104      this.fullTrace.setCorruptedState(
105        true,
106        `Cannot parse entry at index ${this.index}`,
107      );
108      throw e;
109    }
110  }
111}
112
113export class TraceEntryEager<T, U> extends TraceEntry<T> {
114  private readonly value: U;
115
116  constructor(
117    fullTrace: Trace<T>,
118    parser: Parser<T>,
119    index: AbsoluteEntryIndex,
120    timestamp: Timestamp,
121    framesRange: FramesRange | undefined,
122    value: U,
123  ) {
124    super(fullTrace, parser, index, timestamp, framesRange);
125    this.value = value;
126  }
127
128  override getValue(): U {
129    return this.value;
130  }
131}
132
133export class Trace<T> {
134  readonly type: TraceType;
135  readonly lengthEntries: number;
136
137  private readonly parser: Parser<T>;
138  private readonly descriptors: string[];
139  private readonly fullTrace: Trace<T>;
140  private readonly entriesRange: EntriesRange;
141  private frameMap?: FrameMap;
142  private framesRange?: FramesRange;
143  private corruptedState = false;
144  private corruptedReason: string | undefined;
145
146  static fromParser<T>(parser: Parser<T>): Trace<T> {
147    return new Trace(
148      parser.getTraceType(),
149      parser,
150      parser.getDescriptors(),
151      undefined,
152      undefined,
153    );
154  }
155
156  constructor(
157    type: TraceType,
158    parser: Parser<T>,
159    descriptors: string[],
160    fullTrace: Trace<T> | undefined,
161    entriesRange: EntriesRange | undefined,
162  ) {
163    this.type = type;
164    this.parser = parser;
165    this.descriptors = descriptors;
166    this.fullTrace = fullTrace ?? this;
167    this.entriesRange = entriesRange ?? {
168      start: 0,
169      end: parser.getLengthEntries(),
170    };
171    this.lengthEntries = this.entriesRange.end - this.entriesRange.start;
172  }
173
174  getDescriptors(): string[] {
175    return this.parser.getDescriptors();
176  }
177
178  getParser(): Parser<T> {
179    return this.parser;
180  }
181
182  canSearch(): boolean {
183    return [AbstractPerfettoParser, TracesParserInput].some(
184      (ParserType) => this.parser instanceof ParserType,
185    );
186  }
187
188  setFrameInfo(frameMap: FrameMap, framesRange: FramesRange | undefined) {
189    if (frameMap.lengthEntries !== this.fullTrace.lengthEntries) {
190      throw new Error(
191        `Attempted to set a frame map for ${
192          TRACE_INFO[this.type].name
193        } trace with incompatible number of entries`,
194      );
195    }
196    this.frameMap = frameMap;
197    this.framesRange = framesRange;
198  }
199
200  hasFrameInfo(): boolean {
201    return this.frameMap !== undefined;
202  }
203
204  getEntry(index: RelativeEntryIndex): TraceEntryLazy<T> {
205    return this.getEntryInternal(index, (index, timestamp, frames) => {
206      return new TraceEntryLazy<T>(
207        this.fullTrace,
208        this.parser,
209        index,
210        timestamp,
211        frames,
212      );
213    });
214  }
215
216  async customQuery<Q extends CustomQueryType>(
217    type: Q,
218    param?: CustomQueryParamTypeMap[Q],
219  ): Promise<CustomQueryResultTypeMap<T>[Q]> {
220    const makeTraceEntry = <U>(
221      index: RelativeEntryIndex,
222      value: U,
223    ): TraceEntryEager<T, U> => {
224      return this.getEntryInternal(index, (index, timestamp, frames) => {
225        return new TraceEntryEager<T, U>(
226          this.fullTrace,
227          this.parser,
228          index,
229          timestamp,
230          frames,
231          value,
232        );
233      });
234    };
235
236    const processParserResult = ProcessParserResult[type] as (
237      parserResult: CustomQueryParserResultTypeMap[Q],
238      make: typeof makeTraceEntry,
239    ) => CustomQueryResultTypeMap<T>[Q];
240
241    const parserResult = (await this.parser.customQuery(
242      type,
243      this.entriesRange,
244      param,
245    )) as CustomQueryParserResultTypeMap[Q];
246    const finalResult = processParserResult(parserResult, makeTraceEntry);
247    return Promise.resolve(finalResult);
248  }
249
250  getFrame(frame: AbsoluteFrameIndex): Trace<T> {
251    this.checkTraceCanBeAccessedInFrameDomain();
252    const entries = this.frameMap!.getEntriesRange({
253      start: frame,
254      end: frame + 1,
255    });
256    return this.createSlice(entries, {start: frame, end: frame + 1});
257  }
258
259  findClosestEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
260    if (this.lengthEntries === 0) {
261      return undefined;
262    }
263
264    const entry = this.clampEntryToSliceBounds(
265      ArrayUtils.binarySearchFirstGreaterOrEqual(
266        this.getFullTraceTimestamps(),
267        time,
268      ),
269    );
270    if (entry === undefined || entry === this.entriesRange.end) {
271      return this.getEntry(this.lengthEntries - 1);
272    }
273
274    if (entry === this.entriesRange.start) {
275      return this.getEntry(0);
276    }
277
278    const abs = (time: bigint) => (time < 0 ? -time : time);
279    const timeDiff = abs(
280      this.getFullTraceTimestamps()[entry].getValueNs() - time.getValueNs(),
281    );
282    const prevEntry = entry - 1;
283    const prevTimeDiff = abs(
284      this.getFullTraceTimestamps()[prevEntry].getValueNs() - time.getValueNs(),
285    );
286    if (prevTimeDiff < timeDiff) {
287      return this.getEntry(prevEntry - this.entriesRange.start);
288    }
289    return this.getEntry(entry - this.entriesRange.start);
290  }
291
292  findFirstGreaterOrEqualEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
293    if (this.lengthEntries === 0) {
294      return undefined;
295    }
296
297    const pos = this.clampEntryToSliceBounds(
298      ArrayUtils.binarySearchFirstGreaterOrEqual(
299        this.getFullTraceTimestamps(),
300        time,
301      ),
302    );
303    if (pos === undefined || pos === this.entriesRange.end) {
304      return undefined;
305    }
306
307    const entry = this.getEntry(pos - this.entriesRange.start);
308    if (entry.getTimestamp().getValueNs() < time.getValueNs()) {
309      return undefined;
310    }
311
312    return entry;
313  }
314
315  findFirstGreaterEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
316    if (this.lengthEntries === 0) {
317      return undefined;
318    }
319
320    const pos = this.clampEntryToSliceBounds(
321      ArrayUtils.binarySearchFirstGreater(this.getFullTraceTimestamps(), time),
322    );
323    if (pos === undefined || pos === this.entriesRange.end) {
324      return undefined;
325    }
326
327    const entry = this.getEntry(pos - this.entriesRange.start);
328    if (entry.getTimestamp().getValueNs() <= time.getValueNs()) {
329      return undefined;
330    }
331
332    return entry;
333  }
334
335  findLastLowerOrEqualEntry(
336    timestamp: Timestamp,
337  ): TraceEntryLazy<T> | undefined {
338    if (this.lengthEntries === 0) {
339      return undefined;
340    }
341    const firstGreater = this.findFirstGreaterEntry(timestamp);
342    if (!firstGreater) {
343      return this.getEntry(this.lengthEntries - 1);
344    }
345    if (firstGreater.getIndex() === this.entriesRange.start) {
346      return undefined;
347    }
348    return this.getEntry(firstGreater.getIndex() - this.entriesRange.start - 1);
349  }
350
351  findLastLowerEntry(timestamp: Timestamp): TraceEntryLazy<T> | undefined {
352    if (this.lengthEntries === 0) {
353      return undefined;
354    }
355    const firstGreaterOrEqual = this.findFirstGreaterOrEqualEntry(timestamp);
356    if (!firstGreaterOrEqual) {
357      return this.getEntry(this.lengthEntries - 1);
358    }
359    if (firstGreaterOrEqual.getIndex() === this.entriesRange.start) {
360      return undefined;
361    }
362    return this.getEntry(
363      firstGreaterOrEqual.getIndex() - this.entriesRange.start - 1,
364    );
365  }
366
367  sliceEntries(start?: RelativeEntryIndex, end?: RelativeEntryIndex): Trace<T> {
368    const startEntry =
369      this.clampEntryToSliceBounds(this.convertToAbsoluteEntryIndex(start)) ??
370      this.entriesRange.start;
371    const endEntry =
372      this.clampEntryToSliceBounds(this.convertToAbsoluteEntryIndex(end)) ??
373      this.entriesRange.end;
374    const entries: EntriesRange = {
375      start: startEntry,
376      end: endEntry,
377    };
378    const frames = this.frameMap?.getFramesRange(entries);
379    return this.createSlice(entries, frames);
380  }
381
382  sliceTime(start?: Timestamp, end?: Timestamp): Trace<T> {
383    const startEntry =
384      start === undefined
385        ? this.entriesRange.start
386        : this.clampEntryToSliceBounds(
387            ArrayUtils.binarySearchFirstGreaterOrEqual(
388              this.getFullTraceTimestamps(),
389              start,
390            ),
391          ) ?? this.entriesRange.end;
392    const endEntry =
393      end === undefined
394        ? this.entriesRange.end
395        : this.clampEntryToSliceBounds(
396            ArrayUtils.binarySearchFirstGreaterOrEqual(
397              this.getFullTraceTimestamps(),
398              end,
399            ),
400          ) ?? this.entriesRange.end;
401    const entries: EntriesRange = {
402      start: startEntry,
403      end: endEntry,
404    };
405    const frames = this.frameMap?.getFramesRange(entries);
406    return this.createSlice(entries, frames);
407  }
408
409  sliceFrames(start?: AbsoluteFrameIndex, end?: AbsoluteFrameIndex): Trace<T> {
410    this.checkTraceCanBeAccessedInFrameDomain();
411    if (!this.framesRange) {
412      return this.createSlice(undefined, undefined);
413    }
414    const frames: FramesRange = {
415      start: this.clampFrameToSliceBounds(start) ?? this.framesRange.start,
416      end: this.clampFrameToSliceBounds(end) ?? this.framesRange.end,
417    };
418    const entries = this.frameMap!.getEntriesRange(frames);
419    return this.createSlice(entries, frames);
420  }
421
422  forEachEntry(
423    callback: (pos: TraceEntryLazy<T>, index: RelativeEntryIndex) => void,
424  ) {
425    for (let index = 0; index < this.lengthEntries; ++index) {
426      callback(this.getEntry(index), index);
427    }
428  }
429
430  mapEntry<U>(
431    callback: (entry: TraceEntryLazy<T>, index: RelativeEntryIndex) => U,
432  ): U[] {
433    const result: U[] = [];
434    this.forEachEntry((entry, index) => {
435      result.push(callback(entry, index));
436    });
437    return result;
438  }
439
440  forEachTimestamp(
441    callback: (timestamp: Timestamp, index: RelativeEntryIndex) => void,
442  ) {
443    const timestamps = this.getFullTraceTimestamps();
444    for (let index = 0; index < this.lengthEntries; ++index) {
445      callback(timestamps[this.entriesRange.start + index], index);
446    }
447  }
448
449  forEachFrame(callback: (frame: Trace<T>, index: AbsoluteFrameIndex) => void) {
450    this.checkTraceCanBeAccessedInFrameDomain();
451    if (!this.framesRange) {
452      return;
453    }
454    for (
455      let frame = this.framesRange.start;
456      frame < this.framesRange.end;
457      ++frame
458    ) {
459      callback(this.getFrame(frame), frame);
460    }
461  }
462
463  mapFrame<U>(
464    callback: (frame: Trace<T>, index: AbsoluteFrameIndex) => U,
465  ): U[] {
466    const result: U[] = [];
467    this.forEachFrame((traces, index) => {
468      result.push(callback(traces, index));
469    });
470    return result;
471  }
472
473  getFramesRange(): FramesRange | undefined {
474    this.checkTraceCanBeAccessedInFrameDomain();
475    return this.framesRange;
476  }
477
478  isDump(): boolean {
479    return this.lengthEntries === 1;
480  }
481
482  isDumpWithoutTimestamp(): boolean {
483    return this.isDump() && !this.getEntry(0).hasValidTimestamp();
484  }
485
486  isCorrupted(): boolean {
487    return this.corruptedState;
488  }
489
490  getCorruptedReason(): string | undefined {
491    return this.corruptedReason;
492  }
493
494  setCorruptedState(value: boolean, reason?: string) {
495    this.corruptedState = value;
496    this.corruptedReason = reason;
497  }
498
499  spansMultipleDates(): boolean {
500    if (this.lengthEntries > 0) {
501      let firstTs: string | undefined;
502      let i = 0;
503      while (firstTs === undefined && i < this.lengthEntries) {
504        const entry = this.getEntry(i);
505        if (entry.hasValidTimestamp()) {
506          firstTs = entry.getTimestamp().format();
507          break;
508        }
509        i++;
510      }
511      const firstDate = TimestampUtils.extractDateFromHumanTimestamp(
512        assertDefined(firstTs),
513      );
514      if (firstDate) {
515        const lastDate = TimestampUtils.extractDateFromHumanTimestamp(
516          this.getEntry(this.lengthEntries - 1)
517            .getTimestamp()
518            .format(),
519        );
520        return firstDate !== lastDate;
521      }
522    }
523    return false;
524  }
525
526  private getEntryInternal<
527    EntryType extends TraceEntryLazy<T> | TraceEntryEager<T, any>,
528  >(
529    index: RelativeEntryIndex,
530    makeEntry: (
531      absoluteIndex: AbsoluteEntryIndex,
532      timestamp: Timestamp,
533      frames: FramesRange | undefined,
534    ) => EntryType,
535  ): EntryType {
536    const absoluteIndex = this.convertToAbsoluteEntryIndex(
537      index,
538    ) as AbsoluteEntryIndex;
539    if (
540      absoluteIndex < this.entriesRange.start ||
541      absoluteIndex >= this.entriesRange.end
542    ) {
543      throw new Error(
544        `${
545          TRACE_INFO[this.type].name
546        } trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${
547          this.lengthEntries
548        }.`,
549      );
550    }
551    const timestamp = this.getFullTraceTimestamps()[absoluteIndex];
552    const frames = this.clampFramesRangeToSliceBounds(
553      this.frameMap?.getFramesRange({
554        start: absoluteIndex,
555        end: absoluteIndex + 1,
556      }),
557    );
558    return makeEntry(absoluteIndex, timestamp, frames);
559  }
560
561  private getFullTraceTimestamps(): Timestamp[] {
562    const timestamps = this.parser.getTimestamps();
563    if (!timestamps) {
564      throw new Error(
565        `Timestamps expected to be available for this ${
566          TRACE_INFO[this.type].name
567        } trace.`,
568      );
569    }
570    return timestamps;
571  }
572
573  private convertToAbsoluteEntryIndex(
574    index: RelativeEntryIndex | undefined,
575  ): AbsoluteEntryIndex | undefined {
576    if (index === undefined) {
577      return undefined;
578    }
579    if (index < 0) {
580      return this.entriesRange.end + index;
581    }
582    return this.entriesRange.start + index;
583  }
584
585  private createSlice(
586    entries: EntriesRange | undefined,
587    frames: FramesRange | undefined,
588  ): Trace<T> {
589    entries = this.clampEntriesRangeToSliceBounds(entries);
590    frames = this.clampFramesRangeToSliceBounds(frames);
591
592    if (entries === undefined || entries.start >= entries.end) {
593      entries = {
594        start: this.entriesRange.end,
595        end: this.entriesRange.end,
596      };
597    }
598
599    const slice = new Trace<T>(
600      this.type,
601      this.parser,
602      this.descriptors,
603      this.fullTrace,
604      entries,
605    );
606
607    if (this.frameMap) {
608      slice.setFrameInfo(this.frameMap, frames);
609    }
610
611    return slice;
612  }
613
614  private clampEntriesRangeToSliceBounds(
615    entries: EntriesRange | undefined,
616  ): EntriesRange | undefined {
617    if (entries === undefined) {
618      return undefined;
619    }
620    return {
621      start: this.clampEntryToSliceBounds(entries.start) as AbsoluteEntryIndex,
622      end: this.clampEntryToSliceBounds(entries.end) as AbsoluteEntryIndex,
623    };
624  }
625
626  private clampFramesRangeToSliceBounds(
627    frames: FramesRange | undefined,
628  ): FramesRange | undefined {
629    if (frames === undefined) {
630      return undefined;
631    }
632    return {
633      start: this.clampFrameToSliceBounds(frames.start) as AbsoluteFrameIndex,
634      end: this.clampFrameToSliceBounds(frames.end) as AbsoluteFrameIndex,
635    };
636  }
637
638  private clampEntryToSliceBounds(
639    entry: AbsoluteEntryIndex | undefined,
640  ): AbsoluteEntryIndex | undefined {
641    if (entry === undefined) {
642      return undefined;
643    }
644    return Math.min(
645      Math.max(entry, this.entriesRange.start),
646      this.entriesRange.end,
647    );
648  }
649
650  private clampFrameToSliceBounds(
651    frame: AbsoluteFrameIndex | undefined,
652  ): AbsoluteFrameIndex | undefined {
653    if (!this.framesRange || frame === undefined) {
654      return undefined;
655    }
656
657    if (frame < 0) {
658      throw new Error(
659        `Absolute frame index cannot be negative. Found '${frame}'`,
660      );
661    }
662
663    return Math.min(
664      Math.max(frame, this.framesRange.start),
665      this.framesRange.end,
666    );
667  }
668
669  private checkTraceCanBeAccessedInFrameDomain() {
670    if (!this.frameMap) {
671      throw new Error(
672        `Trace ${
673          TRACE_INFO[this.type].name
674        } can't be accessed in frame domain (no frame mapping available)`,
675      );
676    }
677  }
678}
679