Integrate "UI Traces API" with Winscope App/Core

Bug: b/256564627
Test: npm run build:all && npm run test:all
Change-Id: Ic434abc3031b9d53ddb6289fed747971e90c430e
This commit is contained in:
Kean Mariotti
2023-03-06 15:53:22 +00:00
parent 528f5b8f28
commit 3a01939e03
10 changed files with 449 additions and 506 deletions

View File

@@ -273,11 +273,7 @@ export class AppComponent implements TraceDataListener {
}
getLoadedTraceTypes(): TraceType[] {
return this.tracePipeline.getLoadedTraces().map((trace) => trace.type);
}
getVideoData(): Blob | undefined {
return this.timelineData.getScreenRecordingVideo();
return this.tracePipeline.getLoadedTraceFiles().map((trace) => trace.type);
}
onTraceDataLoaded(viewers: Viewer[]) {
@@ -325,8 +321,8 @@ export class AppComponent implements TraceDataListener {
private makeActiveTraceFileInfo(view: View): string {
const traceFile = this.tracePipeline
.getLoadedTraces()
.find((trace) => trace.type === view.dependencies[0])?.traceFile;
.getLoadedTraceFiles()
.find((file) => file.type === view.dependencies[0])?.traceFile;
if (!traceFile) {
return '';
@@ -340,7 +336,7 @@ export class AppComponent implements TraceDataListener {
}
private async makeTraceFilesForDownload(): Promise<File[]> {
return this.tracePipeline.getLoadedTraces().map((trace) => {
return this.tracePipeline.getLoadedTraceFiles().map((trace) => {
const traceType = TRACE_INFO[trace.type].name;
const newName = traceType + '/' + FileUtils.removeDirFromFileName(trace.traceFile.file.name);
return new File([trace.traceFile.file], newName);

View File

@@ -29,7 +29,7 @@ import {
import {MatSnackBar} from '@angular/material/snack-bar';
import {TracePipeline} from 'app/trace_pipeline';
import {PersistentStore} from 'common/persistent_store';
import {TraceFile} from 'trace/trace';
import {TraceFile} from 'trace/trace_file';
import {Connection} from 'trace_collection/connection';
import {ProxyState} from 'trace_collection/proxy_client';
import {ProxyConnection} from 'trace_collection/proxy_connection';
@@ -518,7 +518,7 @@ export class CollectTracesComponent implements OnInit, OnDestroy {
console.log('loading files', this.connect.adbData());
this.tracePipeline.clear();
const traceFiles = this.connect.adbData().map((file) => new TraceFile(file));
const parserErrors = await this.tracePipeline.loadTraces(traceFiles);
const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles);
ParserErrorSnackBarComponent.showIfNeeded(this.ngZone, this.snackBar, parserErrors);
this.traceDataLoaded.emit();
console.log('finished loading data!');

View File

@@ -27,7 +27,7 @@ import {TRACE_INFO} from 'app/trace_info';
import {TracePipeline} from 'app/trace_pipeline';
import {FileUtils, OnFile} from 'common/file_utils';
import {FilesDownloadListener} from 'interfaces/files_download_listener';
import {Trace, TraceFile} from 'trace/trace';
import {LoadedTraceFile, TraceFile} from 'trace/trace_file';
import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
@Component({
@@ -58,9 +58,9 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
</load-progress>
<mat-list
*ngIf="!isLoadingFiles && this.tracePipeline.getLoadedTraces().length > 0"
*ngIf="!isLoadingFiles && this.tracePipeline.getLoadedTraceFiles().length > 0"
class="uploaded-files">
<mat-list-item *ngFor="let trace of this.tracePipeline.getLoadedTraces()">
<mat-list-item *ngFor="let trace of this.tracePipeline.getLoadedTraceFiles()">
<mat-icon matListIcon>
{{ TRACE_INFO[trace.type].icon }}
</mat-icon>
@@ -74,7 +74,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
</mat-list>
<div
*ngIf="!isLoadingFiles && tracePipeline.getLoadedTraces().length === 0"
*ngIf="!isLoadingFiles && tracePipeline.getLoadedTraceFiles().length === 0"
class="drop-info">
<p class="mat-body-3 icon">
<mat-icon inline fontIcon="upload"></mat-icon>
@@ -84,7 +84,7 @@ import {ParserErrorSnackBarComponent} from './parser_error_snack_bar_component';
</mat-card-content>
<div
*ngIf="!isLoadingFiles && tracePipeline.getLoadedTraces().length > 0"
*ngIf="!isLoadingFiles && tracePipeline.getLoadedTraceFiles().length > 0"
class="trace-actions-container">
<button
color="primary"
@@ -231,10 +231,10 @@ export class UploadTracesComponent implements FilesDownloadListener {
await this.processFiles(Array.from(droppedFiles));
}
onRemoveTrace(event: MouseEvent, trace: Trace) {
onRemoveTrace(event: MouseEvent, trace: LoadedTraceFile) {
event.preventDefault();
event.stopPropagation();
this.tracePipeline.removeTrace(trace.type);
this.tracePipeline.removeTraceFile(trace.type);
this.changeDetectorRef.detectChanges();
}
@@ -267,7 +267,7 @@ export class UploadTracesComponent implements FilesDownloadListener {
this.progressMessage = 'Parsing files...';
this.changeDetectorRef.detectChanges();
const parserErrors = await this.tracePipeline.loadTraces(traceFiles, onProgressUpdate);
const parserErrors = await this.tracePipeline.loadTraceFiles(traceFiles, onProgressUpdate);
this.isLoadingFiles = false;
this.changeDetectorRef.detectChanges();

View File

@@ -20,9 +20,10 @@ import {RemoteBugreportReceiver} from 'interfaces/remote_bugreport_receiver';
import {RemoteTimestampReceiver} from 'interfaces/remote_timestamp_receiver';
import {RemoteTimestampSender} from 'interfaces/remote_timestamp_sender';
import {Runnable} from 'interfaces/runnable';
import {TimestampChangeListener} from 'interfaces/timestamp_change_listener';
import {TraceDataListener} from 'interfaces/trace_data_listener';
import {TracePositionUpdateListener} from 'interfaces/trace_position_update_listener';
import {Timestamp, TimestampType} from 'trace/timestamp';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {Viewer} from 'viewers/viewer';
import {ViewerFactory} from 'viewers/viewer_factory';
@@ -35,7 +36,7 @@ export type CrossToolProtocolDependencyInversion = RemoteBugreportReceiver &
export type AbtChromeExtensionProtocolDependencyInversion = BuganizerAttachmentsDownloadEmitter &
Runnable;
export type AppComponentDependencyInversion = TraceDataListener;
export type TimelineComponentDependencyInversion = TimestampChangeListener;
export type TimelineComponentDependencyInversion = TracePositionUpdateListener;
export type UploadTracesComponentDependencyInversion = FilesDownloadListener;
export class Mediator {
@@ -68,8 +69,8 @@ export class Mediator {
this.appComponent = appComponent;
this.storage = storage;
this.timelineData.setOnCurrentTimestampChanged((timestamp) => {
this.onWinscopeCurrentTimestampChanged(timestamp);
this.timelineData.setOnTracePositionUpdate((position) => {
this.onWinscopeTracePositionUpdate(position);
});
this.crossToolProtocol.setOnBugreportReceived(
@@ -112,29 +113,25 @@ export class Mediator {
}
onWinscopeTraceDataLoaded() {
this.processTraceData();
this.processTraces();
}
onWinscopeCurrentTimestampChanged(timestamp: Timestamp | undefined) {
onWinscopeTracePositionUpdate(position: TracePosition) {
this.executeIgnoringRecursiveTimestampNotifications(() => {
const entries = this.tracePipeline.getTraceEntries(timestamp);
this.viewers.forEach((viewer) => {
viewer.notifyCurrentTraceEntries(entries);
});
this.updateViewersTracePosition(position);
if (timestamp) {
if (timestamp.getType() !== TimestampType.REAL) {
console.warn(
'Cannot propagate timestamp change to remote tool.' +
` Remote tool expects timestamp type ${TimestampType.REAL},` +
` but Winscope wants to notify timestamp type ${timestamp.getType()}.`
);
} else {
this.crossToolProtocol.sendTimestamp(timestamp);
}
const timestamp = position.timestamp;
if (timestamp.getType() !== TimestampType.REAL) {
console.warn(
'Cannot propagate timestamp change to remote tool.' +
` Remote tool expects timestamp type ${TimestampType.REAL},` +
` but Winscope wants to notify timestamp type ${timestamp.getType()}.`
);
} else {
this.crossToolProtocol.sendTimestamp(timestamp);
}
this.timelineComponent?.onCurrentTimestampChanged(timestamp);
this.timelineComponent?.onTracePositionUpdate(position);
});
}
@@ -171,17 +168,16 @@ export class Mediator {
return;
}
if (this.timelineData.getCurrentTimestamp() === timestamp) {
if (
this.timelineData.getCurrentPosition()?.timestamp.getValueNs() === timestamp.getValueNs()
) {
return; // no timestamp change
}
const entries = this.tracePipeline.getTraceEntries(timestamp);
this.viewers.forEach((viewer) => {
viewer.notifyCurrentTraceEntries(entries);
});
this.timelineData.setCurrentTimestamp(timestamp);
this.timelineComponent?.onCurrentTimestampChanged(timestamp);
const position = TracePosition.fromTimestamp(timestamp);
this.updateViewersTracePosition(position);
this.timelineData.setPosition(position);
this.timelineComponent?.onTracePositionUpdate(position); //TODO: is this redundant?
});
}
@@ -190,9 +186,10 @@ export class Mediator {
this.uploadTracesComponent?.onFilesDownloaded(files);
}
private processTraceData() {
private processTraces() {
this.tracePipeline.buildTraces();
this.timelineData.initialize(
this.tracePipeline.getTimelines(),
this.tracePipeline.getTraces(),
this.tracePipeline.getScreenRecordingVideo()
);
this.createViewers();
@@ -205,15 +202,26 @@ export class Mediator {
}
private createViewers() {
const traceTypes = this.tracePipeline.getLoadedTraces().map((trace) => trace.type);
this.viewers = new ViewerFactory().createViewers(new Set<TraceType>(traceTypes), this.storage);
const traces = this.tracePipeline.getTraces();
const traceTypes = new Set<TraceType>();
traces.forEachTrace((trace) => {
traceTypes.add(trace.type);
});
this.viewers = new ViewerFactory().createViewers(traceTypes, traces, this.storage);
// Make sure to update the viewers active entries as soon as they are created.
if (this.timelineData.getCurrentTimestamp()) {
this.onWinscopeCurrentTimestampChanged(this.timelineData.getCurrentTimestamp());
// Update the viewers as soon as they are created
const position = this.timelineData.getCurrentPosition();
if (position) {
this.onWinscopeTracePositionUpdate(position);
}
}
private updateViewersTracePosition(position: TracePosition) {
this.viewers.forEach((viewer) => {
viewer.onTracePositionUpdate(position);
});
}
private executeIgnoringRecursiveTimestampNotifications(op: () => void) {
if (this.isChangingCurrentTimestamp) {
return;

View File

@@ -19,7 +19,8 @@ import {CrossToolProtocolStub} from 'cross_tool/cross_tool_protocol_stub';
import {MockStorage} from 'test/unit/mock_storage';
import {UnitTestUtils} from 'test/unit/utils';
import {RealTimestamp} from 'trace/timestamp';
import {TraceFile} from 'trace/trace';
import {TraceFile} from 'trace/trace_file';
import {TracePosition} from 'trace/trace_position';
import {ViewerFactory} from 'viewers/viewer_factory';
import {ViewerStub} from 'viewers/viewer_stub';
import {AppComponentStub} from './components/app_component_stub';
@@ -42,6 +43,8 @@ describe('Mediator', () => {
const TIMESTAMP_10 = new RealTimestamp(10n);
const TIMESTAMP_11 = new RealTimestamp(11n);
const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
beforeEach(async () => {
timelineComponent = new TimelineComponentStub();
@@ -69,18 +72,18 @@ describe('Mediator', () => {
it('handles data load event from Winscope', async () => {
spyOn(timelineData, 'initialize').and.callThrough();
spyOn(appComponent, 'onTraceDataLoaded');
spyOn(viewerStub, 'notifyCurrentTraceEntries');
spyOn(viewerStub, 'onTracePositionUpdate');
await loadTraces();
expect(timelineData.initialize).toHaveBeenCalledTimes(0);
expect(appComponent.onTraceDataLoaded).toHaveBeenCalledTimes(0);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(0);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
mediator.onWinscopeTraceDataLoaded();
expect(timelineData.initialize).toHaveBeenCalledTimes(1);
expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]);
// notifies viewer about current timestamp on creation
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
});
//TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with
@@ -107,95 +110,95 @@ describe('Mediator', () => {
expect(uploadTracesComponent.onFilesDownloaded).toHaveBeenCalledTimes(1);
});
it('propagates current timestamp changed through timeline', async () => {
it('propagates trace position update from timeline data', async () => {
await loadTraces();
mediator.onWinscopeTraceDataLoaded();
spyOn(viewerStub, 'notifyCurrentTraceEntries');
spyOn(timelineComponent, 'onCurrentTimestampChanged');
spyOn(viewerStub, 'onTracePositionUpdate');
spyOn(timelineComponent, 'onTracePositionUpdate');
spyOn(crossToolProtocol, 'sendTimestamp');
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(0);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
// notify timestamp
timelineData.setCurrentTimestamp(TIMESTAMP_10);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1);
timelineData.setPosition(POSITION_10);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1);
// notify same timestamp again (ignored, no timestamp change)
timelineData.setCurrentTimestamp(TIMESTAMP_10);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1);
timelineData.setPosition(POSITION_10);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1);
// notify another timestamp
timelineData.setCurrentTimestamp(TIMESTAMP_11);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(2);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(2);
timelineData.setPosition(POSITION_11);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2);
expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(2);
});
describe('timestamp received from remote tool', () => {
it('propagates timestamp changes', async () => {
it('propagates trace position update', async () => {
await loadTraces();
mediator.onWinscopeTraceDataLoaded();
spyOn(viewerStub, 'notifyCurrentTraceEntries');
spyOn(timelineComponent, 'onCurrentTimestampChanged');
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(0);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
spyOn(viewerStub, 'onTracePositionUpdate');
spyOn(timelineComponent, 'onTracePositionUpdate');
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
// receive timestamp
await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
// receive same timestamp again (ignored, no timestamp change)
await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(1);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1);
// receive another
await crossToolProtocol.onTimestampReceived(TIMESTAMP_11);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(2);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(2);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2);
});
it("doesn't propagate timestamp back to remote tool", async () => {
await loadTraces();
mediator.onWinscopeTraceDataLoaded();
spyOn(viewerStub, 'notifyCurrentTraceEntries');
spyOn(viewerStub, 'onTracePositionUpdate');
spyOn(crossToolProtocol, 'sendTimestamp');
// receive timestamp
await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
expect(viewerStub.notifyCurrentTraceEntries).toHaveBeenCalledTimes(1);
expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1);
expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0);
});
it('defers propagation till traces are loaded and visualized', async () => {
spyOn(timelineComponent, 'onCurrentTimestampChanged');
spyOn(timelineComponent, 'onTracePositionUpdate');
// keep timestamp for later
await crossToolProtocol.onTimestampReceived(TIMESTAMP_10);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
// keep timestamp for later (replace previous one)
await crossToolProtocol.onTimestampReceived(TIMESTAMP_11);
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0);
// apply timestamp
await loadTraces();
mediator.onWinscopeTraceDataLoaded();
expect(timelineComponent.onCurrentTimestampChanged).toHaveBeenCalledWith(TIMESTAMP_11);
expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledWith(POSITION_11);
});
});
const loadTraces = async () => {
const traces = [
const files = [
new TraceFile(
await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb')
),
@@ -208,7 +211,7 @@ describe('Mediator', () => {
)
),
];
const errors = await tracePipeline.loadTraces(traces);
const errors = await tracePipeline.loadTraceFiles(files);
expect(errors).toEqual([]);
};
});

View File

@@ -14,90 +14,89 @@
* limitations under the License.
*/
import {ArrayUtils} from 'common/array_utils';
import {FunctionUtils} from 'common/function_utils';
import {TimeUtils} from 'common/time_utils';
import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceEntry} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {Timeline} from './trace_pipeline';
import {assertDefined} from '../common/assert_utils';
export type TimestampCallbackType = (timestamp: Timestamp | undefined) => void;
export type TracePositionCallbackType = (position: TracePosition) => void;
export interface TimeRange {
from: Timestamp;
to: Timestamp;
}
interface TimestampWithIndex {
index: number;
timestamp: Timestamp;
}
export class TimelineData {
private timelines = new Map<TraceType, Timestamp[]>();
private timestampType?: TimestampType = undefined;
private explicitlySetTimestamp?: Timestamp = undefined;
private explicitlySetSelection?: TimeRange = undefined;
private screenRecordingVideo?: Blob = undefined;
private traces = new Traces();
private screenRecordingVideo?: Blob;
private timestampType?: TimestampType;
private firstEntry?: TraceEntry<{}>;
private lastEntry?: TraceEntry<{}>;
private explicitlySetPosition?: TracePosition;
private explicitlySetSelection?: TimeRange;
private activeViewTraceTypes: TraceType[] = []; // dependencies of current active view
private onCurrentTimestampChanged: TimestampCallbackType = FunctionUtils.DO_NOTHING;
private onTracePositionUpdate: TracePositionCallbackType = FunctionUtils.DO_NOTHING;
initialize(timelines: Timeline[], screenRecordingVideo: Blob | undefined) {
initialize(traces: Traces, screenRecordingVideo: Blob | undefined) {
this.clear();
this.traces = traces;
this.screenRecordingVideo = screenRecordingVideo;
this.firstEntry = this.findFirstEntry();
this.lastEntry = this.findLastEntry();
this.timestampType = this.firstEntry?.getTimestamp().getType();
const allTimestamps = timelines.flatMap((timeline) => timeline.timestamps);
if (allTimestamps.some((timestamp) => timestamp.getType() !== allTimestamps[0].getType())) {
throw Error('Added timeline has inconsistent timestamps.');
const position = this.getCurrentPosition();
if (position) {
this.onTracePositionUpdate(position);
}
if (allTimestamps.length > 0) {
this.timestampType = allTimestamps[0].getType();
}
timelines.forEach((timeline) => {
this.timelines.set(timeline.traceType, timeline.timestamps);
});
this.onCurrentTimestampChanged(this.getCurrentTimestamp());
}
setOnCurrentTimestampChanged(callback: TimestampCallbackType) {
this.onCurrentTimestampChanged = callback;
setOnTracePositionUpdate(callback: TracePositionCallbackType) {
this.onTracePositionUpdate = callback;
}
getCurrentTimestamp(): Timestamp | undefined {
if (this.explicitlySetTimestamp !== undefined) {
return this.explicitlySetTimestamp;
getCurrentPosition(): TracePosition | undefined {
if (this.explicitlySetPosition) {
return this.explicitlySetPosition;
}
if (this.getFirstTimestampOfActiveViewTraces() !== undefined) {
return this.getFirstTimestampOfActiveViewTraces();
const firstActiveEntry = this.getFirstEntryOfActiveViewTraces();
if (firstActiveEntry) {
return TracePosition.fromTraceEntry(firstActiveEntry);
}
return this.getFirstTimestamp();
if (this.firstEntry) {
return TracePosition.fromTraceEntry(this.firstEntry);
}
return undefined;
}
setCurrentTimestamp(timestamp: Timestamp | undefined) {
setPosition(position: TracePosition | undefined) {
if (!this.hasTimestamps()) {
console.warn('Attempted to set timestamp on traces with no timestamps/entries...');
console.warn('Attempted to set position on traces with no timestamps/entries...');
return;
}
if (timestamp !== undefined) {
if (position) {
if (this.timestampType === undefined) {
throw Error('Attempted to set explicit timestamp but no timestamp type is available');
throw Error('Attempted to set explicit position but no timestamp type is available');
}
if (timestamp.getType() !== this.timestampType) {
throw Error('Attempted to set explicit timestamp with incompatible type');
if (position.timestamp.getType() !== this.timestampType) {
throw Error('Attempted to set explicit position with incompatible timestamp type');
}
}
this.applyOperationAndNotifyIfCurrentTimestampChanged(() => {
this.explicitlySetTimestamp = timestamp;
this.applyOperationAndNotifyIfCurrentPositionChanged(() => {
this.explicitlySetPosition = position;
});
}
setActiveViewTraceTypes(types: TraceType[]) {
this.applyOperationAndNotifyIfCurrentTimestampChanged(() => {
this.applyOperationAndNotifyIfCurrentPositionChanged(() => {
this.activeViewTraceTypes = types;
});
}
@@ -106,130 +105,125 @@ export class TimelineData {
return this.timestampType;
}
getFullRange(): TimeRange {
if (!this.hasTimestamps()) {
throw Error('Trying to get full range when there are no timestamps');
getFullTimeRange(): TimeRange {
if (!this.firstEntry || !this.lastEntry) {
throw Error('Trying to get full time range when there are no timestamps');
}
return {
from: this.getFirstTimestamp()!,
to: this.getLastTimestamp()!,
from: this.firstEntry.getTimestamp(),
to: this.lastEntry.getTimestamp(),
};
}
getSelectionRange(): TimeRange {
getSelectionTimeRange(): TimeRange {
if (this.explicitlySetSelection === undefined) {
return this.getFullRange();
return this.getFullTimeRange();
} else {
return this.explicitlySetSelection;
}
}
setSelectionRange(selection: TimeRange) {
setSelectionTimeRange(selection: TimeRange) {
this.explicitlySetSelection = selection;
}
getTimelines(): Map<TraceType, Timestamp[]> {
return this.timelines;
getTraces(): Traces {
return this.traces;
}
getScreenRecordingVideo(): Blob | undefined {
return this.screenRecordingVideo;
}
searchCorrespondingScreenRecordingTimeSeconds(timestamp: Timestamp): number | undefined {
const timestamps = this.timelines.get(TraceType.SCREEN_RECORDING);
if (!timestamps) {
searchCorrespondingScreenRecordingTimeSeconds(position: TracePosition): number | undefined {
const trace = this.traces.getTrace(TraceType.SCREEN_RECORDING);
if (!trace || trace.lengthEntries === 0) {
return undefined;
}
const firstTimestamp = timestamps[0];
const correspondingTimestamp = this.searchCorrespondingTimestampFor(
TraceType.SCREEN_RECORDING,
timestamp
)?.timestamp;
if (correspondingTimestamp === undefined) {
const firstTimestamp = trace.getEntry(0).getTimestamp();
const entry = TraceEntryFinder.findCorrespondingEntry(trace, position);
if (!entry) {
return undefined;
}
return ScreenRecordingUtils.timestampToVideoTimeSeconds(firstTimestamp, correspondingTimestamp);
return ScreenRecordingUtils.timestampToVideoTimeSeconds(firstTimestamp, entry.getTimestamp());
}
hasTimestamps(): boolean {
return Array.from(this.timelines.values()).some((timestamps) => timestamps.length > 0);
return this.firstEntry !== undefined;
}
hasMoreThanOneDistinctTimestamp(): boolean {
return this.hasTimestamps() && this.getFirstTimestamp() !== this.getLastTimestamp();
return (
this.hasTimestamps() &&
this.firstEntry?.getTimestamp().getValueNs() !== this.lastEntry?.getTimestamp().getValueNs()
);
}
getCurrentTimestampFor(type: TraceType): Timestamp | undefined {
return this.searchCorrespondingTimestampFor(type, this.getCurrentTimestamp())?.timestamp;
getPreviousEntryFor(type: TraceType): TraceEntry<{}> | undefined {
const trace = assertDefined(this.traces.getTrace(type));
if (trace.lengthEntries === 0) {
return undefined;
}
const currentIndex = this.findCurrentEntryFor(type)?.getIndex();
if (currentIndex === undefined || currentIndex === 0) {
return undefined;
}
return trace.getEntry(currentIndex - 1);
}
getPreviousTimestampFor(type: TraceType): Timestamp | undefined {
const currentIndex = this.searchCorrespondingTimestampFor(
type,
this.getCurrentTimestamp()
)?.index;
getNextEntryFor(type: TraceType): TraceEntry<{}> | undefined {
const trace = assertDefined(this.traces.getTrace(type));
if (trace.lengthEntries === 0) {
return undefined;
}
const currentIndex = this.findCurrentEntryFor(type)?.getIndex();
if (currentIndex === undefined) {
// Only acceptable reason for this to be undefined is if we are before the first entry for this type
if (
this.timelines.get(type)!.length === 0 ||
this.getCurrentTimestamp()!.getValueNs() < this.timelines.get(type)![0].getValueNs()
) {
return undefined;
}
throw Error(`Missing active timestamp for trace type ${type}`);
return trace.getEntry(0);
}
const previousIndex = currentIndex - 1;
if (previousIndex < 0) {
if (currentIndex + 1 >= trace.lengthEntries) {
return undefined;
}
return this.timelines.get(type)?.[previousIndex];
return trace.getEntry(currentIndex + 1);
}
getNextTimestampFor(type: TraceType): Timestamp | undefined {
const currentIndex =
this.searchCorrespondingTimestampFor(type, this.getCurrentTimestamp())?.index ?? -1;
if (this.timelines.get(type)?.length === 0 ?? true) {
throw Error(`Missing active timestamp for trace type ${type}`);
}
const timestamps = this.timelines.get(type);
if (timestamps === undefined) {
throw Error('Timestamps for tracetype not found');
}
const nextIndex = currentIndex + 1;
if (nextIndex >= timestamps.length) {
findCurrentEntryFor(type: TraceType): TraceEntry<{}> | undefined {
const position = this.getCurrentPosition();
if (!position) {
return undefined;
}
return timestamps[nextIndex];
return TraceEntryFinder.findCorrespondingEntry(
assertDefined(this.traces.getTrace(type)),
position
);
}
moveToPreviousTimestampFor(type: TraceType) {
const prevTimestamp = this.getPreviousTimestampFor(type);
if (prevTimestamp !== undefined) {
this.setCurrentTimestamp(prevTimestamp);
moveToPreviousEntryFor(type: TraceType) {
const prevEntry = this.getPreviousEntryFor(type);
if (prevEntry !== undefined) {
this.setPosition(TracePosition.fromTraceEntry(prevEntry));
}
}
moveToNextTimestampFor(type: TraceType) {
const nextTimestamp = this.getNextTimestampFor(type);
if (nextTimestamp !== undefined) {
this.setCurrentTimestamp(nextTimestamp);
moveToNextEntryFor(type: TraceType) {
const nextEntry = this.getNextEntryFor(type);
if (nextEntry !== undefined) {
this.setPosition(TracePosition.fromTraceEntry(nextEntry));
}
}
clear() {
this.applyOperationAndNotifyIfCurrentTimestampChanged(() => {
this.timelines.clear();
this.explicitlySetTimestamp = undefined;
this.applyOperationAndNotifyIfCurrentPositionChanged(() => {
this.traces = new Traces();
this.firstEntry = undefined;
this.lastEntry = undefined;
this.explicitlySetPosition = undefined;
this.timestampType = undefined;
this.explicitlySetSelection = undefined;
this.screenRecordingVideo = undefined;
@@ -237,71 +231,58 @@ export class TimelineData {
});
}
private getFirstTimestamp(): Timestamp | undefined {
if (!this.hasTimestamps()) {
return undefined;
}
private findFirstEntry(): TraceEntry<{}> | undefined {
let first: TraceEntry<{}> | undefined = undefined;
return Array.from(this.timelines.values())
.map((timestamps) => timestamps[0])
.filter((timestamp) => timestamp !== undefined)
.reduce((prev, current) => (prev < current ? prev : current));
this.traces.forEachTrace((trace) => {
if (trace.lengthEntries === 0) {
return;
}
const candidate = trace.getEntry(0);
if (!first || candidate.getTimestamp() < first.getTimestamp()) {
first = candidate;
}
});
return first;
}
private getLastTimestamp(): Timestamp | undefined {
if (!this.hasTimestamps()) {
return undefined;
}
private findLastEntry(): TraceEntry<{}> | undefined {
let last: TraceEntry<{}> | undefined = undefined;
return Array.from(this.timelines.values())
.map((timestamps) => timestamps[timestamps.length - 1])
.filter((timestamp) => timestamp !== undefined)
.reduce((prev, current) => (prev > current ? prev : current));
this.traces.forEachTrace((trace) => {
if (trace.lengthEntries === 0) {
return;
}
const candidate = trace.getEntry(trace.lengthEntries - 1);
if (!last || candidate.getTimestamp() > last.getTimestamp()) {
last = candidate;
}
});
return last;
}
private searchCorrespondingTimestampFor(
type: TraceType,
timestamp: Timestamp | undefined
): TimestampWithIndex | undefined {
if (timestamp === undefined) {
private getFirstEntryOfActiveViewTraces(): TraceEntry<{}> | undefined {
const activeEntries = this.activeViewTraceTypes
.map((traceType) => assertDefined(this.traces.getTrace(traceType)))
.filter((trace) => trace.lengthEntries > 0)
.map((trace) => trace.getEntry(0))
.sort((a, b) => {
return TimeUtils.compareFn(a.getTimestamp(), b.getTimestamp());
});
if (activeEntries.length === 0) {
return undefined;
}
if (timestamp.getType() !== this.timestampType) {
throw Error('Invalid timestamp type');
}
const timeline = this.timelines.get(type);
if (timeline === undefined) {
throw Error(`No timeline for requested trace type ${type}`);
}
const index = ArrayUtils.binarySearchLowerOrEqual(timeline, timestamp);
if (index === undefined) {
return undefined;
}
return {index, timestamp: timeline[index]};
return activeEntries[0];
}
private getFirstTimestampOfActiveViewTraces(): Timestamp | undefined {
if (this.activeViewTraceTypes.length === 0) {
return undefined;
}
const activeTimestamps = this.activeViewTraceTypes
.map((traceType) => this.timelines.get(traceType)!)
.map((timestamps) => timestamps[0])
.filter((timestamp) => timestamp !== undefined)
.sort(TimeUtils.compareFn);
if (activeTimestamps.length === 0) {
return undefined;
}
return activeTimestamps[0];
}
private applyOperationAndNotifyIfCurrentTimestampChanged(op: () => void) {
const prevTimestamp = this.getCurrentTimestamp();
private applyOperationAndNotifyIfCurrentPositionChanged(op: () => void) {
const prevPosition = this.getCurrentPosition();
op();
if (prevTimestamp !== this.getCurrentTimestamp()) {
this.onCurrentTimestampChanged(this.getCurrentTimestamp());
const currentPosition = this.getCurrentPosition();
if (currentPosition && (!prevPosition || !currentPosition.isEqual(prevPosition))) {
this.onTracePositionUpdate(currentPosition);
}
}
}

View File

@@ -14,169 +14,162 @@
* limitations under the License.
*/
import {Timestamp, TimestampType} from 'trace/timestamp';
import {TracesBuilder} from 'test/unit/traces_builder';
import {RealTimestamp, Timestamp, TimestampType} from 'trace/timestamp';
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {TimelineData} from './timeline_data';
import {Timeline} from './trace_pipeline';
class TimestampChangedObserver {
onCurrentTimestampChanged(timestamp: Timestamp | undefined) {
class TracePositionUpdateListener {
onTracePositionUpdate(position: TracePosition) {
// do nothing
}
}
describe('TimelineData', () => {
let timelineData: TimelineData;
const timestampChangedObserver = new TimestampChangedObserver();
const positionUpdateListener = new TracePositionUpdateListener();
const timestamp10 = new Timestamp(TimestampType.REAL, 10n);
const timestamp11 = new Timestamp(TimestampType.REAL, 11n);
const timelines: Timeline[] = [
{
traceType: TraceType.SURFACE_FLINGER,
timestamps: [timestamp10],
},
{
traceType: TraceType.WINDOW_MANAGER,
timestamps: [timestamp11],
},
];
const traces = new TracesBuilder()
.setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
.setTimestamps(TraceType.WINDOW_MANAGER, [timestamp11])
.build();
const position10 = TracePosition.fromTraceEntry(
traces.getTrace(TraceType.SURFACE_FLINGER)!.getEntry(0)
);
const position11 = TracePosition.fromTraceEntry(
traces.getTrace(TraceType.WINDOW_MANAGER)!.getEntry(0)
);
beforeEach(() => {
timelineData = new TimelineData();
timelineData.setOnCurrentTimestampChanged((timestamp) => {
timestampChangedObserver.onCurrentTimestampChanged(timestamp);
timelineData.setOnTracePositionUpdate((position) => {
positionUpdateListener.onTracePositionUpdate(position);
});
});
it('sets timelines', () => {
expect(timelineData.getCurrentTimestamp()).toBeUndefined();
it('can be initialized', () => {
expect(timelineData.getCurrentPosition()).toBeUndefined();
timelineData.initialize(timelines, undefined);
expect(timelineData.getCurrentTimestamp()).toEqual(timestamp10);
timelineData.initialize(traces, undefined);
expect(timelineData.getCurrentPosition()).toBeDefined();
});
it('uses first timestamp by default', () => {
timelineData.initialize(timelines, undefined);
expect(timelineData.getCurrentTimestamp()?.getValueNs()).toEqual(10n);
it('uses first entry by default', () => {
timelineData.initialize(traces, undefined);
expect(timelineData.getCurrentPosition()).toEqual(position10);
});
it('uses explicit timestamp if set', () => {
timelineData.initialize(timelines, undefined);
expect(timelineData.getCurrentTimestamp()?.getValueNs()).toEqual(10n);
it('uses explicit position if set', () => {
timelineData.initialize(traces, undefined);
expect(timelineData.getCurrentPosition()).toEqual(position10);
const explicitTimestamp = new Timestamp(TimestampType.REAL, 1000n);
timelineData.setCurrentTimestamp(explicitTimestamp);
expect(timelineData.getCurrentTimestamp()).toEqual(explicitTimestamp);
const explicitPosition = TracePosition.fromTimestamp(new RealTimestamp(1000n));
timelineData.setPosition(explicitPosition);
expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
expect(timelineData.getCurrentTimestamp()).toEqual(explicitTimestamp);
expect(timelineData.getCurrentPosition()).toEqual(explicitPosition);
});
it('sets active trace types and update current timestamp accordingly', () => {
timelineData.initialize(timelines, undefined);
it('sets active trace types and update current position accordingly', () => {
timelineData.initialize(traces, undefined);
timelineData.setActiveViewTraceTypes([]);
expect(timelineData.getCurrentTimestamp()).toEqual(timestamp10);
expect(timelineData.getCurrentPosition()).toEqual(position10);
timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
expect(timelineData.getCurrentTimestamp()).toEqual(timestamp11);
expect(timelineData.getCurrentPosition()).toEqual(position11);
timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
expect(timelineData.getCurrentTimestamp()).toEqual(timestamp10);
expect(timelineData.getCurrentPosition()).toEqual(position10);
timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]);
expect(timelineData.getCurrentTimestamp()).toEqual(timestamp10);
expect(timelineData.getCurrentPosition()).toEqual(position10);
});
it('notifies callback when current timestamp changes', () => {
spyOn(timestampChangedObserver, 'onCurrentTimestampChanged');
expect(timestampChangedObserver.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
it('executes callback on position update', () => {
spyOn(positionUpdateListener, 'onTracePositionUpdate');
expect(positionUpdateListener.onTracePositionUpdate).toHaveBeenCalledTimes(0);
timelineData.initialize(timelines, undefined);
expect(timestampChangedObserver.onCurrentTimestampChanged).toHaveBeenCalledTimes(1);
timelineData.initialize(traces, undefined);
expect(positionUpdateListener.onTracePositionUpdate).toHaveBeenCalledTimes(1);
timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
expect(timestampChangedObserver.onCurrentTimestampChanged).toHaveBeenCalledTimes(2);
expect(positionUpdateListener.onTracePositionUpdate).toHaveBeenCalledTimes(2);
});
it("doesn't notify observers when current timestamp doesn't change", () => {
timelineData.initialize(timelines, undefined);
it("doesn't execute callback when position doesn't change", () => {
timelineData.initialize(traces, undefined);
spyOn(timestampChangedObserver, 'onCurrentTimestampChanged');
expect(timestampChangedObserver.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
spyOn(positionUpdateListener, 'onTracePositionUpdate');
expect(positionUpdateListener.onTracePositionUpdate).toHaveBeenCalledTimes(0);
timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
expect(timestampChangedObserver.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
expect(positionUpdateListener.onTracePositionUpdate).toHaveBeenCalledTimes(0);
timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]);
expect(timestampChangedObserver.onCurrentTimestampChanged).toHaveBeenCalledTimes(0);
expect(positionUpdateListener.onTracePositionUpdate).toHaveBeenCalledTimes(0);
});
it('hasTimestamps()', () => {
expect(timelineData.hasTimestamps()).toBeFalse();
timelineData.initialize([], undefined);
expect(timelineData.hasTimestamps()).toBeFalse();
timelineData.initialize(
[
{
traceType: TraceType.SURFACE_FLINGER,
timestamps: [],
},
],
undefined
);
expect(timelineData.hasTimestamps()).toBeFalse();
timelineData.initialize(
[
{
traceType: TraceType.SURFACE_FLINGER,
timestamps: [new Timestamp(TimestampType.REAL, 10n)],
},
],
undefined
);
expect(timelineData.hasTimestamps()).toBeTrue();
// no trace
{
const traces = new TracesBuilder().build();
timelineData.initialize(traces, undefined);
expect(timelineData.hasTimestamps()).toBeFalse();
}
// trace without timestamps
{
const traces = new TracesBuilder().setTimestamps(TraceType.SURFACE_FLINGER, []).build();
timelineData.initialize(traces, undefined);
expect(timelineData.hasTimestamps()).toBeFalse();
}
// trace with timestamps
{
const traces = new TracesBuilder()
.setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
.build();
timelineData.initialize(traces, undefined);
expect(timelineData.hasTimestamps()).toBeTrue();
}
});
it('hasMoreThanOneDistinctTimestamp()', () => {
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
timelineData.initialize([], undefined);
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
timelineData.initialize(
[
{
traceType: TraceType.SURFACE_FLINGER,
timestamps: [new Timestamp(TimestampType.REAL, 10n)],
},
{
traceType: TraceType.WINDOW_MANAGER,
timestamps: [new Timestamp(TimestampType.REAL, 10n)],
},
],
undefined
);
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
timelineData.initialize(
[
{
traceType: TraceType.SURFACE_FLINGER,
timestamps: [new Timestamp(TimestampType.REAL, 10n)],
},
{
traceType: TraceType.WINDOW_MANAGER,
timestamps: [new Timestamp(TimestampType.REAL, 11n)],
},
],
undefined
);
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeTrue();
// no trace
{
const traces = new TracesBuilder().build();
timelineData.initialize(traces, undefined);
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
}
// no distinct timestamps
{
const traces = new TracesBuilder()
.setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
.setTimestamps(TraceType.WINDOW_MANAGER, [timestamp10])
.build();
timelineData.initialize(traces, undefined);
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
}
// distinct timestamps
{
const traces = new TracesBuilder()
.setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
.setTimestamps(TraceType.WINDOW_MANAGER, [timestamp11])
.build();
timelineData.initialize(traces, undefined);
expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeTrue();
}
});
});

View File

@@ -14,26 +14,23 @@
* limitations under the License.
*/
import {ArrayUtils} from 'common/array_utils';
import {FunctionUtils, OnProgressUpdateType} from 'common/function_utils';
import {Parser} from 'parsers/parser';
import {ParserError, ParserFactory} from 'parsers/parser_factory';
import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
import {Timestamp, TimestampType} from 'trace/timestamp';
import {Trace, TraceFile} from 'trace/trace';
import {FrameMapper} from 'trace/frame_mapper';
import {Parser} from 'trace/parser';
import {TimestampType} from 'trace/timestamp';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
import {LoadedTraceFile, TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
interface Timeline {
traceType: TraceType;
timestamps: Timestamp[];
}
class TracePipeline {
private parserFactory = new ParserFactory();
private parsers: Parser[] = [];
private parsers: Array<Parser<object>> = [];
private traces?: Traces;
private commonTimestampType?: TimestampType;
async loadTraces(
async loadTraceFiles(
traceFiles: TraceFile[],
onLoadProgressUpdate: OnProgressUpdateType = FunctionUtils.DO_NOTHING
): Promise<ParserError[]> {
@@ -45,78 +42,51 @@ class TracePipeline {
return parserErrors;
}
removeTrace(type: TraceType) {
removeTraceFile(type: TraceType) {
this.parsers = this.parsers.filter((parser) => parser.getTraceType() !== type);
}
getLoadedTraces(): Trace[] {
return this.parsers.map((parser: Parser) => parser.getTrace());
getLoadedTraceFiles(): LoadedTraceFile[] {
return this.parsers.map(
(parser: Parser<object>) => new LoadedTraceFile(parser.getTraceFile(), parser.getTraceType())
);
}
getTraceEntries(timestamp: Timestamp | undefined): Map<TraceType, any> {
const traceEntries: Map<TraceType, any> = new Map<TraceType, any>();
if (!timestamp) {
return traceEntries;
}
buildTraces() {
const commonTimestampType = this.getCommonTimestampType();
this.traces = new Traces();
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]);
}
const trace = new Trace(
parser.getTraceType(),
parser.getTraceFile(),
undefined,
parser,
commonTimestampType,
{start: 0, end: parser.getLengthEntries()}
);
this.traces?.setTrace(parser.getTraceType(), trace);
});
return traceEntries;
new FrameMapper(this.traces).computeMapping();
}
getTimelines(): Timeline[] {
const timelines = this.parsers.map((parser): Timeline => {
const timestamps = parser.getTimestamps(this.getCommonTimestampType());
if (timestamps === undefined) {
throw Error('Failed to get timestamps from parser');
}
return {traceType: parser.getTraceType(), timestamps};
});
return timelines;
getTraces(): Traces {
this.checkTracesWereBuilt();
return this.traces!;
}
getScreenRecordingVideo(): undefined | Blob {
const parser = this.parsers.find(
(parser) => parser.getTraceType() === TraceType.SCREEN_RECORDING
);
if (!parser) {
const screenRecording = this.getTraces().getTrace(TraceType.SCREEN_RECORDING);
if (!screenRecording || screenRecording.lengthEntries === 0) {
return undefined;
}
const timestamps = parser.getTimestamps(this.getCommonTimestampType());
if (!timestamps || timestamps.length === 0) {
return undefined;
}
return (parser.getTraceEntry(timestamps[0]) as ScreenRecordingTraceEntry)?.videoData;
return screenRecording.getEntry(0).getValue().videoData;
}
clear() {
this.parserFactory = new ParserFactory();
this.parsers = [];
this.traces = undefined;
this.commonTimestampType = undefined;
}
@@ -135,6 +105,14 @@ class TracePipeline {
throw Error('Failed to find common timestamp type across all traces');
}
private checkTracesWereBuilt() {
if (!this.traces) {
throw new Error(
`Can't access traces before building them. Did you forget to call '${this.buildTraces.name}'?`
);
}
}
}
export {Timeline, TracePipeline};
export {TracePipeline};

View File

@@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TracesUtils} from 'test/unit/traces_utils';
import {UnitTestUtils} from 'test/unit/utils';
import {Timestamp, TimestampType} from 'trace/timestamp';
import {TraceFile} from 'trace/trace';
import {TraceFile} from 'trace/trace_file';
import {TraceType} from 'trace/trace_type';
import {TracePipeline} from './trace_pipeline';
@@ -27,9 +28,15 @@ describe('TracePipeline', () => {
});
it('can load valid trace files', async () => {
expect(tracePipeline.getLoadedTraces().length).toEqual(0);
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(0);
await loadValidSfWmTraces();
expect(tracePipeline.getLoadedTraces().length).toEqual(2);
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(2);
const traceEntries = TracesUtils.extractEntries(tracePipeline.getTraces());
expect(traceEntries.get(TraceType.WINDOW_MANAGER)?.length).toBeGreaterThan(0);
expect(traceEntries.get(TraceType.SURFACE_FLINGER)?.length).toBeGreaterThan(0);
});
it('is robust to invalid trace files', async () => {
@@ -37,19 +44,21 @@ describe('TracePipeline', () => {
new TraceFile(await UnitTestUtils.getFixtureFile('winscope_homepage.png')),
];
const errors = await tracePipeline.loadTraces(invalidTraceFiles);
const errors = await tracePipeline.loadTraceFiles(invalidTraceFiles);
tracePipeline.buildTraces();
expect(errors.length).toEqual(1);
expect(tracePipeline.getLoadedTraces().length).toEqual(0);
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(0);
});
it('is robust to mixed valid and invalid trace files', async () => {
expect(tracePipeline.getLoadedTraces().length).toEqual(0);
const traces = [
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(0);
const files = [
new TraceFile(await UnitTestUtils.getFixtureFile('winscope_homepage.png')),
new TraceFile(await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb')),
];
const errors = await tracePipeline.loadTraces(traces);
expect(tracePipeline.getLoadedTraces().length).toEqual(1);
const errors = await tracePipeline.loadTraceFiles(files);
tracePipeline.buildTraces();
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(1);
expect(errors.length).toEqual(1);
});
@@ -58,89 +67,48 @@ describe('TracePipeline', () => {
new TraceFile(await UnitTestUtils.getFixtureFile('traces/no_entries_InputMethodClients.pb')),
];
const errors = await tracePipeline.loadTraces(traceFilesWithNoEntries);
const errors = await tracePipeline.loadTraceFiles(traceFilesWithNoEntries);
tracePipeline.buildTraces();
expect(errors.length).toEqual(0);
expect(tracePipeline.getLoadedTraces().length).toEqual(1);
const timelines = tracePipeline.getTimelines();
expect(timelines.length).toEqual(1);
expect(timelines[0].timestamps).toEqual([]);
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(1);
});
it('can remove traces', async () => {
await loadValidSfWmTraces();
expect(tracePipeline.getLoadedTraces().length).toEqual(2);
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(2);
tracePipeline.removeTrace(TraceType.SURFACE_FLINGER);
expect(tracePipeline.getLoadedTraces().length).toEqual(1);
tracePipeline.removeTraceFile(TraceType.SURFACE_FLINGER);
tracePipeline.buildTraces();
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(1);
tracePipeline.removeTrace(TraceType.WINDOW_MANAGER);
expect(tracePipeline.getLoadedTraces().length).toEqual(0);
tracePipeline.removeTraceFile(TraceType.WINDOW_MANAGER);
tracePipeline.buildTraces();
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(0);
});
it('gets loaded traces', async () => {
it('gets loaded trace files', async () => {
await loadValidSfWmTraces();
const traces = tracePipeline.getLoadedTraces();
expect(traces.length).toEqual(2);
expect(traces[0].traceFile.file).toBeTruthy();
const files = tracePipeline.getLoadedTraceFiles();
expect(files.length).toEqual(2);
expect(files[0].traceFile).toBeTruthy();
const actualTraceTypes = new Set(traces.map((trace) => trace.type));
const actualTraceTypes = new Set(files.map((file) => file.type));
const expectedTraceTypes = new Set([TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]);
expect(actualTraceTypes).toEqual(expectedTraceTypes);
});
it('gets trace entries for a given timestamp', async () => {
const traceFiles = [
new TraceFile(
await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb')
),
new TraceFile(
await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/WindowManager.pb')
),
];
const errors = await tracePipeline.loadTraces(traceFiles);
expect(errors.length).toEqual(0);
{
const entries = tracePipeline.getTraceEntries(undefined);
expect(entries.size).toEqual(0);
}
{
const timestamp = new Timestamp(TimestampType.REAL, 0n);
const entries = tracePipeline.getTraceEntries(timestamp);
expect(entries.size).toEqual(0);
}
{
const twoHundredYearsTimestamp = new Timestamp(
TimestampType.REAL,
200n * 365n * 24n * 60n * 3600n * 1000000000n
);
const entries = tracePipeline.getTraceEntries(twoHundredYearsTimestamp);
expect(entries.size).toEqual(2);
}
});
it('gets timelines', async () => {
it('builds traces', async () => {
await loadValidSfWmTraces();
const traces = tracePipeline.getTraces();
const timelines = tracePipeline.getTimelines();
const actualTraceTypes = new Set(timelines.map((timeline) => timeline.traceType));
const expectedTraceTypes = new Set([TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER]);
expect(actualTraceTypes).toEqual(expectedTraceTypes);
timelines.forEach((timeline) => {
expect(timeline.timestamps.length).toBeGreaterThan(0);
});
expect(traces.getTrace(TraceType.SURFACE_FLINGER)).toBeDefined();
expect(traces.getTrace(TraceType.WINDOW_MANAGER)).toBeDefined();
});
it('gets screenrecording data', async () => {
expect(tracePipeline.getScreenRecordingVideo()).toBeUndefined();
const traceFiles = [
new TraceFile(
await UnitTestUtils.getFixtureFile(
@@ -148,7 +116,8 @@ describe('TracePipeline', () => {
)
),
];
await tracePipeline.loadTraces(traceFiles);
await tracePipeline.loadTraceFiles(traceFiles);
tracePipeline.buildTraces();
const video = tracePipeline.getScreenRecordingVideo();
expect(video).toBeDefined();
@@ -157,12 +126,25 @@ describe('TracePipeline', () => {
it('can be cleared', async () => {
await loadValidSfWmTraces();
expect(tracePipeline.getLoadedTraces().length).toBeGreaterThan(0);
expect(tracePipeline.getTimelines().length).toBeGreaterThan(0);
expect(tracePipeline.getLoadedTraceFiles().length).toBeGreaterThan(0);
tracePipeline.clear();
expect(tracePipeline.getLoadedTraces().length).toEqual(0);
expect(tracePipeline.getTimelines().length).toEqual(0);
expect(tracePipeline.getLoadedTraceFiles().length).toEqual(0);
expect(() => {
tracePipeline.getTraces();
}).toThrow();
expect(() => {
tracePipeline.getScreenRecordingVideo();
}).toThrow();
});
it('throws if accessed before traces are built', async () => {
expect(() => {
tracePipeline.getTraces();
}).toThrow();
expect(() => {
tracePipeline.getScreenRecordingVideo();
}).toThrow();
});
const loadValidSfWmTraces = async () => {
@@ -175,7 +157,9 @@ describe('TracePipeline', () => {
),
];
const errors = await tracePipeline.loadTraces(traceFiles);
const errors = await tracePipeline.loadTraceFiles(traceFiles);
expect(errors.length).toEqual(0);
tracePipeline.buildTraces();
};
});

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import {Timestamp} from 'trace/timestamp';
import {TracePosition} from 'trace/trace_position';
export interface TimestampChangeListener {
onCurrentTimestampChanged(timestamp: Timestamp | undefined): void;
export interface TracePositionUpdateListener {
onTracePositionUpdate(position: TracePosition): void;
}