Integrate "UI Traces API" with Winscope App/Core

Bug: b/256564627
Test: npm run build:all && npm run test:all
Change-Id: Ic434abc3031b9d53ddb6289fed747971e90c430e
diff --git a/tools/winscope/src/app/components/app_component.ts b/tools/winscope/src/app/components/app_component.ts
index a4c971a..e22b7c7 100644
--- a/tools/winscope/src/app/components/app_component.ts
+++ b/tools/winscope/src/app/components/app_component.ts
@@ -273,11 +273,7 @@
   }
 
   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 @@
 
   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 @@
   }
 
   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);
diff --git a/tools/winscope/src/app/components/collect_traces_component.ts b/tools/winscope/src/app/components/collect_traces_component.ts
index 521515a..961f798 100644
--- a/tools/winscope/src/app/components/collect_traces_component.ts
+++ b/tools/winscope/src/app/components/collect_traces_component.ts
@@ -29,7 +29,7 @@
 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 @@
     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!');
diff --git a/tools/winscope/src/app/components/upload_traces_component.ts b/tools/winscope/src/app/components/upload_traces_component.ts
index 8993667..f53e696 100644
--- a/tools/winscope/src/app/components/upload_traces_component.ts
+++ b/tools/winscope/src/app/components/upload_traces_component.ts
@@ -27,7 +27,7 @@
 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 @@
         </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 @@
         </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 @@
       </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 @@
     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 @@
 
     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();
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index 8964abf..5513b90 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -20,9 +20,10 @@
 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 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 @@
     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 @@
   }
 
   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 @@
         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 @@
     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 @@
   }
 
   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;
diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts
index 28730bc..b597ddc 100644
--- a/tools/winscope/src/app/mediator_test.ts
+++ b/tools/winscope/src/app/mediator_test.ts
@@ -19,7 +19,8 @@
 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 @@
 
   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 @@
   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 @@
     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 @@
         )
       ),
     ];
-    const errors = await tracePipeline.loadTraces(traces);
+    const errors = await tracePipeline.loadTraceFiles(files);
     expect(errors).toEqual([]);
   };
 });
diff --git a/tools/winscope/src/app/timeline_data.ts b/tools/winscope/src/app/timeline_data.ts
index 694c8af..ab9082b 100644
--- a/tools/winscope/src/app/timeline_data.ts
+++ b/tools/winscope/src/app/timeline_data.ts
@@ -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 @@
     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 @@
     });
   }
 
-  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);
     }
   }
 }
diff --git a/tools/winscope/src/app/timeline_data_test.ts b/tools/winscope/src/app/timeline_data_test.ts
index 362bf1e..09ce031 100644
--- a/tools/winscope/src/app/timeline_data_test.ts
+++ b/tools/winscope/src/app/timeline_data_test.ts
@@ -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();
+    }
   });
 });
diff --git a/tools/winscope/src/app/trace_pipeline.ts b/tools/winscope/src/app/trace_pipeline.ts
index 27500a5..5620b4c 100644
--- a/tools/winscope/src/app/trace_pipeline.ts
+++ b/tools/winscope/src/app/trace_pipeline.ts
@@ -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 @@
     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>();
+  buildTraces() {
+    const commonTimestampType = this.getCommonTimestampType();
 
-    if (!timestamp) {
-      return traceEntries;
-    }
-
+    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 @@
 
     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};
diff --git a/tools/winscope/src/app/trace_pipeline_test.ts b/tools/winscope/src/app/trace_pipeline_test.ts
index daddbf8..43d29c3 100644
--- a/tools/winscope/src/app/trace_pipeline_test.ts
+++ b/tools/winscope/src/app/trace_pipeline_test.ts
@@ -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 @@
   });
 
   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 @@
       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 @@
       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 @@
         )
       ),
     ];
-    await tracePipeline.loadTraces(traceFiles);
+    await tracePipeline.loadTraceFiles(traceFiles);
+    tracePipeline.buildTraces();
 
     const video = tracePipeline.getScreenRecordingVideo();
     expect(video).toBeDefined();
@@ -157,12 +126,25 @@
 
   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 @@
       ),
     ];
 
-    const errors = await tracePipeline.loadTraces(traceFiles);
+    const errors = await tracePipeline.loadTraceFiles(traceFiles);
     expect(errors.length).toEqual(0);
+
+    tracePipeline.buildTraces();
   };
 });
diff --git a/tools/winscope/src/interfaces/timestamp_change_listener.ts b/tools/winscope/src/interfaces/trace_position_update_listener.ts
similarity index 79%
rename from tools/winscope/src/interfaces/timestamp_change_listener.ts
rename to tools/winscope/src/interfaces/trace_position_update_listener.ts
index 154a1be..571f3ef 100644
--- a/tools/winscope/src/interfaces/timestamp_change_listener.ts
+++ b/tools/winscope/src/interfaces/trace_position_update_listener.ts
@@ -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;
 }