blob: 8117d30cf93f959db974859274028176d62ab5f1 [file] [log] [blame]
/*
* Copyright (C) 2022 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/utils/array_utils";
import {Timestamp, TimestampType} from "common/trace/timestamp";
import {TraceType} from "common/trace/trace_type";
import {Parser} from "parsers/parser";
import {ParserError, ParserFactory} from "parsers/parser_factory";
import { setTraces } from "trace_collection/set_traces";
import { Viewer } from "viewers/viewer";
import { ViewerFactory } from "viewers/viewer_factory";
import { LoadedTrace } from "app/loaded_trace";
import { FileUtils } from "common/utils/file_utils";
import { TRACE_INFO } from "app/trace_info";
import { TimelineCoordinator, TimestampChangeObserver, Timeline} from "./timeline_coordinator";
import { Inject, Injectable } from "@angular/core";
import { ScreenRecordingTraceEntry } from "common/trace/screen_recording";
@Injectable()
class TraceCoordinator implements TimestampChangeObserver {
private parsers: Parser[] = [];
private viewers: Viewer[] = [];
constructor(@Inject(TimelineCoordinator) private timelineCoordinator: TimelineCoordinator) {
this.timelineCoordinator.registerObserver(this);
}
public async setTraces(traces: File[]) {
traces = this.parsers.map(parser => parser.getTrace()).concat(traces);
let parserErrors: ParserError[];
[this.parsers, parserErrors] = await new ParserFactory().createParsers(traces);
this.addAllTracesToTimelineCoordinator();
this.addScreenRecodingTimeMappingToTraceCooordinator();
return parserErrors;
}
public removeTrace(type: TraceType) {
this.parsers = this.parsers.filter(parser => parser.getTraceType() !== type);
this.timelineCoordinator.removeTimeline(type);
if (type === TraceType.SCREEN_RECORDING) {
this.timelineCoordinator.removeScreenRecordingData();
}
}
private addAllTracesToTimelineCoordinator() {
const timelines: Timeline[] = this.parsers.map(parser => {
const timestamps = parser.getTimestamps(this.timestampTypeToUse());
if (timestamps === undefined) {
throw Error("Couldn't get timestamps from trace parser.");
}
return {traceType: parser.getTraceType(), timestamps: timestamps};
});
this.timelineCoordinator.setTimelines(timelines);
}
private addScreenRecodingTimeMappingToTraceCooordinator() {
const parser = this.getParserFor(TraceType.SCREEN_RECORDING);
if (parser === undefined) {
return;
}
const timestampMapping = new Map<Timestamp, number>();
let videoData: Blob|undefined = undefined;
for (const timestamp of parser.getTimestamps(this.timestampTypeToUse()) ?? []) {
const entry = parser.getTraceEntry(timestamp) as ScreenRecordingTraceEntry;
timestampMapping.set(timestamp, entry.videoTimeSeconds);
if (videoData === undefined) {
videoData = entry.videoData;
}
}
if (videoData === undefined) {
throw Error("No video data available!");
}
this.timelineCoordinator.setScreenRecordingData(videoData, timestampMapping);
}
private timestampTypeToUse() {
const priorityOrder = [TimestampType.REAL, TimestampType.ELAPSED];
for (const type of priorityOrder) {
if (this.parsers.every(it => it.getTimestamps(type) !== undefined)) {
return type;
}
}
throw Error("No common timestamp type across all traces");
}
public createViewers(storage: Storage) {
const activeTraceTypes = this.parsers.map(parser => parser.getTraceType());
this.viewers = new ViewerFactory().createViewers(new Set<TraceType>(activeTraceTypes), storage);
// Make sure to update the viewers active entries as soon as they are created.
if (this.timelineCoordinator.currentTimestamp) {
this.onCurrentTimestampChanged(this.timelineCoordinator.currentTimestamp);
}
}
public getLoadedTraces(): LoadedTrace[] {
return this.parsers.map((parser: Parser) => {
const name = (<File>parser.getTrace()).name;
const type = parser.getTraceType();
return {name: name, type: type};
});
}
public getParsers(): Parser[] {
return this.parsers;
}
public getViewers(): Viewer[] {
return this.viewers;
}
public findParser(traceType: TraceType): Parser | null {
const parser = this.parsers.find(parser => parser.getTraceType() === traceType);
return parser ?? null;
}
public onCurrentTimestampChanged(timestamp: Timestamp|undefined) {
const entries = this.getCurrentTraceEntries(timestamp);
this.viewers.forEach(viewer => {
viewer.notifyCurrentTraceEntries(entries);
});
}
private getCurrentTraceEntries(timestamp: Timestamp|undefined): Map<TraceType, any> {
const traceEntries: Map<TraceType, any> = new Map<TraceType, any>();
if (!timestamp) {
return traceEntries;
}
this.parsers.forEach(parser => {
const targetTimestamp = timestamp;
const entry = parser.getTraceEntry(targetTimestamp);
let prevEntry = null;
const parserTimestamps = parser.getTimestamps(timestamp.getType());
if (parserTimestamps === undefined) {
throw new Error(`Unexpected timestamp type ${timestamp.getType()}.`
+ ` Not supported by parser for trace type: ${parser.getTraceType()}`);
}
const index = ArrayUtils.binarySearchLowerOrEqual(parserTimestamps, targetTimestamp);
if (index !== undefined && index > 0) {
prevEntry = parser.getTraceEntry(parserTimestamps[index-1]);
}
if (entry !== undefined) {
traceEntries.set(parser.getTraceType(), [entry, prevEntry]);
}
});
return traceEntries;
}
public clearData() {
this.parsers = [];
this.viewers = [];
setTraces.dataReady = false;
this.timelineCoordinator.clearData();
}
public async getUnzippedFiles(files: File[]): Promise<File[]> {
const unzippedFiles: File[] = [];
for (let i=0; i<files.length; i++) {
if (FileUtils.isZipFile(files[i])) {
const unzippedFile = await FileUtils.unzipFile(files[i]);
unzippedFiles.push(...unzippedFile);
} else {
unzippedFiles.push(files[i]);
}
}
return unzippedFiles;
}
public async getTraceForDownload(parser: Parser): Promise<File | null> {
const trace = parser.getTrace();
if (trace) {
const traceType = TRACE_INFO[parser.getTraceType()].name;
const name = traceType + "/" + FileUtils.removeDirFromFileName(trace.name);
const blob = await trace.arrayBuffer();
return new File([blob], name);
}
return null;
}
public async getAllTracesForDownload(): Promise<File[]> {
const traces: File[] = [];
for (let i=0; i < this.parsers.length; i++) {
const trace = await this.getTraceForDownload(this.parsers[i]);
if (trace) {
traces.push(trace);
}
}
return traces;
}
public getParserFor(traceType: TraceType): undefined|Parser {
const matchingParsers = this.getParsers()
.filter((parser) => parser.getTraceType() === traceType);
if (matchingParsers.length === 0) {
return undefined;
}
if (matchingParsers.length > 1) {
throw Error(`Too many matching parsers for trace type ${traceType}. `);
}
return matchingParsers[0];
}
}
export { TraceCoordinator };