• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {FrameMap} from './frame_map';
19import {
20  AbsoluteEntryIndex,
21  AbsoluteFrameIndex,
22  EntriesRange,
23  FramesRange,
24  RelativeEntryIndex,
25} from './index_types';
26import {Parser} from './parser';
27import {Timestamp, TimestampType} from './timestamp';
28import {TraceType} from './trace_type';
29
30export {
31  AbsoluteEntryIndex,
32  AbsoluteFrameIndex,
33  EntriesRange,
34  FramesRange,
35  RelativeEntryIndex,
36} from './index_types';
37
38export class TraceEntry<T> {
39  constructor(
40    private readonly fullTrace: Trace<T>,
41    private readonly parser: Parser<T>,
42    private readonly index: AbsoluteEntryIndex,
43    private readonly timestamp: Timestamp,
44    private readonly framesRange: FramesRange | undefined
45  ) {}
46
47  getFullTrace(): Trace<T> {
48    return this.fullTrace;
49  }
50
51  getIndex(): AbsoluteEntryIndex {
52    return this.index;
53  }
54
55  getTimestamp(): Timestamp {
56    return this.timestamp;
57  }
58
59  getFramesRange(): FramesRange | undefined {
60    if (!this.fullTrace.hasFrameInfo()) {
61      throw new Error(
62        `Trace ${this.fullTrace.type} can't be accessed in frame domain (no frame info available)`
63      );
64    }
65    return this.framesRange;
66  }
67
68  async getValue(): Promise<T> {
69    return await this.parser.getEntry(this.index, this.timestamp.getType());
70  }
71}
72
73export class Trace<T> {
74  readonly type: TraceType;
75  readonly lengthEntries: number;
76
77  private readonly parser: Parser<T>;
78  private readonly descriptors: string[];
79  private readonly fullTrace: Trace<T>;
80  private timestampType: TimestampType | undefined;
81  private readonly entriesRange: EntriesRange;
82  private frameMap?: FrameMap;
83  private framesRange?: FramesRange;
84
85  static newUninitializedTrace<T>(parser: Parser<T>): Trace<T> {
86    return new Trace(
87      parser.getTraceType(),
88      parser,
89      parser.getDescriptors(),
90      undefined,
91      undefined,
92      undefined
93    );
94  }
95
96  static newInitializedTrace<T>(
97    type: TraceType,
98    entryProvider: Parser<T>,
99    descriptors: string[],
100    timestampType: TimestampType,
101    entriesRange: EntriesRange
102  ): Trace<T> {
103    return new Trace(type, entryProvider, descriptors, undefined, timestampType, entriesRange);
104  }
105
106  init(timestampType: TimestampType) {
107    this.timestampType = timestampType;
108  }
109
110  private constructor(
111    type: TraceType,
112    parser: Parser<T>,
113    descriptors: string[],
114    fullTrace: Trace<T> | undefined,
115    timestampType: TimestampType | undefined,
116    entriesRange: EntriesRange | undefined
117  ) {
118    this.type = type;
119    this.parser = parser;
120    this.descriptors = descriptors;
121    this.fullTrace = fullTrace ?? this;
122    this.entriesRange = entriesRange ?? {start: 0, end: parser.getLengthEntries()};
123    this.lengthEntries = this.entriesRange.end - this.entriesRange.start;
124    this.timestampType = timestampType;
125  }
126
127  getDescriptors(): string[] {
128    return this.parser.getDescriptors();
129  }
130
131  getTimestampType(): TimestampType {
132    if (this.timestampType === undefined) {
133      throw new Error('Trace no fully initialized yet!');
134    }
135    return this.timestampType;
136  }
137
138  setFrameInfo(frameMap: FrameMap, framesRange: FramesRange | undefined) {
139    if (frameMap.lengthEntries !== this.fullTrace.lengthEntries) {
140      throw new Error('Attemped to set a frame map with incompatible number of entries');
141    }
142    this.frameMap = frameMap;
143    this.framesRange = framesRange;
144  }
145
146  hasFrameInfo(): boolean {
147    return this.frameMap !== undefined;
148  }
149
150  getEntry(index: RelativeEntryIndex): TraceEntry<T> {
151    const entry = this.convertToAbsoluteEntryIndex(index) as AbsoluteEntryIndex;
152    if (entry < this.entriesRange.start || entry >= this.entriesRange.end) {
153      throw new Error(
154        `Trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${this.lengthEntries}.`
155      );
156    }
157    const frames = this.clampFramesRangeToSliceBounds(
158      this.frameMap?.getFramesRange({start: entry, end: entry + 1})
159    );
160    return new TraceEntry<T>(
161      this.fullTrace,
162      this.parser,
163      entry,
164      this.getFullTraceTimestamps()[entry],
165      frames
166    );
167  }
168
169  getFrame(frame: AbsoluteFrameIndex): Trace<T> {
170    this.checkTraceCanBeAccessedInFrameDomain();
171    const entries = this.frameMap!.getEntriesRange({start: frame, end: frame + 1});
172    return this.createSlice(entries, {start: frame, end: frame + 1});
173  }
174
175  findClosestEntry(time: Timestamp): TraceEntry<T> | undefined {
176    this.checkTimestampIsCompatible(time);
177    if (this.lengthEntries === 0) {
178      return undefined;
179    }
180
181    const entry = this.clampEntryToSliceBounds(
182      ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), time)
183    );
184    if (entry === undefined || entry === this.entriesRange.end) {
185      return this.getEntry(this.lengthEntries - 1);
186    }
187
188    if (entry === this.entriesRange.start) {
189      return this.getEntry(0);
190    }
191
192    const abs = (time: bigint) => (time < 0 ? -time : time);
193    const timeDiff = abs(this.getFullTraceTimestamps()[entry].getValueNs() - time.getValueNs());
194    const prevEntry = entry - 1;
195    const prevTimeDiff = abs(
196      this.getFullTraceTimestamps()[prevEntry].getValueNs() - time.getValueNs()
197    );
198    if (prevTimeDiff < timeDiff) {
199      return this.getEntry(prevEntry - this.entriesRange.start);
200    }
201    return this.getEntry(entry - this.entriesRange.start);
202  }
203
204  findFirstGreaterOrEqualEntry(time: Timestamp): TraceEntry<T> | undefined {
205    this.checkTimestampIsCompatible(time);
206    if (this.lengthEntries === 0) {
207      return undefined;
208    }
209
210    const pos = this.clampEntryToSliceBounds(
211      ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), time)
212    );
213    if (pos === undefined || pos === this.entriesRange.end) {
214      return undefined;
215    }
216
217    const entry = this.getEntry(pos - this.entriesRange.start);
218    if (entry.getTimestamp() < time) {
219      return undefined;
220    }
221
222    return entry;
223  }
224
225  findFirstGreaterEntry(time: Timestamp): TraceEntry<T> | undefined {
226    this.checkTimestampIsCompatible(time);
227    if (this.lengthEntries === 0) {
228      return undefined;
229    }
230
231    const pos = this.clampEntryToSliceBounds(
232      ArrayUtils.binarySearchFirstGreater(this.getFullTraceTimestamps(), time)
233    );
234    if (pos === undefined || pos === this.entriesRange.end) {
235      return undefined;
236    }
237
238    const entry = this.getEntry(pos - this.entriesRange.start);
239    if (entry.getTimestamp() <= time) {
240      return undefined;
241    }
242
243    return entry;
244  }
245
246  findLastLowerOrEqualEntry(timestamp: Timestamp): TraceEntry<T> | undefined {
247    if (this.lengthEntries === 0) {
248      return undefined;
249    }
250    const firstGreater = this.findFirstGreaterEntry(timestamp);
251    if (!firstGreater) {
252      return this.getEntry(this.lengthEntries - 1);
253    }
254    if (firstGreater.getIndex() === this.entriesRange.start) {
255      return undefined;
256    }
257    return this.getEntry(firstGreater.getIndex() - this.entriesRange.start - 1);
258  }
259
260  findLastLowerEntry(timestamp: Timestamp): TraceEntry<T> | undefined {
261    if (this.lengthEntries === 0) {
262      return undefined;
263    }
264    const firstGreaterOrEqual = this.findFirstGreaterOrEqualEntry(timestamp);
265    if (!firstGreaterOrEqual) {
266      return this.getEntry(this.lengthEntries - 1);
267    }
268    if (firstGreaterOrEqual.getIndex() === this.entriesRange.start) {
269      return undefined;
270    }
271    return this.getEntry(firstGreaterOrEqual.getIndex() - this.entriesRange.start - 1);
272  }
273
274  sliceEntries(start?: RelativeEntryIndex, end?: RelativeEntryIndex): Trace<T> {
275    const startEntry =
276      this.clampEntryToSliceBounds(this.convertToAbsoluteEntryIndex(start)) ??
277      this.entriesRange.start;
278    const endEntry =
279      this.clampEntryToSliceBounds(this.convertToAbsoluteEntryIndex(end)) ?? this.entriesRange.end;
280    const entries: EntriesRange = {
281      start: startEntry,
282      end: endEntry,
283    };
284    const frames = this.frameMap?.getFramesRange(entries);
285    return this.createSlice(entries, frames);
286  }
287
288  sliceTime(start?: Timestamp, end?: Timestamp): Trace<T> {
289    this.checkTimestampIsCompatible(start);
290    this.checkTimestampIsCompatible(end);
291    const startEntry =
292      start === undefined
293        ? this.entriesRange.start
294        : this.clampEntryToSliceBounds(
295            ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), start)
296          ) ?? this.entriesRange.end;
297    const endEntry =
298      end === undefined
299        ? this.entriesRange.end
300        : this.clampEntryToSliceBounds(
301            ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), end)
302          ) ?? this.entriesRange.end;
303    const entries: EntriesRange = {
304      start: startEntry,
305      end: endEntry,
306    };
307    const frames = this.frameMap?.getFramesRange(entries);
308    return this.createSlice(entries, frames);
309  }
310
311  sliceFrames(start?: AbsoluteFrameIndex, end?: AbsoluteFrameIndex): Trace<T> {
312    this.checkTraceCanBeAccessedInFrameDomain();
313    if (!this.framesRange) {
314      return this.createSlice(undefined, undefined);
315    }
316    const frames: FramesRange = {
317      start: this.clampFrameToSliceBounds(start) ?? this.framesRange.start,
318      end: this.clampFrameToSliceBounds(end) ?? this.framesRange.end,
319    };
320    const entries = this.frameMap!.getEntriesRange(frames);
321    return this.createSlice(entries, frames);
322  }
323
324  forEachEntry(callback: (pos: TraceEntry<T>, index: RelativeEntryIndex) => void) {
325    for (let index = 0; index < this.lengthEntries; ++index) {
326      callback(this.getEntry(index), index);
327    }
328  }
329
330  mapEntry<U>(callback: (entry: TraceEntry<T>, index: RelativeEntryIndex) => U): U[] {
331    const result: U[] = [];
332    this.forEachEntry((entry, index) => {
333      result.push(callback(entry, index));
334    });
335    return result;
336  }
337
338  forEachTimestamp(callback: (timestamp: Timestamp, index: RelativeEntryIndex) => void) {
339    const timestamps = this.getFullTraceTimestamps();
340    for (let index = 0; index < this.lengthEntries; ++index) {
341      callback(timestamps[this.entriesRange.start + index], index);
342    }
343  }
344
345  forEachFrame(callback: (frame: Trace<T>, index: AbsoluteFrameIndex) => void) {
346    this.checkTraceCanBeAccessedInFrameDomain();
347    if (!this.framesRange) {
348      return;
349    }
350    for (let frame = this.framesRange.start; frame < this.framesRange.end; ++frame) {
351      callback(this.getFrame(frame), frame);
352    }
353  }
354
355  mapFrame<U>(callback: (frame: Trace<T>, index: AbsoluteFrameIndex) => U): U[] {
356    const result: U[] = [];
357    this.forEachFrame((traces, index) => {
358      result.push(callback(traces, index));
359    });
360    return result;
361  }
362
363  getFramesRange(): FramesRange | undefined {
364    this.checkTraceCanBeAccessedInFrameDomain();
365    return this.framesRange;
366  }
367
368  private getFullTraceTimestamps(): Timestamp[] {
369    if (this.timestampType === undefined) {
370      throw new Error('Forgot to initialize trace?');
371    }
372
373    const timestamps = this.parser.getTimestamps(this.timestampType);
374    if (!timestamps) {
375      throw new Error(`Timestamp type ${this.timestampType} is expected to be available`);
376    }
377    return timestamps;
378  }
379
380  private convertToAbsoluteEntryIndex(
381    index: RelativeEntryIndex | undefined
382  ): AbsoluteEntryIndex | undefined {
383    if (index === undefined) {
384      return undefined;
385    }
386    if (index < 0) {
387      return this.entriesRange.end + index;
388    }
389    return this.entriesRange.start + index;
390  }
391
392  private createSlice(
393    entries: EntriesRange | undefined,
394    frames: FramesRange | undefined
395  ): Trace<T> {
396    entries = this.clampEntriesRangeToSliceBounds(entries);
397    frames = this.clampFramesRangeToSliceBounds(frames);
398
399    if (entries === undefined || entries.start >= entries.end) {
400      entries = {
401        start: this.entriesRange.end,
402        end: this.entriesRange.end,
403      };
404    }
405
406    const slice = new Trace<T>(
407      this.type,
408      this.parser,
409      this.descriptors,
410      this.fullTrace,
411      this.timestampType,
412      entries
413    );
414
415    if (this.frameMap) {
416      slice.setFrameInfo(this.frameMap, frames);
417    }
418
419    return slice;
420  }
421
422  private clampEntriesRangeToSliceBounds(
423    entries: EntriesRange | undefined
424  ): EntriesRange | undefined {
425    if (entries === undefined) {
426      return undefined;
427    }
428    return {
429      start: this.clampEntryToSliceBounds(entries.start) as AbsoluteEntryIndex,
430      end: this.clampEntryToSliceBounds(entries.end) as AbsoluteEntryIndex,
431    };
432  }
433
434  private clampFramesRangeToSliceBounds(frames: FramesRange | undefined): FramesRange | undefined {
435    if (frames === undefined) {
436      return undefined;
437    }
438    return {
439      start: this.clampFrameToSliceBounds(frames.start) as AbsoluteFrameIndex,
440      end: this.clampFrameToSliceBounds(frames.end) as AbsoluteFrameIndex,
441    };
442  }
443
444  private clampEntryToSliceBounds(
445    entry: AbsoluteEntryIndex | undefined
446  ): AbsoluteEntryIndex | undefined {
447    if (entry === undefined) {
448      return undefined;
449    }
450    return Math.min(Math.max(entry, this.entriesRange.start), this.entriesRange.end);
451  }
452
453  private clampFrameToSliceBounds(
454    frame: AbsoluteFrameIndex | undefined
455  ): AbsoluteFrameIndex | undefined {
456    if (!this.framesRange || frame === undefined) {
457      return undefined;
458    }
459
460    if (frame < 0) {
461      throw new Error(`Absolute frame index cannot be negative. Found '${frame}'`);
462    }
463
464    return Math.min(Math.max(frame, this.framesRange.start), this.framesRange.end);
465  }
466
467  private checkTraceCanBeAccessedInFrameDomain() {
468    if (!this.frameMap) {
469      throw new Error(
470        `Trace ${this.type} can't be accessed in frame domain (no frame mapping available)`
471      );
472    }
473  }
474
475  private checkTimestampIsCompatible(timestamp?: Timestamp) {
476    if (!timestamp) {
477      return;
478    }
479    const timestamps = this.parser.getTimestamps(timestamp.getType());
480    if (timestamps === undefined) {
481      throw new Error(
482        `Trace ${this.type} can't be accessed using timestamp of type ${timestamp.getType()}`
483      );
484    }
485  }
486}
487