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