blob: ea6a69de5ce7b73f157a2446a1eeef5fe10acf9c [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {ArrayUtils} from 'common/array_utils';
import {Timestamp, TimestampType} from '../common/time';
import {
CustomQueryParamTypeMap,
CustomQueryParserResultTypeMap,
CustomQueryResultTypeMap,
CustomQueryType,
ProcessParserResult,
} from './custom_query';
import {FrameMap} from './frame_map';
import {
AbsoluteEntryIndex,
AbsoluteFrameIndex,
EntriesRange,
FramesRange,
RelativeEntryIndex,
} from './index_types';
import {Parser} from './parser';
import {TraceType} from './trace_type';
export {
AbsoluteEntryIndex,
AbsoluteFrameIndex,
EntriesRange,
FramesRange,
RelativeEntryIndex,
} from './index_types';
export abstract class TraceEntry<T> {
constructor(
protected readonly fullTrace: Trace<T>,
protected readonly parser: Parser<T>,
protected readonly index: AbsoluteEntryIndex,
protected readonly timestamp: Timestamp,
protected readonly framesRange: FramesRange | undefined
) {}
getFullTrace(): Trace<T> {
return this.fullTrace;
}
getIndex(): AbsoluteEntryIndex {
return this.index;
}
getTimestamp(): Timestamp {
return this.timestamp;
}
getFramesRange(): FramesRange | undefined {
if (!this.fullTrace.hasFrameInfo()) {
throw new Error(
`Trace ${this.fullTrace.type} can't be accessed in frame domain (no frame info available)`
);
}
return this.framesRange;
}
abstract getValue(): any;
}
export class TraceEntryLazy<T> extends TraceEntry<T> {
constructor(
fullTrace: Trace<T>,
parser: Parser<T>,
index: AbsoluteEntryIndex,
timestamp: Timestamp,
framesRange: FramesRange | undefined
) {
super(fullTrace, parser, index, timestamp, framesRange);
}
override async getValue(): Promise<T> {
return await this.parser.getEntry(this.index, this.timestamp.getType());
}
}
export class TraceEntryEager<T, U> extends TraceEntry<T> {
private readonly value: U;
constructor(
fullTrace: Trace<T>,
parser: Parser<T>,
index: AbsoluteEntryIndex,
timestamp: Timestamp,
framesRange: FramesRange | undefined,
value: U
) {
super(fullTrace, parser, index, timestamp, framesRange);
this.value = value;
}
override getValue(): U {
return this.value;
}
}
export class Trace<T> {
readonly type: TraceType;
readonly lengthEntries: number;
private readonly parser: Parser<T>;
private readonly descriptors: string[];
private readonly fullTrace: Trace<T>;
private timestampType: TimestampType;
private readonly entriesRange: EntriesRange;
private frameMap?: FrameMap;
private framesRange?: FramesRange;
static fromParser<T>(parser: Parser<T>, timestampType: TimestampType): Trace<T> {
return new Trace(
parser.getTraceType(),
parser,
parser.getDescriptors(),
undefined,
timestampType,
undefined
);
}
constructor(
type: TraceType,
parser: Parser<T>,
descriptors: string[],
fullTrace: Trace<T> | undefined,
timestampType: TimestampType,
entriesRange: EntriesRange | undefined
) {
this.type = type;
this.parser = parser;
this.descriptors = descriptors;
this.fullTrace = fullTrace ?? this;
this.entriesRange = entriesRange ?? {start: 0, end: parser.getLengthEntries()};
this.lengthEntries = this.entriesRange.end - this.entriesRange.start;
this.timestampType = timestampType;
}
getDescriptors(): string[] {
return this.parser.getDescriptors();
}
getTimestampType(): TimestampType {
if (this.timestampType === undefined) {
throw new Error('Trace no fully initialized yet!');
}
return this.timestampType;
}
setFrameInfo(frameMap: FrameMap, framesRange: FramesRange | undefined) {
if (frameMap.lengthEntries !== this.fullTrace.lengthEntries) {
throw new Error('Attemped to set a frame map with incompatible number of entries');
}
this.frameMap = frameMap;
this.framesRange = framesRange;
}
hasFrameInfo(): boolean {
return this.frameMap !== undefined;
}
getEntry(index: RelativeEntryIndex): TraceEntryLazy<T> {
return this.getEntryInternal(index, (index, timestamp, frames) => {
return new TraceEntryLazy<T>(this.fullTrace, this.parser, index, timestamp, frames);
});
}
async customQuery<Q extends CustomQueryType>(
type: Q,
param?: CustomQueryParamTypeMap[Q]
): Promise<CustomQueryResultTypeMap<T>[Q]> {
const makeTraceEntry = <U>(index: RelativeEntryIndex, value: U): TraceEntryEager<T, U> => {
return this.getEntryInternal(index, (index, timestamp, frames) => {
return new TraceEntryEager<T, U>(
this.fullTrace,
this.parser,
index,
timestamp,
frames,
value
);
});
};
const processParserResult = ProcessParserResult[type] as (
parserResult: CustomQueryParserResultTypeMap[Q],
make: typeof makeTraceEntry
) => CustomQueryResultTypeMap<T>[Q];
const parserResult = (await this.parser.customQuery(
type,
this.entriesRange,
param
)) as CustomQueryParserResultTypeMap[Q];
const finalResult = processParserResult(parserResult, makeTraceEntry);
return Promise.resolve(finalResult);
}
getFrame(frame: AbsoluteFrameIndex): Trace<T> {
this.checkTraceCanBeAccessedInFrameDomain();
const entries = this.frameMap!.getEntriesRange({start: frame, end: frame + 1});
return this.createSlice(entries, {start: frame, end: frame + 1});
}
findClosestEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
this.checkTimestampIsCompatible(time);
if (this.lengthEntries === 0) {
return undefined;
}
const entry = this.clampEntryToSliceBounds(
ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), time)
);
if (entry === undefined || entry === this.entriesRange.end) {
return this.getEntry(this.lengthEntries - 1);
}
if (entry === this.entriesRange.start) {
return this.getEntry(0);
}
const abs = (time: bigint) => (time < 0 ? -time : time);
const timeDiff = abs(this.getFullTraceTimestamps()[entry].getValueNs() - time.getValueNs());
const prevEntry = entry - 1;
const prevTimeDiff = abs(
this.getFullTraceTimestamps()[prevEntry].getValueNs() - time.getValueNs()
);
if (prevTimeDiff < timeDiff) {
return this.getEntry(prevEntry - this.entriesRange.start);
}
return this.getEntry(entry - this.entriesRange.start);
}
findFirstGreaterOrEqualEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
this.checkTimestampIsCompatible(time);
if (this.lengthEntries === 0) {
return undefined;
}
const pos = this.clampEntryToSliceBounds(
ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), time)
);
if (pos === undefined || pos === this.entriesRange.end) {
return undefined;
}
const entry = this.getEntry(pos - this.entriesRange.start);
if (entry.getTimestamp() < time) {
return undefined;
}
return entry;
}
findFirstGreaterEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
this.checkTimestampIsCompatible(time);
if (this.lengthEntries === 0) {
return undefined;
}
const pos = this.clampEntryToSliceBounds(
ArrayUtils.binarySearchFirstGreater(this.getFullTraceTimestamps(), time)
);
if (pos === undefined || pos === this.entriesRange.end) {
return undefined;
}
const entry = this.getEntry(pos - this.entriesRange.start);
if (entry.getTimestamp() <= time) {
return undefined;
}
return entry;
}
findLastLowerOrEqualEntry(timestamp: Timestamp): TraceEntryLazy<T> | undefined {
if (this.lengthEntries === 0) {
return undefined;
}
const firstGreater = this.findFirstGreaterEntry(timestamp);
if (!firstGreater) {
return this.getEntry(this.lengthEntries - 1);
}
if (firstGreater.getIndex() === this.entriesRange.start) {
return undefined;
}
return this.getEntry(firstGreater.getIndex() - this.entriesRange.start - 1);
}
findLastLowerEntry(timestamp: Timestamp): TraceEntryLazy<T> | undefined {
if (this.lengthEntries === 0) {
return undefined;
}
const firstGreaterOrEqual = this.findFirstGreaterOrEqualEntry(timestamp);
if (!firstGreaterOrEqual) {
return this.getEntry(this.lengthEntries - 1);
}
if (firstGreaterOrEqual.getIndex() === this.entriesRange.start) {
return undefined;
}
return this.getEntry(firstGreaterOrEqual.getIndex() - this.entriesRange.start - 1);
}
sliceEntries(start?: RelativeEntryIndex, end?: RelativeEntryIndex): Trace<T> {
const startEntry =
this.clampEntryToSliceBounds(this.convertToAbsoluteEntryIndex(start)) ??
this.entriesRange.start;
const endEntry =
this.clampEntryToSliceBounds(this.convertToAbsoluteEntryIndex(end)) ?? this.entriesRange.end;
const entries: EntriesRange = {
start: startEntry,
end: endEntry,
};
const frames = this.frameMap?.getFramesRange(entries);
return this.createSlice(entries, frames);
}
sliceTime(start?: Timestamp, end?: Timestamp): Trace<T> {
this.checkTimestampIsCompatible(start);
this.checkTimestampIsCompatible(end);
const startEntry =
start === undefined
? this.entriesRange.start
: this.clampEntryToSliceBounds(
ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), start)
) ?? this.entriesRange.end;
const endEntry =
end === undefined
? this.entriesRange.end
: this.clampEntryToSliceBounds(
ArrayUtils.binarySearchFirstGreaterOrEqual(this.getFullTraceTimestamps(), end)
) ?? this.entriesRange.end;
const entries: EntriesRange = {
start: startEntry,
end: endEntry,
};
const frames = this.frameMap?.getFramesRange(entries);
return this.createSlice(entries, frames);
}
sliceFrames(start?: AbsoluteFrameIndex, end?: AbsoluteFrameIndex): Trace<T> {
this.checkTraceCanBeAccessedInFrameDomain();
if (!this.framesRange) {
return this.createSlice(undefined, undefined);
}
const frames: FramesRange = {
start: this.clampFrameToSliceBounds(start) ?? this.framesRange.start,
end: this.clampFrameToSliceBounds(end) ?? this.framesRange.end,
};
const entries = this.frameMap!.getEntriesRange(frames);
return this.createSlice(entries, frames);
}
forEachEntry(callback: (pos: TraceEntryLazy<T>, index: RelativeEntryIndex) => void) {
for (let index = 0; index < this.lengthEntries; ++index) {
callback(this.getEntry(index), index);
}
}
mapEntry<U>(callback: (entry: TraceEntryLazy<T>, index: RelativeEntryIndex) => U): U[] {
const result: U[] = [];
this.forEachEntry((entry, index) => {
result.push(callback(entry, index));
});
return result;
}
forEachTimestamp(callback: (timestamp: Timestamp, index: RelativeEntryIndex) => void) {
const timestamps = this.getFullTraceTimestamps();
for (let index = 0; index < this.lengthEntries; ++index) {
callback(timestamps[this.entriesRange.start + index], index);
}
}
forEachFrame(callback: (frame: Trace<T>, index: AbsoluteFrameIndex) => void) {
this.checkTraceCanBeAccessedInFrameDomain();
if (!this.framesRange) {
return;
}
for (let frame = this.framesRange.start; frame < this.framesRange.end; ++frame) {
callback(this.getFrame(frame), frame);
}
}
mapFrame<U>(callback: (frame: Trace<T>, index: AbsoluteFrameIndex) => U): U[] {
const result: U[] = [];
this.forEachFrame((traces, index) => {
result.push(callback(traces, index));
});
return result;
}
getFramesRange(): FramesRange | undefined {
this.checkTraceCanBeAccessedInFrameDomain();
return this.framesRange;
}
private getEntryInternal<EntryType extends TraceEntryLazy<T> | TraceEntryEager<T, any>>(
index: RelativeEntryIndex,
makeEntry: (
absoluteIndex: AbsoluteEntryIndex,
timestamp: Timestamp,
frames: FramesRange | undefined
) => EntryType
): EntryType {
const absoluteIndex = this.convertToAbsoluteEntryIndex(index) as AbsoluteEntryIndex;
if (absoluteIndex < this.entriesRange.start || absoluteIndex >= this.entriesRange.end) {
throw new Error(
`Trace entry's index out of bounds. Input relative index: ${index}. Slice length: ${this.lengthEntries}.`
);
}
const timestamp = this.getFullTraceTimestamps()[absoluteIndex];
const frames = this.clampFramesRangeToSliceBounds(
this.frameMap?.getFramesRange({start: absoluteIndex, end: absoluteIndex + 1})
);
return makeEntry(absoluteIndex, timestamp, frames);
}
private getFullTraceTimestamps(): Timestamp[] {
if (this.timestampType === undefined) {
throw new Error('Forgot to initialize trace?');
}
const timestamps = this.parser.getTimestamps(this.timestampType);
if (!timestamps) {
throw new Error(`Timestamp type ${this.timestampType} is expected to be available`);
}
return timestamps;
}
private convertToAbsoluteEntryIndex(
index: RelativeEntryIndex | undefined
): AbsoluteEntryIndex | undefined {
if (index === undefined) {
return undefined;
}
if (index < 0) {
return this.entriesRange.end + index;
}
return this.entriesRange.start + index;
}
private createSlice(
entries: EntriesRange | undefined,
frames: FramesRange | undefined
): Trace<T> {
entries = this.clampEntriesRangeToSliceBounds(entries);
frames = this.clampFramesRangeToSliceBounds(frames);
if (entries === undefined || entries.start >= entries.end) {
entries = {
start: this.entriesRange.end,
end: this.entriesRange.end,
};
}
const slice = new Trace<T>(
this.type,
this.parser,
this.descriptors,
this.fullTrace,
this.timestampType,
entries
);
if (this.frameMap) {
slice.setFrameInfo(this.frameMap, frames);
}
return slice;
}
private clampEntriesRangeToSliceBounds(
entries: EntriesRange | undefined
): EntriesRange | undefined {
if (entries === undefined) {
return undefined;
}
return {
start: this.clampEntryToSliceBounds(entries.start) as AbsoluteEntryIndex,
end: this.clampEntryToSliceBounds(entries.end) as AbsoluteEntryIndex,
};
}
private clampFramesRangeToSliceBounds(frames: FramesRange | undefined): FramesRange | undefined {
if (frames === undefined) {
return undefined;
}
return {
start: this.clampFrameToSliceBounds(frames.start) as AbsoluteFrameIndex,
end: this.clampFrameToSliceBounds(frames.end) as AbsoluteFrameIndex,
};
}
private clampEntryToSliceBounds(
entry: AbsoluteEntryIndex | undefined
): AbsoluteEntryIndex | undefined {
if (entry === undefined) {
return undefined;
}
return Math.min(Math.max(entry, this.entriesRange.start), this.entriesRange.end);
}
private clampFrameToSliceBounds(
frame: AbsoluteFrameIndex | undefined
): AbsoluteFrameIndex | undefined {
if (!this.framesRange || frame === undefined) {
return undefined;
}
if (frame < 0) {
throw new Error(`Absolute frame index cannot be negative. Found '${frame}'`);
}
return Math.min(Math.max(frame, this.framesRange.start), this.framesRange.end);
}
private checkTraceCanBeAccessedInFrameDomain() {
if (!this.frameMap) {
throw new Error(
`Trace ${this.type} can't be accessed in frame domain (no frame mapping available)`
);
}
}
private checkTimestampIsCompatible(timestamp?: Timestamp) {
if (!timestamp) {
return;
}
const timestamps = this.parser.getTimestamps(timestamp.getType());
if (timestamps === undefined) {
throw new Error(
`Trace ${this.type} can't be accessed using timestamp of type ${timestamp.getType()}`
);
}
}
}