Squashed timestamp refactor (for reference).

Bug: 332662639
Test: npm run test:unit:ci

Change-Id: Ib39341814dc34ebd503b1a427e40fd633ded96b9
diff --git a/tools/winscope/src/app/components/app_component_test.ts b/tools/winscope/src/app/components/app_component_test.ts
index 766606c..16bc612 100644
--- a/tools/winscope/src/app/components/app_component_test.ts
+++ b/tools/winscope/src/app/components/app_component_test.ts
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {ClipboardModule} from '@angular/cdk/clipboard';
 import {CommonModule} from '@angular/common';
 import {ChangeDetectionStrategy} from '@angular/core';
 import {
@@ -37,11 +38,9 @@
 import {MatSnackBarModule} from '@angular/material/snack-bar';
 import {MatToolbarModule} from '@angular/material/toolbar';
 import {MatTooltipModule} from '@angular/material/tooltip';
+import {Title} from '@angular/platform-browser';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
-
-import {ClipboardModule} from '@angular/cdk/clipboard';
-import {Title} from '@angular/platform-browser';
 import {FileUtils} from 'common/file_utils';
 import {
   AppRefreshDumpsRequest,
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
index aaba8c0..2ef5c55 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component.ts
@@ -26,7 +26,7 @@
 import {Point} from 'common/geometry_types';
 import {Rect} from 'common/rect';
 import {TimeRange, Timestamp} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {ComponentTimestampConverter} from 'common/timestamp_converter';
 import {Trace, TraceEntry} from 'trace/trace';
 import {TracePosition} from 'trace/trace_position';
 import {AbstractTimelineRowComponent} from './abstract_timeline_row_component';
@@ -52,6 +52,7 @@
   @Input() trace: Trace<{}> | undefined;
   @Input() selectedEntry: TraceEntry<{}> | undefined;
   @Input() selectionRange: TimeRange | undefined;
+  @Input() timestampConverter: ComponentTimestampConverter | undefined;
 
   @Output() readonly onTracePositionUpdate = new EventEmitter<TracePosition>();
 
@@ -172,11 +173,7 @@
       (BigInt(Math.floor(x)) * (end - start)) /
         BigInt(this.getAvailableWidth()) +
       start;
-    return NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(
-      assertDefined(this.selectionRange).from.getType(),
-      ts,
-      0n,
-    );
+    return assertDefined(this.timestampConverter).makeTimestampFromNs(ts);
   }
 
   private drawEntry(entry: Timestamp) {
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
index 792c15a..2c9e56d 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/default_timeline_row_component_test.ts
@@ -27,7 +27,7 @@
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
 import {Rect} from 'common/rect';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {waitToBeCalled} from 'test/utils';
 import {TraceType} from 'trace/trace_type';
@@ -252,16 +252,17 @@
       .setType(TraceType.TRANSITION)
       .setEntries([{}, {}, {}, {}])
       .setTimestamps([
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(15n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(70n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(12n),
+        TimestampConverterUtils.makeRealTimestamp(15n),
+        TimestampConverterUtils.makeRealTimestamp(70n),
       ])
       .build();
     component.selectionRange = {
-      from: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(low),
-      to: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(high),
+      from: TimestampConverterUtils.makeRealTimestamp(low),
+      to: TimestampConverterUtils.makeRealTimestamp(high),
     };
+    component.timestampConverter = TimestampConverterUtils.TIMESTAMP_CONVERTER;
   }
 
   async function drawCorrectEntryOnClick(
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
index 1f934b7..6644c2a 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component.ts
@@ -55,6 +55,7 @@
           [traceEntries]="timelineData.getTransitions()"
           [selectedEntry]="timelineData.findCurrentEntryFor(trace.type)"
           [selectionRange]="timelineData.getSelectionTimeRange()"
+          [timestampConverter]="timelineData.getTimestampConverter()"
           (onTracePositionUpdate)="onTracePositionUpdate.emit($event)"
           (onScrollEvent)="updateScroll($event)"
           class="single-timeline">
@@ -65,6 +66,7 @@
           [trace]="trace"
           [selectedEntry]="timelineData.findCurrentEntryFor(trace.type)"
           [selectionRange]="timelineData.getSelectionTimeRange()"
+          [timestampConverter]="timelineData.getTimestampConverter()"
           (onTracePositionUpdate)="onTracePositionUpdate.emit($event)"
           (onScrollEvent)="updateScroll($event)"
           class="single-timeline">
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
index 0cabdaa..c71ba8e 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/expanded_timeline_component_test.ts
@@ -27,8 +27,8 @@
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {TimelineData} from 'app/timeline_data';
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
@@ -41,12 +41,12 @@
   let component: ExpandedTimelineComponent;
   let htmlElement: HTMLElement;
   let timelineData: TimelineData;
-  const time10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-  const time11 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n);
-  const time12 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n);
-  const time30 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(30n);
-  const time60 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(60n);
-  const time110 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n);
+  const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const time11 = TimestampConverterUtils.makeRealTimestamp(11n);
+  const time12 = TimestampConverterUtils.makeRealTimestamp(12n);
+  const time30 = TimestampConverterUtils.makeRealTimestamp(30n);
+  const time60 = TimestampConverterUtils.makeRealTimestamp(60n);
+  const time110 = TimestampConverterUtils.makeRealTimestamp(110n);
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -120,7 +120,11 @@
       .setTimestamps(TraceType.TRANSITION, [time10, time60])
       .setTimestamps(TraceType.PROTO_LOG, [])
       .build();
-    await timelineData.initialize(traces, undefined);
+    await timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     component.timelineData = timelineData;
   });
 
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
index f2cf12b..6e3e971 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component.ts
@@ -27,6 +27,7 @@
 import {Point} from 'common/geometry_types';
 import {Rect} from 'common/rect';
 import {TimeRange, Timestamp} from 'common/time';
+import {ComponentTimestampConverter} from 'common/timestamp_converter';
 import {Trace, TraceEntry} from 'trace/trace';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
@@ -58,6 +59,7 @@
   @Input() traceEntries: PropertyTreeNode[] | undefined;
   @Input() selectedEntry: TraceEntry<PropertyTreeNode> | undefined;
   @Input() selectionRange: TimeRange | undefined;
+  @Input() timestampConverter: ComponentTimestampConverter | undefined;
 
   @Output() readonly onTracePositionUpdate = new EventEmitter<TracePosition>();
 
@@ -104,8 +106,8 @@
       }
       const timeRange = TimelineUtils.getTimeRangeForTransition(
         transition,
-        entry.getTimestamp().getType(),
         assertDefined(this.selectionRange),
+        assertDefined(this.timestampConverter),
       );
       if (!timeRange) {
         return;
@@ -133,8 +135,8 @@
       }
       const timeRange = TimelineUtils.getTimeRangeForTransition(
         transition,
-        entry.getTimestamp().getType(),
         assertDefined(this.selectionRange),
+        assertDefined(this.timestampConverter),
       );
 
       if (!timeRange) {
@@ -170,8 +172,8 @@
     }
     const timeRange = TimelineUtils.getTimeRangeForTransition(
       transition,
-      this.hoveringEntry.getTimestamp().getType(),
       assertDefined(this.selectionRange),
+      assertDefined(this.timestampConverter),
     );
 
     if (!timeRange) {
@@ -252,8 +254,8 @@
     }
     const timeRange = TimelineUtils.getTimeRangeForTransition(
       transition,
-      this.selectedEntry.getTimestamp().getType(),
       assertDefined(this.selectionRange),
+      assertDefined(this.timestampConverter),
     );
     if (!timeRange) {
       return;
@@ -286,8 +288,8 @@
 
       const timeRange = TimelineUtils.getTimeRangeForTransition(
         transition,
-        entry.getTimestamp().getType(),
         assertDefined(this.selectionRange),
+        assertDefined(this.timestampConverter),
       );
 
       if (!timeRange) {
diff --git a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
index 3b46b73..1badb3a 100644
--- a/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/expanded-timeline/transition_timeline_component_test.ts
@@ -26,8 +26,8 @@
 import {MatTooltipModule} from '@angular/material/tooltip';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {Rect} from 'common/rect';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {waitToBeCalled} from 'test/utils';
 import {TraceType} from 'trace/trace_type';
@@ -37,15 +37,15 @@
 describe('TransitionTimelineComponent', () => {
   let fixture: ComponentFixture<TransitionTimelineComponent>;
   let component: TransitionTimelineComponent;
-  const time0 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n);
-  const time10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-  const time20 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(20n);
-  const time30 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(30n);
-  const time35 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(35n);
-  const time60 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(60n);
-  const time85 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(85n);
-  const time110 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n);
-  const time160 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(160n);
+  const time0 = TimestampConverterUtils.makeRealTimestamp(0n);
+  const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const time20 = TimestampConverterUtils.makeRealTimestamp(20n);
+  const time30 = TimestampConverterUtils.makeRealTimestamp(30n);
+  const time35 = TimestampConverterUtils.makeRealTimestamp(35n);
+  const time60 = TimestampConverterUtils.makeRealTimestamp(60n);
+  const time85 = TimestampConverterUtils.makeRealTimestamp(85n);
+  const time110 = TimestampConverterUtils.makeRealTimestamp(110n);
+  const time160 = TimestampConverterUtils.makeRealTimestamp(160n);
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -69,6 +69,7 @@
       .compileComponents();
     fixture = TestBed.createComponent(TransitionTimelineComponent);
     component = fixture.componentInstance;
+    component.timestampConverter = TimestampConverterUtils.TIMESTAMP_CONVERTER;
   });
 
   it('can be created', () => {
@@ -115,14 +116,14 @@
       .setType(TraceType.TRANSITION)
       .setEntries(transitions)
       .setTimestamps([
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(60n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(60n),
       ])
       .build();
     component.traceEntries = transitions;
     component.selectionRange = {
-      from: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-      to: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n),
+      from: TimestampConverterUtils.makeRealTimestamp(10n),
+      to: TimestampConverterUtils.makeRealTimestamp(110n),
     };
 
     const drawRectSpy = spyOn(component.canvasDrawer, 'drawRect');
@@ -614,12 +615,12 @@
     component.trace = new TraceBuilder<PropertyTreeNode>()
       .setType(TraceType.TRANSITION)
       .setEntries(transitions)
-      .setTimestamps([NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n)])
+      .setTimestamps([TimestampConverterUtils.makeRealTimestamp(10n)])
       .build();
     component.traceEntries = transitions;
     component.selectionRange = {
-      from: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-      to: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n),
+      from: TimestampConverterUtils.makeRealTimestamp(10n),
+      to: TimestampConverterUtils.makeRealTimestamp(110n),
     };
 
     fixture.detectChanges();
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
index de2c408..a472f35 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/drawer/mini_timeline_drawer_input.ts
@@ -37,7 +37,11 @@
   ) {}
 
   transform(mapToRange: Segment): MiniCanvasDrawerData {
-    const transformer = new Transformer(this.zoomRange, mapToRange);
+    const transformer = new Transformer(
+      this.zoomRange,
+      mapToRange,
+      assertDefined(this.timelineData.getTimestampConverter()),
+    );
 
     return new MiniCanvasDrawerData(
       transformer.transform(this.selectedPosition),
@@ -117,8 +121,8 @@
 
     const timeRange = TimelineUtils.getTimeRangeForTransition(
       transition,
-      entry.getTimestamp().getType(),
       this.selection,
+      assertDefined(this.timelineData.getTimestampConverter()),
     );
 
     if (!timeRange) {
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
index e7b262d..8a25a9a 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component.ts
@@ -59,6 +59,7 @@
             [fullRange]="timelineData.getFullTimeRange()"
             [zoomRange]="timelineData.getZoomRange()"
             [currentPosition]="timelineData.getCurrentPosition()"
+            [timestampConverter]="timelineData.getTimestampConverter()"
             (onZoomChanged)="onZoomChanged($event)"></slider>
         </div>
       </div>
@@ -214,14 +215,16 @@
     const timelineData = assertDefined(this.timelineData);
     const fullRange = timelineData.getFullTimeRange();
     const currentZoomRange = timelineData.getZoomRange();
-    const currentZoomWidth = currentZoomRange.to.minus(currentZoomRange.from);
+    const currentZoomWidth = currentZoomRange.to.minus(
+      currentZoomRange.from.getValueNs(),
+    );
     const zoomToWidth = currentZoomWidth
       .times(zoomRatio.nominator)
       .div(zoomRatio.denominator);
 
     const cursorPosition = timelineData.getCurrentPosition()?.timestamp;
     const currentMiddle = currentZoomRange.from
-      .plus(currentZoomRange.to)
+      .add(currentZoomRange.to.getValueNs())
       .div(2n);
 
     let newFrom: Timestamp;
@@ -246,9 +249,9 @@
         rightAdjustment = currentZoomWidth.times(0n);
       }
 
-      newFrom = currentZoomRange.from.plus(leftAdjustment);
-      newTo = currentZoomRange.to.minus(rightAdjustment);
-      const newMiddle = newFrom.plus(newTo).div(2n);
+      newFrom = currentZoomRange.from.add(leftAdjustment.getValueNs());
+      newTo = currentZoomRange.to.minus(rightAdjustment.getValueNs());
+      const newMiddle = newFrom.add(newTo.getValueNs()).div(2n);
 
       if (
         (zoomTowards.getValueNs() <= currentMiddle.getValueNs() &&
@@ -257,18 +260,18 @@
           newMiddle.getValueNs() > zoomTowards.getValueNs())
       ) {
         // Moved past middle, so ensure cursor is in the middle
-        newFrom = zoomTowards.minus(zoomToWidth.div(2n));
-        newTo = zoomTowards.plus(zoomToWidth.div(2n));
+        newFrom = zoomTowards.minus(zoomToWidth.div(2n).getValueNs());
+        newTo = zoomTowards.add(zoomToWidth.div(2n).getValueNs());
       }
     } else {
-      newFrom = zoomOn.minus(zoomToWidth.div(2n));
-      newTo = zoomOn.plus(zoomToWidth.div(2n));
+      newFrom = zoomOn.minus(zoomToWidth.div(2n).getValueNs());
+      newTo = zoomOn.add(zoomToWidth.div(2n).getValueNs());
     }
 
     if (newFrom.getValueNs() < fullRange.from.getValueNs()) {
       newTo = TimestampUtils.min(
         fullRange.to,
-        newTo.plus(fullRange.from.minus(newFrom)),
+        newTo.add(fullRange.from.minus(newFrom.getValueNs()).getValueNs()),
       );
       newFrom = fullRange.from;
     }
@@ -276,7 +279,7 @@
     if (newTo.getValueNs() > fullRange.to.getValueNs()) {
       newFrom = TimestampUtils.max(
         fullRange.from,
-        newFrom.minus(newTo.minus(fullRange.to)),
+        newFrom.minus(newTo.minus(fullRange.to.getValueNs()).getValueNs()),
       );
       newTo = fullRange.to;
     }
@@ -373,13 +376,15 @@
   }
 
   private updateZoomByScrollEvent(event: WheelEvent) {
+    const timelineData = assertDefined(this.timelineData);
     const canvas = event.target as HTMLCanvasElement;
     const xPosInCanvas = event.x - canvas.offsetLeft;
-    const zoomRange = assertDefined(this.timelineData).getZoomRange();
+    const zoomRange = timelineData.getZoomRange();
 
     const zoomTo = new Transformer(
       zoomRange,
       assertDefined(this.drawer).getUsableRange(),
+      assertDefined(timelineData.getTimestampConverter()),
     ).untransform(xPosInCanvas);
 
     if (event.deltaY < 0) {
@@ -396,20 +401,28 @@
     const zoomRange = timelineData.getZoomRange();
 
     const usableRange = assertDefined(this.drawer).getUsableRange();
-    const transformer = new Transformer(zoomRange, usableRange);
+    const transformer = new Transformer(
+      zoomRange,
+      usableRange,
+      assertDefined(timelineData.getTimestampConverter()),
+    );
     const shiftAmount = transformer
       .untransform(usableRange.from + scrollAmount)
-      .minus(zoomRange.from);
-    let newFrom = zoomRange.from.plus(shiftAmount);
-    let newTo = zoomRange.to.plus(shiftAmount);
+      .minus(zoomRange.from.getValueNs());
+    let newFrom = zoomRange.from.add(shiftAmount.getValueNs());
+    let newTo = zoomRange.to.add(shiftAmount.getValueNs());
 
     if (newFrom.getValueNs() < fullRange.from.getValueNs()) {
-      newTo = newTo.plus(fullRange.from.minus(newFrom));
+      newTo = newTo.add(
+        fullRange.from.minus(newFrom.getValueNs()).getValueNs(),
+      );
       newFrom = fullRange.from;
     }
 
     if (newTo.getValueNs() > fullRange.to.getValueNs()) {
-      newFrom = newFrom.minus(newTo.minus(fullRange.to));
+      newFrom = newFrom.minus(
+        newTo.minus(fullRange.to.getValueNs()).getValueNs(),
+      );
       newTo = fullRange.to;
     }
 
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
index 41e57b5..9f2f591 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/mini_timeline_component_test.ts
@@ -27,7 +27,7 @@
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {TimelineData} from 'app/timeline_data';
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {dragElement} from 'test/utils';
 import {TracePosition} from 'trace/trace_position';
@@ -41,16 +41,16 @@
   let htmlElement: HTMLElement;
   let timelineData: TimelineData;
 
-  const timestamp10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-  const timestamp15 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(15n);
-  const timestamp16 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(16n);
-  const timestamp20 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(20n);
-  const timestamp700 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(700n);
-  const timestamp810 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(810n);
-  const timestamp1000 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1000n);
+  const timestamp10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const timestamp15 = TimestampConverterUtils.makeRealTimestamp(15n);
+  const timestamp16 = TimestampConverterUtils.makeRealTimestamp(16n);
+  const timestamp20 = TimestampConverterUtils.makeRealTimestamp(20n);
+  const timestamp700 = TimestampConverterUtils.makeRealTimestamp(700n);
+  const timestamp810 = TimestampConverterUtils.makeRealTimestamp(810n);
+  const timestamp1000 = TimestampConverterUtils.makeRealTimestamp(1000n);
 
   const position800 = TracePosition.fromTimestamp(
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(800n),
+    TimestampConverterUtils.makeRealTimestamp(800n),
   );
 
   beforeEach(async () => {
@@ -83,7 +83,11 @@
       .setTimestamps(TraceType.TRANSACTIONS, [timestamp10, timestamp20])
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp20])
       .build();
-    await timelineData.initialize(traces, undefined);
+    await timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     component.timelineData = timelineData;
     expect(timelineData.getCurrentPosition()).toBeTruthy();
     component.currentTracePosition = timelineData.getCurrentPosition()!;
@@ -256,7 +260,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     let initialZoom = {
       from: timestamp10,
@@ -285,14 +293,21 @@
       expect(finalZoom.to.getValueNs()).toBeLessThanOrEqual(
         Number(initialZoom.to.getValueNs()),
       );
-      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeLessThan(
-        Number(initialZoom.to.minus(initialZoom.from).getValueNs()),
+      expect(
+        finalZoom.to.minus(finalZoom.from.getValueNs()).getValueNs(),
+      ).toBeLessThan(
+        Number(
+          initialZoom.to.minus(initialZoom.from.getValueNs()).getValueNs(),
+        ),
       );
 
       // center to get closer to cursor or stay on cursor
-      const curCenter = finalZoom.from.plus(finalZoom.to).div(2n).getValueNs();
+      const curCenter = finalZoom.from
+        .add(finalZoom.to.getValueNs())
+        .div(2n)
+        .getValueNs();
       const prevCenter = initialZoom.from
-        .plus(initialZoom.to)
+        .add(initialZoom.to.getValueNs())
         .div(2n)
         .getValueNs();
 
@@ -316,7 +331,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     let initialZoom = {
       from: timestamp700,
@@ -345,14 +364,21 @@
       expect(finalZoom.to.getValueNs()).toBeGreaterThanOrEqual(
         Number(initialZoom.to.getValueNs()),
       );
-      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeGreaterThan(
-        Number(initialZoom.to.minus(initialZoom.from).getValueNs()),
+      expect(
+        finalZoom.to.minus(finalZoom.from.getValueNs()).getValueNs(),
+      ).toBeGreaterThan(
+        Number(
+          initialZoom.to.minus(initialZoom.from.getValueNs()).getValueNs(),
+        ),
       );
 
       // center to get closer to cursor or stay on cursor unless we reach the edge
-      const curCenter = finalZoom.from.plus(finalZoom.to).div(2n).getValueNs();
+      const curCenter = finalZoom.from
+        .add(finalZoom.to.getValueNs())
+        .div(2n)
+        .getValueNs();
       const prevCenter = initialZoom.from
-        .plus(initialZoom.to)
+        .add(initialZoom.to.getValueNs())
         .div(2n)
         .getValueNs();
 
@@ -380,7 +406,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     const initialZoom = {
       from: timestamp10,
@@ -410,7 +440,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    assertDefined(component.timelineData).initialize(traces, undefined);
+    assertDefined(component.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     let initialZoom = {
       from: timestamp10,
@@ -431,8 +465,12 @@
       fixture.detectChanges();
       const finalZoom = timelineData.getZoomRange();
       expect(finalZoom).not.toBe(initialZoom);
-      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeLessThan(
-        Number(initialZoom.to.minus(initialZoom.from).getValueNs()),
+      expect(
+        finalZoom.to.minus(finalZoom.from.getValueNs()).getValueNs(),
+      ).toBeLessThan(
+        Number(
+          initialZoom.to.minus(initialZoom.from.getValueNs()).getValueNs(),
+        ),
       );
 
       initialZoom = finalZoom;
@@ -446,7 +484,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    assertDefined(component.timelineData).initialize(traces, undefined);
+    assertDefined(component.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     let initialZoom = {
       from: timestamp700,
@@ -467,8 +509,12 @@
       fixture.detectChanges();
       const finalZoom = timelineData.getZoomRange();
       expect(finalZoom).not.toBe(initialZoom);
-      expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBeGreaterThan(
-        Number(initialZoom.to.minus(initialZoom.from).getValueNs()),
+      expect(
+        finalZoom.to.minus(finalZoom.from.getValueNs()).getValueNs(),
+      ).toBeGreaterThan(
+        Number(
+          initialZoom.to.minus(initialZoom.from.getValueNs()).getValueNs(),
+        ),
       );
 
       initialZoom = finalZoom;
@@ -482,7 +528,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    assertDefined(component.timelineData).initialize(traces, undefined);
+    assertDefined(component.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     const initialZoom = {
       from: timestamp10,
@@ -511,7 +561,11 @@
       .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000])
       .build();
 
-    assertDefined(component.timelineData).initialize(traces, undefined);
+    assertDefined(component.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     const initialZoom = {
       from: timestamp10,
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
index ac085fe..16476b7 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component.ts
@@ -30,7 +30,7 @@
 import {assertDefined} from 'common/assert_utils';
 import {Point} from 'common/geometry_types';
 import {TimeRange, Timestamp} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {ComponentTimestampConverter} from 'common/timestamp_converter';
 import {TracePosition} from 'trace/trace_position';
 import {Transformer} from './transformer';
 
@@ -123,6 +123,7 @@
   @Input() fullRange: TimeRange | undefined;
   @Input() zoomRange: TimeRange | undefined;
   @Input() currentPosition: TracePosition | undefined;
+  @Input() timestampConverter: ComponentTimestampConverter | undefined;
 
   @Output() readonly onZoomChanged = new EventEmitter<TimeRange>();
 
@@ -151,8 +152,8 @@
 
   syncDragPositionTo(zoomRange: TimeRange) {
     this.sliderWidth = this.computeSliderWidth();
-    const middleOfZoomRange = zoomRange.from.plus(
-      zoomRange.to.minus(zoomRange.from).div(2n),
+    const middleOfZoomRange = zoomRange.from.add(
+      zoomRange.to.minus(zoomRange.from.getValueNs()).div(2n).getValueNs(),
     );
 
     this.dragPosition = {
@@ -172,7 +173,11 @@
     const width = this.viewInitialized
       ? this.sliderBox.nativeElement.offsetWidth
       : 0;
-    return new Transformer(assertDefined(this.fullRange), {from: 0, to: width});
+    return new Transformer(
+      assertDefined(this.fullRange),
+      {from: 0, to: width},
+      assertDefined(this.timestampConverter),
+    );
   }
 
   ngAfterViewInit(): void {
@@ -230,14 +235,14 @@
     // Calculation to adjust for min width slider
     const from = this.getTransformer()
       .untransform(newX + this.sliderWidth / 2)
-      .minus(zoomRange.to.minus(zoomRange.from).div(2n));
+      .minus(
+        zoomRange.to.minus(zoomRange.from.getValueNs()).div(2n).getValueNs(),
+      );
 
-    const to = NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(
-      assertDefined(this.zoomRange).to.getType(),
+    const to = assertDefined(this.timestampConverter).makeTimestampFromNs(
       from.getValueNs() +
         (assertDefined(this.zoomRange).to.getValueNs() -
           assertDefined(this.zoomRange).from.getValueNs()),
-      0n,
     );
 
     this.onZoomChanged.emit({from, to});
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
index d002dce..fe82a54 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/slider_component_test.ts
@@ -27,7 +27,7 @@
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
 import {TimeRange} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {dragElement} from 'test/utils';
 import {TracePosition} from 'trace/trace_position';
 import {MIN_SLIDER_WIDTH, SliderComponent} from './slider_component';
@@ -36,12 +36,12 @@
   let fixture: ComponentFixture<SliderComponent>;
   let component: SliderComponent;
   let htmlElement: HTMLElement;
-  const time100 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n);
-  const time125 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(125n);
-  const time126 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(126n);
-  const time150 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(150n);
-  const time175 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(175n);
-  const time200 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(200n);
+  const time100 = TimestampConverterUtils.makeRealTimestamp(100n);
+  const time125 = TimestampConverterUtils.makeRealTimestamp(125n);
+  const time126 = TimestampConverterUtils.makeRealTimestamp(126n);
+  const time150 = TimestampConverterUtils.makeRealTimestamp(150n);
+  const time175 = TimestampConverterUtils.makeRealTimestamp(175n);
+  const time200 = TimestampConverterUtils.makeRealTimestamp(200n);
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
@@ -76,6 +76,7 @@
       to: time175,
     };
     component.currentPosition = TracePosition.fromTimestamp(time150);
+    component.timestampConverter = TimestampConverterUtils.TIMESTAMP_CONVERTER;
 
     fixture.detectChanges();
   });
@@ -201,8 +202,8 @@
     const finalZoom = assertDefined<TimeRange>(lastZoomUpdate);
     expect(finalZoom.from).not.toBe(initialZoom.from);
     expect(finalZoom.to).not.toBe(initialZoom.to);
-    expect(finalZoom.to.minus(finalZoom.from).getValueNs()).toBe(
-      initialZoom.to.minus(initialZoom.from).getValueNs(),
+    expect(finalZoom.to.minus(finalZoom.from.getValueNs()).getValueNs()).toBe(
+      initialZoom.to.minus(initialZoom.from.getValueNs()).getValueNs(),
     );
   }));
 
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts b/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
index 533423a..b6e3895 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/transformer.ts
@@ -15,21 +15,21 @@
  */
 
 import {Segment} from 'app/components/timeline/segment';
-import {TimeRange, Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimeRange, Timestamp} from 'common/time';
+import {ComponentTimestampConverter} from 'common/timestamp_converter';
 
 export class Transformer {
-  private timestampType: TimestampType;
-
   private fromWidth: bigint;
   private targetWidth: number;
 
   private fromOffset: bigint;
   private toOffset: number;
 
-  constructor(private fromRange: TimeRange, private toRange: Segment) {
-    this.timestampType = fromRange.from.getType();
-
+  constructor(
+    private fromRange: TimeRange,
+    private toRange: Segment,
+    private timestampConverter: ComponentTimestampConverter,
+  ) {
     this.fromWidth =
       this.fromRange.to.getValueNs() - this.fromRange.from.getValueNs();
     // Needs to be a whole number to be compatible with bigints
@@ -53,10 +53,6 @@
     const valueNs =
       this.fromOffset +
       (BigInt(x - this.toOffset) * this.fromWidth) / BigInt(this.targetWidth);
-    return NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(
-      this.timestampType,
-      valueNs,
-      0n,
-    );
+    return this.timestampConverter.makeTimestampFromNs(valueNs);
   }
 }
diff --git a/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts b/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
index 0832c7d..f39e25b 100644
--- a/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
+++ b/tools/winscope/src/app/components/timeline/mini-timeline/transformer_test.ts
@@ -14,20 +14,24 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {Transformer} from './transformer';
 
 describe('Transformer', () => {
   it('can transform', () => {
     const fromRange = {
-      from: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1689763211000000000n),
-      to: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1689763571000000000n),
+      from: TimestampConverterUtils.makeRealTimestamp(1689763211000000000n),
+      to: TimestampConverterUtils.makeRealTimestamp(1689763571000000000n),
     };
     const toRange = {
       from: 100,
       to: 1100,
     };
-    const transformer = new Transformer(fromRange, toRange);
+    const transformer = new Transformer(
+      fromRange,
+      toRange,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     const rangeStart = fromRange.from.getValueNs();
     const rangeEnd = fromRange.to.getValueNs();
@@ -38,42 +42,46 @@
 
     expect(
       transformer.transform(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(rangeStart + range / 2n),
+        TimestampConverterUtils.makeRealTimestamp(rangeStart + range / 2n),
       ),
     ).toBe(toRange.from + (toRange.to - toRange.from) / 2);
     expect(
       transformer.transform(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(rangeStart + range / 4n),
+        TimestampConverterUtils.makeRealTimestamp(rangeStart + range / 4n),
       ),
     ).toBe(toRange.from + (toRange.to - toRange.from) / 4);
     expect(
       transformer.transform(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(rangeStart + range / 20n),
+        TimestampConverterUtils.makeRealTimestamp(rangeStart + range / 20n),
       ),
     ).toBe(toRange.from + (toRange.to - toRange.from) / 20);
 
     expect(
       transformer.transform(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(rangeStart - range / 2n),
+        TimestampConverterUtils.makeRealTimestamp(rangeStart - range / 2n),
       ),
     ).toBe(toRange.from - (toRange.to - toRange.from) / 2);
     expect(
       transformer.transform(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(rangeEnd + range / 2n),
+        TimestampConverterUtils.makeRealTimestamp(rangeEnd + range / 2n),
       ),
     ).toBe(toRange.to + (toRange.to - toRange.from) / 2);
   });
 
   it('can untransform', () => {
     const fromRange = {
-      from: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1689763211000000000n),
-      to: NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1689763571000000000n),
+      from: TimestampConverterUtils.makeRealTimestamp(1689763211000000000n),
+      to: TimestampConverterUtils.makeRealTimestamp(1689763571000000000n),
     };
     const toRange = {
       from: 100,
       to: 1100,
     };
-    const transformer = new Transformer(fromRange, toRange);
+    const transformer = new Transformer(
+      fromRange,
+      toRange,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     const rangeStart = fromRange.from.getValueNs();
     const range = fromRange.to.getValueNs() - fromRange.from.getValueNs();
diff --git a/tools/winscope/src/app/components/timeline/timeline_component.ts b/tools/winscope/src/app/components/timeline/timeline_component.ts
index f3d0ffd..9b01820 100644
--- a/tools/winscope/src/app/components/timeline/timeline_component.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_component.ts
@@ -26,7 +26,12 @@
   ViewChild,
   ViewEncapsulation,
 } from '@angular/core';
-import {FormControl, FormGroup, Validators} from '@angular/forms';
+import {
+  FormControl,
+  FormGroup,
+  ValidationErrors,
+  Validators,
+} from '@angular/forms';
 import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
 import {TimelineData} from 'app/timeline_data';
 import {TRACE_INFO} from 'app/trace_info';
@@ -34,8 +39,7 @@
 import {FunctionUtils} from 'common/function_utils';
 import {PersistentStore} from 'common/persistent_store';
 import {StringUtils} from 'common/string_utils';
-import {TimeRange, Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimeRange, Timestamp} from 'common/time';
 import {TimestampUtils} from 'common/timestamp_utils';
 import {
   ExpandedTimelineToggled,
@@ -90,26 +94,14 @@
           <div id="time-selector">
             <form [formGroup]="timestampForm" class="time-selector-form">
               <mat-form-field
-                class="time-input elapsed"
+                class="time-input human"
                 appearance="fill"
-                (keydown.enter)="onKeydownEnterElapsedTimeInputField($event)"
-                (change)="onHumanElapsedTimeInputChange($event)"
-                *ngIf="!usingRealtime()">
+                (keydown.enter)="onKeydownEnterTimeInputField($event)"
+                (change)="onHumanTimeInputChange($event)">
                 <input
                   matInput
-                  name="humanElapsedTimeInput"
-                  [formControl]="selectedElapsedTimeFormControl" />
-              </mat-form-field>
-              <mat-form-field
-                class="time-input real"
-                appearance="fill"
-                (keydown.enter)="onKeydownEnterRealTimeInputField($event)"
-                (change)="onHumanRealTimeInputChange($event)"
-                *ngIf="usingRealtime()">
-                <input
-                  matInput
-                  name="humanRealTimeInput"
-                  [formControl]="selectedRealTimeFormControl" />
+                  name="humanTimeInput"
+                  [formControl]="selectedTimeFormControl" />
               </mat-form-field>
               <mat-form-field
                 class="time-input nano"
@@ -413,19 +405,9 @@
   selectedTraces: TraceType[] = [];
   sortedAvailableTraces: TraceType[] = [];
   selectedTracesFormControl = new FormControl<TraceType[]>([]);
-  selectedElapsedTimeFormControl = new FormControl(
+  selectedTimeFormControl = new FormControl(
     'undefined',
-    Validators.compose([
-      Validators.required,
-      Validators.pattern(TimestampUtils.HUMAN_ELAPSED_TIMESTAMP_REGEX),
-    ]),
-  );
-  selectedRealTimeFormControl = new FormControl(
-    'undefined',
-    Validators.compose([
-      Validators.required,
-      Validators.pattern(TimestampUtils.HUMAN_REAL_TIMESTAMP_REGEX),
-    ]),
+    Validators.compose([Validators.required, this.validateTimeFormat]),
   );
   selectedNsFormControl = new FormControl(
     'undefined',
@@ -435,8 +417,7 @@
     ]),
   );
   timestampForm = new FormGroup({
-    selectedElapsedTime: this.selectedElapsedTimeFormControl,
-    selectedRealTime: this.selectedRealTimeFormControl,
+    selectedTime: this.selectedTimeFormControl,
     selectedNs: this.selectedNsFormControl,
   });
   TRACE_INFO = TRACE_INFO;
@@ -555,12 +536,6 @@
     await this.emitEvent(new TracePositionUpdate(position));
   }
 
-  usingRealtime(): boolean {
-    return (
-      assertDefined(this.timelineData).getTimestampType() === TimestampType.REAL
-    );
-  }
-
   updateSeekTimestamp(timestamp: Timestamp | undefined) {
     if (timestamp) {
       this.seekTracePosition = assertDefined(
@@ -675,25 +650,16 @@
     await this.emitEvent(new TracePositionUpdate(position));
   }
 
-  async onHumanElapsedTimeInputChange(event: Event) {
-    if (event.type !== 'change' || !this.selectedElapsedTimeFormControl.valid) {
-      return;
-    }
-    const target = event.target as HTMLInputElement;
-    const timestamp = TimestampUtils.parseHumanElapsed(target.value);
-    await this.updatePosition(
-      assertDefined(this.timelineData).makePositionFromActiveTrace(timestamp),
-    );
-    this.updateTimeInputValuesToCurrentTimestamp();
-  }
-
-  async onHumanRealTimeInputChange(event: Event) {
-    if (event.type !== 'change' || !this.selectedRealTimeFormControl.valid) {
+  async onHumanTimeInputChange(event: Event) {
+    if (event.type !== 'change' || !this.selectedTimeFormControl.valid) {
       return;
     }
     const target = event.target as HTMLInputElement;
 
-    const timestamp = TimestampUtils.parseHumanReal(target.value);
+    const timelineData = assertDefined(this.timelineData);
+    const timestamp = assertDefined(
+      timelineData.getTimestampConverter(),
+    ).makeTimestampFromHuman(target.value);
     await this.updatePosition(
       assertDefined(this.timelineData).makePositionFromActiveTrace(timestamp),
     );
@@ -707,25 +673,17 @@
     const target = event.target as HTMLInputElement;
     const timelineData = assertDefined(this.timelineData);
 
-    const timestamp = NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(
-      assertDefined(timelineData.getTimestampType()),
-      StringUtils.parseBigIntStrippingUnit(target.value),
-      0n,
-    );
+    const timestamp = assertDefined(
+      timelineData.getTimestampConverter(),
+    ).makeTimestampFromNs(StringUtils.parseBigIntStrippingUnit(target.value));
     await this.updatePosition(
       timelineData.makePositionFromActiveTrace(timestamp),
     );
     this.updateTimeInputValuesToCurrentTimestamp();
   }
 
-  onKeydownEnterElapsedTimeInputField(event: KeyboardEvent) {
-    if (this.selectedElapsedTimeFormControl.valid) {
-      (event.target as HTMLInputElement).blur();
-    }
-  }
-
-  onKeydownEnterRealTimeInputField(event: KeyboardEvent) {
-    if (this.selectedRealTimeFormControl.valid) {
+  onKeydownEnterTimeInputField(event: KeyboardEvent) {
+    if (this.selectedTimeFormControl.valid) {
       (event.target as HTMLInputElement).blur();
     }
   }
@@ -741,21 +699,15 @@
   }
 
   private updateTimeInputValuesToCurrentTimestamp() {
-    const currentNs = this.getCurrentTracePosition().timestamp.getValueNs();
-    this.selectedElapsedTimeFormControl.setValue(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(currentNs),
-        false,
-      ),
+    const currentTimestampNs =
+      this.getCurrentTracePosition().timestamp.getValueNs();
+    const timelineData = assertDefined(this.timelineData);
+    this.selectedTimeFormControl.setValue(
+      assertDefined(timelineData.getTimestampConverter())
+        .makeTimestampFromNs(currentTimestampNs)
+        .format(),
     );
-    this.selectedRealTimeFormControl.setValue(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(currentNs),
-      ),
-    );
-    this.selectedNsFormControl.setValue(
-      `${this.getCurrentTracePosition().timestamp.getValueNs()} ns`,
-    );
+    this.selectedNsFormControl.setValue(`${currentTimestampNs} ns`);
   }
 
   private getSelectedTracesSortedByDisplayOrder(): TraceType[] {
@@ -793,4 +745,12 @@
 
     this.store.add(this.storeKeyDeselectedTraces, JSON.stringify(storedTraces));
   }
+
+  private validateTimeFormat(control: FormControl): ValidationErrors | null {
+    const timestampHuman = control.value ?? '';
+    const valid =
+      TimestampUtils.HUMAN_REAL_TIMESTAMP_REGEX.test(timestampHuman) ||
+      TimestampUtils.HUMAN_ELAPSED_TIMESTAMP_REGEX.test(timestampHuman);
+    return !valid ? {invalidInput: control.value} : null;
+  }
 }
diff --git a/tools/winscope/src/app/components/timeline/timeline_component_test.ts b/tools/winscope/src/app/components/timeline/timeline_component_test.ts
index 0fe6415..af77597 100644
--- a/tools/winscope/src/app/components/timeline/timeline_component_test.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_component_test.ts
@@ -40,8 +40,8 @@
 import {TRACE_INFO} from 'app/trace_info';
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStore} from 'common/persistent_store';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {ExpandedTimelineToggled, WinscopeEvent} from 'messaging/winscope_event';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
@@ -53,12 +53,12 @@
 import {TimelineComponent} from './timeline_component';
 
 describe('TimelineComponent', () => {
-  const time90 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(90n);
-  const time100 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n);
-  const time101 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(101n);
-  const time105 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(105n);
-  const time110 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n);
-  const time112 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(112n);
+  const time90 = TimestampConverterUtils.makeRealTimestamp(90n);
+  const time100 = TimestampConverterUtils.makeRealTimestamp(100n);
+  const time101 = TimestampConverterUtils.makeRealTimestamp(101n);
+  const time105 = TimestampConverterUtils.makeRealTimestamp(105n);
+  const time110 = TimestampConverterUtils.makeRealTimestamp(110n);
+  const time112 = TimestampConverterUtils.makeRealTimestamp(112n);
 
   const position90 = TracePosition.fromTimestamp(time90);
   const position100 = TracePosition.fromTimestamp(time100);
@@ -114,7 +114,11 @@
     const traces = new TracesBuilder()
       .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110])
       .build();
-    assertDefined(component.timelineData).initialize(traces, undefined);
+    assertDefined(component.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     fixture.detectChanges();
 
     const timelineComponent = assertDefined(component.timeline);
@@ -157,6 +161,7 @@
     assertDefined(assertDefined(component.timelineData)).initialize(
       traces,
       undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
     );
     fixture.detectChanges();
 
@@ -202,7 +207,11 @@
       .setTimestamps(TraceType.SURFACE_FLINGER, [])
       .setTimestamps(TraceType.WINDOW_MANAGER, [time100])
       .build();
-    assertDefined(component.timelineData).initialize(traces, undefined);
+    assertDefined(component.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     fixture.detectChanges();
   });
 
@@ -573,7 +582,7 @@
         ?.timestamp.getValueNs(),
     ).toEqual(100n);
     const timeInputField = assertDefined(
-      fixture.debugElement.query(By.css('.time-input.real')),
+      fixture.debugElement.query(By.css('.time-input.human')),
     );
 
     testCurrentTimestampOnTimeInput(
@@ -766,7 +775,11 @@
       .build();
 
     const timelineData = assertDefined(hostComponent.timelineData);
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     hostComponent.availableTraces = [
       TraceType.SURFACE_FLINGER,
       TraceType.WINDOW_MANAGER,
@@ -783,7 +796,11 @@
       .setTimestamps(TraceType.SCREEN_RECORDING, [time110])
       .setTimestamps(TraceType.PROTO_LOG, [time100])
       .build();
-    assertDefined(hostComponent.timelineData).initialize(traces, undefined);
+    assertDefined(hostComponent.timelineData).initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     hostComponent.availableTraces = [
       TraceType.SURFACE_FLINGER,
       TraceType.WINDOW_MANAGER,
diff --git a/tools/winscope/src/app/components/timeline/timeline_utils.ts b/tools/winscope/src/app/components/timeline/timeline_utils.ts
index 350b1f4..62d776a 100644
--- a/tools/winscope/src/app/components/timeline/timeline_utils.ts
+++ b/tools/winscope/src/app/components/timeline/timeline_utils.ts
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
-import {TimeRange, Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimeRange, Timestamp} from 'common/time';
+import {ComponentTimestampConverter} from 'common/timestamp_converter';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 
 export class TimelineUtils {
   static getTimeRangeForTransition(
     transition: PropertyTreeNode,
-    timestampType: TimestampType,
     fullTimeRange: TimeRange,
+    converter: ComponentTimestampConverter,
   ): TimeRange | undefined {
     const shellData = transition.getChildByName('shellData');
     const wmData = transition.getChildByName('wmData');
@@ -50,50 +50,20 @@
       return undefined;
     }
 
-    let dispatchTimeNs: bigint | undefined;
-    let finishTimeNs: bigint | undefined;
-
     const timeRangeMin = fullTimeRange.from.getValueNs();
     const timeRangeMax = fullTimeRange.to.getValueNs();
 
-    if (timestampType === TimestampType.ELAPSED) {
-      const startOffset =
-        shellData
-          ?.getChildByName('realToElapsedTimeOffsetTimestamp')
-          ?.getValue()
-          .getValueNs() ?? 0n;
-      const finishOffset = aborted
-        ? startOffset
-        : shellData
-            ?.getChildByName('realToElapsedTimeOffsetTimestamp')
-            ?.getValue()
-            .getValueNs() ?? 0n;
+    const dispatchTimeNs = dispatchTimestamp
+      ? dispatchTimestamp.getValueNs()
+      : timeRangeMin;
+    const finishTimeNs = finishOrAbortTimestamp
+      ? finishOrAbortTimestamp.getValueNs()
+      : timeRangeMax;
 
-      dispatchTimeNs = dispatchTimestamp
-        ? dispatchTimestamp.getValueNs() - startOffset
-        : timeRangeMin;
-      finishTimeNs = finishOrAbortTimestamp
-        ? finishOrAbortTimestamp.getValueNs() - finishOffset
-        : timeRangeMax;
-    } else {
-      dispatchTimeNs = dispatchTimestamp
-        ? dispatchTimestamp.getValueNs()
-        : timeRangeMin;
-      finishTimeNs = finishOrAbortTimestamp
-        ? finishOrAbortTimestamp.getValueNs()
-        : timeRangeMax;
-    }
-
-    const startTime = NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(
-      timestampType,
+    const startTime = converter.makeTimestampFromNs(
       dispatchTimeNs > timeRangeMin ? dispatchTimeNs : timeRangeMin,
-      0n,
     );
-    const finishTime = NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(
-      timestampType,
-      finishTimeNs,
-      0n,
-    );
+    const finishTime = converter.makeTimestampFromNs(finishTimeNs);
 
     return {
       from: startTime,
diff --git a/tools/winscope/src/app/loaded_parsers.ts b/tools/winscope/src/app/loaded_parsers.ts
index 1ac0efe..e5ecd72 100644
--- a/tools/winscope/src/app/loaded_parsers.ts
+++ b/tools/winscope/src/app/loaded_parsers.ts
@@ -15,7 +15,8 @@
  */
 
 import {FileUtils} from 'common/file_utils';
-import {INVALID_TIME_NS, TimeRange, TimestampType} from 'common/time';
+import {INVALID_TIME_NS, TimeRange} from 'common/time';
+import {TIME_UNIT_TO_NANO} from 'common/time_units';
 import {UserNotificationsListener} from 'messaging/user_notifications_listener';
 import {TraceHasOldData, TraceOverridden} from 'messaging/user_warnings';
 import {FileAndParser} from 'parsers/file_and_parser';
@@ -26,8 +27,16 @@
 import {TRACE_INFO} from './trace_info';
 
 export class LoadedParsers {
-  static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_TRACES_NS =
-    5n * 60n * 1000000000n; // 5m
+  static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_TRACES_NS = BigInt(
+    5 * TIME_UNIT_TO_NANO.m,
+  ); // 5m
+  static readonly MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET = BigInt(
+    5 * TIME_UNIT_TO_NANO.s,
+  ); // 5s
+  static readonly REAL_TIME_TRACES_WITHOUT_RTE_OFFSET = [
+    TraceType.CUJS,
+    TraceType.EVENT_LOG,
+  ];
 
   private legacyParsers = new Map<TraceType, FileAndParser>();
   private perfettoParsers = new Map<TraceType, FileAndParser>();
@@ -40,7 +49,14 @@
     if (perfettoParsers) {
       this.addPerfettoParsers(perfettoParsers, userNotificationsListener);
     }
-
+    // Traces were simultaneously upgraded to contain real-to-boottime or real-to-monotonic offsets.
+    // If we have a mix of parsers with and without offsets, the ones without must be dangling
+    // trace files with old data, and should be filtered out.
+    legacyParsers = this.filterOutParsersWithoutOffsetsIfRequired(
+      legacyParsers,
+      perfettoParsers,
+      userNotificationsListener,
+    );
     legacyParsers = this.filterOutLegacyParsersWithOldData(
       legacyParsers,
       userNotificationsListener,
@@ -100,21 +116,6 @@
     return await FileUtils.createZipArchive(uniqueArchiveFiles);
   }
 
-  findCommonTimestampType(): TimestampType | undefined {
-    return this.findCommonTimestampTypeInternal(this.getParsers());
-  }
-  private findCommonTimestampTypeInternal(
-    parsers: Array<Parser<object>>,
-  ): TimestampType | undefined {
-    const priorityOrder = [TimestampType.REAL, TimestampType.ELAPSED];
-    for (const type of priorityOrder) {
-      if (parsers.every((parser) => parser.getTimestamps(type) !== undefined)) {
-        return type;
-      }
-    }
-    return undefined;
-  }
-
   private addLegacyParsers(
     parsers: FileAndParser[],
     userNotificationsListener: UserNotificationsListener,
@@ -211,22 +212,58 @@
     newLegacyParsers: FileAndParser[],
     userNotificationsListener: UserNotificationsListener,
   ): FileAndParser[] {
-    const allParsers = [
+    let allParsers = [
       ...newLegacyParsers,
       ...this.legacyParsers.values(),
       ...this.perfettoParsers.values(),
     ];
 
-    const commonTimestampType = this.findCommonTimestampTypeInternal(
-      allParsers.map(({parser}) => parser),
+    const latestMonotonicOffset = this.getLatestRealToMonotonicOffset(
+      allParsers.map(({parser, file}) => parser),
     );
-    if (commonTimestampType === undefined) {
-      return newLegacyParsers;
-    }
+    const latestBootTimeOffset = this.getLatestRealToBootTimeOffset(
+      allParsers.map(({parser, file}) => parser),
+    );
+
+    newLegacyParsers = newLegacyParsers.filter(({parser, file}) => {
+      const monotonicOffset = parser.getRealToMonotonicTimeOffsetNs();
+      if (monotonicOffset && latestMonotonicOffset) {
+        const isOldData =
+          Math.abs(Number(monotonicOffset - latestMonotonicOffset)) >
+          LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET;
+        if (isOldData) {
+          userNotificationsListener.onNotifications([
+            new TraceHasOldData(file.getDescriptor()),
+          ]);
+          return false;
+        }
+      }
+
+      const bootTimeOffset = parser.getRealToBootTimeOffsetNs();
+      if (bootTimeOffset && latestBootTimeOffset) {
+        const isOldData =
+          Math.abs(Number(bootTimeOffset - latestBootTimeOffset)) >
+          LoadedParsers.MAX_ALLOWED_TIME_GAP_BETWEEN_RTE_OFFSET;
+        if (isOldData) {
+          userNotificationsListener.onNotifications([
+            new TraceHasOldData(file.getDescriptor()),
+          ]);
+          return false;
+        }
+      }
+
+      return true;
+    });
+
+    allParsers = [
+      ...newLegacyParsers,
+      ...this.legacyParsers.values(),
+      ...this.perfettoParsers.values(),
+    ];
 
     const timeRanges = allParsers
       .map(({parser}) => {
-        const timestamps = parser.getTimestamps(commonTimestampType);
+        const timestamps = parser.getTimestamps();
         if (!timestamps || timestamps.length === 0) {
           return undefined;
         }
@@ -246,7 +283,7 @@
         return true;
       }
 
-      const timestamps = parser.getTimestamps(commonTimestampType);
+      const timestamps = parser.getTimestamps();
       if (!timestamps || timestamps.length === 0) {
         return true;
       }
@@ -320,6 +357,50 @@
     return newLegacyParsers;
   }
 
+  private filterOutParsersWithoutOffsetsIfRequired(
+    newLegacyParsers: FileAndParser[],
+    perfettoParsers: FileAndParsers | undefined,
+    userNotificationsListener: UserNotificationsListener,
+  ): FileAndParser[] {
+    const hasParserWithOffset =
+      perfettoParsers ||
+      newLegacyParsers.find(({parser, file}) => {
+        return (
+          parser.getRealToBootTimeOffsetNs() !== undefined ||
+          parser.getRealToMonotonicTimeOffsetNs() !== undefined
+        );
+      });
+    const hasParserWithoutOffset = newLegacyParsers.find(({parser, file}) => {
+      return (
+        parser.getRealToBootTimeOffsetNs() === undefined &&
+        parser.getRealToMonotonicTimeOffsetNs() === undefined
+      );
+    });
+
+    if (hasParserWithOffset && hasParserWithoutOffset) {
+      return newLegacyParsers.filter(({parser, file}) => {
+        if (
+          LoadedParsers.REAL_TIME_TRACES_WITHOUT_RTE_OFFSET.some(
+            (traceType) => parser.getTraceType() === traceType,
+          )
+        ) {
+          return true;
+        }
+        const hasOffset =
+          parser.getRealToMonotonicTimeOffsetNs() !== undefined ||
+          parser.getRealToBootTimeOffsetNs() !== undefined;
+        if (!hasOffset) {
+          userNotificationsListener.onNotifications([
+            new TraceHasOldData(parser.getDescriptors().join()),
+          ]);
+        }
+        return hasOffset;
+      });
+    }
+
+    return newLegacyParsers;
+  }
+
   private findLastTimeGapAboveThreshold(
     ranges: readonly TimeRange[],
   ): TimeRange | undefined {
@@ -338,4 +419,34 @@
 
     return undefined;
   }
+
+  getLatestRealToMonotonicOffset(
+    parsers: Array<Parser<object>>,
+  ): bigint | undefined {
+    const p = parsers
+      .filter((offset) => offset.getRealToMonotonicTimeOffsetNs() !== undefined)
+      .sort((a, b) => {
+        return Number(
+          (a.getRealToMonotonicTimeOffsetNs() ?? 0n) -
+            (b.getRealToMonotonicTimeOffsetNs() ?? 0n),
+        );
+      })
+      .at(-1);
+    return p?.getRealToMonotonicTimeOffsetNs();
+  }
+
+  getLatestRealToBootTimeOffset(
+    parsers: Array<Parser<object>>,
+  ): bigint | undefined {
+    const p = parsers
+      .filter((offset) => offset.getRealToBootTimeOffsetNs() !== undefined)
+      .sort((a, b) => {
+        return Number(
+          (a.getRealToBootTimeOffsetNs() ?? 0n) -
+            (b.getRealToBootTimeOffsetNs() ?? 0n),
+        );
+      })
+      .at(-1);
+    return p?.getRealToBootTimeOffsetNs();
+  }
 }
diff --git a/tools/winscope/src/app/loaded_parsers_test.ts b/tools/winscope/src/app/loaded_parsers_test.ts
index f2bf1b6..4944835 100644
--- a/tools/winscope/src/app/loaded_parsers_test.ts
+++ b/tools/winscope/src/app/loaded_parsers_test.ts
@@ -15,34 +15,41 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimeRange, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimeRange} from 'common/time';
 import {UserWarning} from 'messaging/user_warning';
 import {TraceHasOldData, TraceOverridden} from 'messaging/user_warnings';
 import {FileAndParser} from 'parsers/file_and_parser';
 import {FileAndParsers} from 'parsers/file_and_parsers';
 import {ParserBuilder} from 'test/unit/parser_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
 import {TraceType} from 'trace/trace_type';
 import {LoadedParsers} from './loaded_parsers';
 
 describe('LoadedParsers', () => {
-  const realZeroTimestamp = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n);
-  const elapsedZeroTimestamp =
-    NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n);
+  const realZeroTimestamp = TimestampConverterUtils.makeRealTimestamp(0n);
+  const elapsedZeroTimestamp = TimestampConverterUtils.makeElapsedTimestamp(0n);
   const oldTimestamps = [
     realZeroTimestamp,
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1n),
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(2n),
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(3n),
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(4n),
+    TimestampConverterUtils.makeRealTimestamp(1n),
+    TimestampConverterUtils.makeRealTimestamp(2n),
+    TimestampConverterUtils.makeRealTimestamp(3n),
+    TimestampConverterUtils.makeRealTimestamp(4n),
+  ];
+
+  const elapsedTimestamps = [
+    elapsedZeroTimestamp,
+    TimestampConverterUtils.makeElapsedTimestamp(1n),
+    TimestampConverterUtils.makeElapsedTimestamp(2n),
+    TimestampConverterUtils.makeElapsedTimestamp(3n),
+    TimestampConverterUtils.makeElapsedTimestamp(4n),
   ];
 
   const timestamps = [
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(5n * 60n * 1000000000n + 10n), // 5m10ns
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(5n * 60n * 1000000000n + 11n), // 5m11ns
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(5n * 60n * 1000000000n + 12n), // 5m12ns
+    TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 10n), // 5m10ns
+    TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 11n), // 5m11ns
+    TimestampConverterUtils.makeRealTimestamp(5n * 60n * 1000000000n + 12n), // 5m12ns
   ];
 
   const filename = 'filename';
@@ -68,6 +75,12 @@
     .setTimestamps([])
     .setDescriptors([filename])
     .build();
+  const parserSf_elapsed = new ParserBuilder<object>()
+    .setType(TraceType.SURFACE_FLINGER)
+    .setTimestamps(elapsedTimestamps)
+    .setDescriptors([filename])
+    .setNoOffsets(true)
+    .build();
   const parserWm0 = new ParserBuilder<object>()
     .setType(TraceType.WINDOW_MANAGER)
     .setTimestamps(timestamps)
@@ -83,6 +96,12 @@
     .setTimestamps([realZeroTimestamp])
     .setDescriptors([filename])
     .build();
+  const parserWm_elapsed = new ParserBuilder<object>()
+    .setType(TraceType.WINDOW_MANAGER)
+    .setTimestamps(elapsedTimestamps)
+    .setDescriptors([filename])
+    .setNoOffsets(true)
+    .build();
   const parserWmTransitions = new ParserBuilder<object>()
     .setType(TraceType.WM_TRANSITION)
     .setTimestamps([
@@ -92,6 +111,12 @@
     ])
     .setDescriptors([filename])
     .build();
+  const parserEventlog = new ParserBuilder<object>()
+    .setType(TraceType.EVENT_LOG)
+    .setTimestamps(timestamps)
+    .setDescriptors([filename])
+    .setNoOffsets(true)
+    .build();
 
   let loadedParsers: LoadedParsers;
   let warnings: UserWarning[] = [];
@@ -132,13 +157,26 @@
     expectLoadResult([parserWm0], [new TraceOverridden(filename)]);
   });
 
+  it('drops elapsed-only parsers if parsers with real timestamps present', () => {
+    loadParsers([parserSf_elapsed, parserSf0], []);
+    expectLoadResult([parserSf0], [new TraceHasOldData(filename)]);
+  });
+
+  it('doesnt drop elapsed-only parsers if no parsers with real timestamps present', () => {
+    loadParsers([parserSf_elapsed, parserWm_elapsed], []);
+    expectLoadResult([parserSf_elapsed, parserWm_elapsed], []);
+  });
+
+  it('keeps real-time parsers without offset', () => {
+    loadParsers([parserSf0, parserEventlog], []);
+    expectLoadResult([parserSf0, parserEventlog], []);
+  });
+
   describe('drops legacy parser with old data (dangling old trace file)', () => {
     const timeGapFrom = assertDefined(
-      parserSf_longButOldData.getTimestamps(TimestampType.REAL)?.at(-1),
+      parserSf_longButOldData.getTimestamps()?.at(-1),
     );
-    const timeGapTo = assertDefined(
-      parserWm0.getTimestamps(TimestampType.REAL)?.at(0),
-    );
+    const timeGapTo = assertDefined(parserWm0.getTimestamps()?.at(0));
     const timeGap = new TimeRange(timeGapFrom, timeGapTo);
 
     it('taking into account other legacy parsers', () => {
@@ -174,9 +212,7 @@
 
     it('is robust to traces with time range overlap', () => {
       const parser = parserSf0;
-      const timestamps = assertDefined(
-        parserSf0.getTimestamps(TimestampType.REAL),
-      );
+      const timestamps = assertDefined(parserSf0.getTimestamps());
 
       const timestampsOverlappingFront = [
         timestamps[0].add(-1n),
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index 824ac9e..0b6c76b 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {Timestamp} from 'common/time';
 import {TimeUtils} from 'common/time_utils';
+import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
 import {Analytics} from 'logging/analytics';
 import {ProgressListener} from 'messaging/progress_listener';
 import {UserNotificationsListener} from 'messaging/user_notifications_listener';
@@ -41,7 +41,7 @@
 export class Mediator {
   private abtChromeExtensionProtocol: WinscopeEventEmitter &
     WinscopeEventListener;
-  private crossToolProtocol: WinscopeEventEmitter & WinscopeEventListener;
+  private crossToolProtocol: CrossToolProtocol;
   private uploadTracesComponent?: ProgressListener;
   private collectTracesComponent?: ProgressListener & WinscopeEventListener;
   private traceViewComponent?: WinscopeEventEmitter & WinscopeEventListener;
@@ -55,14 +55,14 @@
   private viewers: Viewer[] = [];
   private focusedTabView: undefined | View;
   private areViewersLoaded = false;
-  private lastRemoteToolTimestampReceived: Timestamp | undefined;
+  private lastRemoteToolRealNsReceived: bigint | undefined;
   private currentProgressListener?: ProgressListener;
 
   constructor(
     tracePipeline: TracePipeline,
     timelineData: TimelineData,
     abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener,
-    crossToolProtocol: WinscopeEventEmitter & WinscopeEventListener,
+    crossToolProtocol: CrossToolProtocol,
     appComponent: WinscopeEventListener,
     userNotificationsListener: UserNotificationsListener,
     storage: Storage,
@@ -254,12 +254,8 @@
     });
 
     if (!omitCrossToolProtocol) {
-      const utcTimestamp = position.timestamp.toUTC();
-      const utcPosition = position.entry
-        ? TracePosition.fromTraceEntry(position.entry, utcTimestamp)
-        : TracePosition.fromTimestamp(utcTimestamp);
-      const utcEvent = new TracePositionUpdate(utcPosition);
-      promises.push(this.crossToolProtocol.onWinscopeEvent(utcEvent));
+      const event = new TracePositionUpdate(position);
+      promises.push(this.crossToolProtocol.onWinscopeEvent(event));
     }
 
     await Promise.all(promises);
@@ -285,23 +281,21 @@
   }
 
   private async processRemoteToolTimestampReceived(timestampNs: bigint) {
-    const factory = this.tracePipeline.getTimestampFactory();
-    const timestamp = factory.makeRealTimestamp(timestampNs);
-    this.lastRemoteToolTimestampReceived = timestamp;
+    const timestamp = this.tracePipeline
+      .getTimestampConverter()
+      .tryMakeTimestampFromRealNs(timestampNs);
+    if (timestamp === undefined) {
+      console.warn(
+        'Cannot apply new timestamp received from remote tool, as Winscope is only accepting elapsed timestamps for the loaded traces.',
+      );
+      return;
+    }
+    this.lastRemoteToolRealNsReceived = timestamp.getValueNs();
 
     if (!this.areViewersLoaded) {
       return; // apply timestamp later when traces are visualized
     }
 
-    if (this.timelineData.getTimestampType() !== timestamp.getType()) {
-      console.warn(
-        'Cannot apply new timestamp received from remote tool.' +
-          ` Remote tool notified timestamp type ${timestamp.getType()},` +
-          ` but Winscope is accepting timestamp type ${this.timelineData.getTimestampType()}.`,
-      );
-      return;
-    }
-
     const position = this.timelineData.makePositionFromActiveTrace(timestamp);
     this.timelineData.setPosition(position);
 
@@ -343,6 +337,11 @@
     await this.timelineData.initialize(
       this.tracePipeline.getTraces(),
       await this.tracePipeline.getScreenRecordingVideo(),
+      this.tracePipeline.getTimestampConverter(),
+    );
+
+    this.crossToolProtocol.setTimestampConverter(
+      this.tracePipeline.getTimestampConverter(),
     );
 
     this.viewers = new ViewerFactory().createViewers(
@@ -384,14 +383,15 @@
   }
 
   private getInitialTracePosition(): TracePosition | undefined {
-    if (
-      this.lastRemoteToolTimestampReceived &&
-      this.timelineData.getTimestampType() ===
-        this.lastRemoteToolTimestampReceived.getType()
-    ) {
-      return this.timelineData.makePositionFromActiveTrace(
-        this.lastRemoteToolTimestampReceived,
-      );
+    if (this.lastRemoteToolRealNsReceived !== undefined) {
+      const lastRemoteToolTimestamp = this.tracePipeline
+        .getTimestampConverter()
+        .tryMakeTimestampFromRealNs(this.lastRemoteToolRealNsReceived);
+      if (lastRemoteToolTimestamp) {
+        return this.timelineData.makePositionFromActiveTrace(
+          lastRemoteToolTimestamp,
+        );
+      }
     }
 
     const position = this.timelineData.getCurrentPosition();
@@ -426,8 +426,9 @@
     this.timelineData.clear();
     this.viewers = [];
     this.areViewersLoaded = false;
-    this.lastRemoteToolTimestampReceived = undefined;
+    this.lastRemoteToolRealNsReceived = undefined;
     this.focusedTabView = undefined;
+    this.crossToolProtocol?.setTimestampConverter(undefined);
     await this.appComponent.onWinscopeEvent(new ViewersUnloaded());
   }
 
diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts
index 99583ab..56a06a4 100644
--- a/tools/winscope/src/app/mediator_test.ts
+++ b/tools/winscope/src/app/mediator_test.ts
@@ -16,10 +16,9 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {FunctionUtils} from 'common/function_utils';
-import {
-  NO_TIMEZONE_OFFSET_FACTORY,
-  TimestampFactory,
-} from 'common/timestamp_factory';
+import {TimezoneInfo} from 'common/time';
+import {TimestampConverter} from 'common/timestamp_converter';
+import {CrossToolProtocol} from 'cross_tool/cross_tool_protocol';
 import {ProgressListener} from 'messaging/progress_listener';
 import {ProgressListenerStub} from 'messaging/progress_listener_stub';
 import {UserNotificationsListener} from 'messaging/user_notifications_listener';
@@ -46,6 +45,7 @@
 import {WinscopeEventListener} from 'messaging/winscope_event_listener';
 import {WinscopeEventListenerStub} from 'messaging/winscope_event_listener_stub';
 import {MockStorage} from 'test/unit/mock_storage';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
@@ -75,7 +75,7 @@
   let tracePipeline: TracePipeline;
   let timelineData: TimelineData;
   let abtChromeExtensionProtocol: WinscopeEventEmitter & WinscopeEventListener;
-  let crossToolProtocol: WinscopeEventEmitter & WinscopeEventListener;
+  let crossToolProtocol: CrossToolProtocol;
   let appComponent: WinscopeEventListener;
   let timelineComponent: WinscopeEventEmitter & WinscopeEventListener;
   let uploadTracesComponent: ProgressListenerStub;
@@ -87,8 +87,8 @@
   const viewers = [viewerStub0, viewerStub1, viewerOverlay];
   let tracePositionUpdateListeners: WinscopeEventListener[];
 
-  const TIMESTAMP_10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-  const TIMESTAMP_11 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n);
+  const TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const TIMESTAMP_11 = TimestampConverterUtils.makeRealTimestamp(11n);
 
   const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10);
   const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11);
@@ -116,10 +116,7 @@
       new WinscopeEventEmitterStub(),
       new WinscopeEventListenerStub(),
     );
-    crossToolProtocol = FunctionUtils.mixin(
-      new WinscopeEventEmitterStub(),
-      new WinscopeEventListenerStub(),
-    );
+    crossToolProtocol = new CrossToolProtocol();
     appComponent = new WinscopeEventListenerStub();
     timelineComponent = FunctionUtils.mixin(
       new WinscopeEventEmitterStub(),
@@ -270,19 +267,20 @@
   });
 
   it('propagates trace position update according to timezone', async () => {
-    const timezoneInfo = {
+    const timezoneInfo: TimezoneInfo = {
       timezone: 'Asia/Kolkata',
       locale: 'en-US',
+      utcOffsetMs: 19800000,
     };
-    const factory = new TimestampFactory(timezoneInfo);
-    spyOn(tracePipeline, 'getTimestampFactory').and.returnValue(factory);
+    const converter = new TimestampConverter(timezoneInfo, 0n);
+    spyOn(tracePipeline, 'getTimestampConverter').and.returnValue(converter);
     await loadFiles();
     await loadTraceView();
 
     // notify position
     resetSpyCalls();
     const expectedPosition = TracePosition.fromTimestamp(
-      factory.makeRealTimestamp(10n),
+      converter.makeTimestampFromRealNs(10n),
     );
     await mediator.onWinscopeEvent(new TracePositionUpdate(expectedPosition));
     checkTracePositionUpdateEvents(
@@ -300,7 +298,7 @@
     resetSpyCalls();
     const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs();
     const timestamp =
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(finalTimestampNs);
+      TimestampConverterUtils.makeRealTimestamp(finalTimestampNs);
     const position = TracePosition.fromTimestamp(timestamp);
 
     await mediator.onWinscopeEvent(new TracePositionUpdate(position, true));
@@ -337,6 +335,7 @@
 
   describe('timestamp received from remote tool', () => {
     it('propagates trace position update', async () => {
+      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
       await loadFiles();
       await loadTraceView();
 
@@ -362,12 +361,13 @@
     });
 
     it('propagates trace position update according to timezone', async () => {
-      const timezoneInfo = {
+      const timezoneInfo: TimezoneInfo = {
         timezone: 'Asia/Kolkata',
         locale: 'en-US',
+        utcOffsetMs: 19800000,
       };
-      const factory = new TimestampFactory(timezoneInfo);
-      spyOn(tracePipeline, 'getTimestampFactory').and.returnValue(factory);
+      const converter = new TimestampConverter(timezoneInfo, 0n);
+      spyOn(tracePipeline, 'getTimestampConverter').and.returnValue(converter);
       await loadFiles();
       await loadTraceView();
 
@@ -375,7 +375,7 @@
       resetSpyCalls();
 
       const expectedPosition = TracePosition.fromTimestamp(
-        factory.makeRealTimestamp(10n),
+        converter.makeTimestampFromRealNs(10n),
       );
       await mediator.onWinscopeEvent(new RemoteToolTimestampReceived(10n));
       checkTracePositionUpdateEvents(
@@ -385,6 +385,7 @@
     });
 
     it("doesn't propagate timestamp back to remote tool", async () => {
+      tracePipeline.getTimestampConverter().setRealToMonotonicTimeOffsetNs(0n);
       await loadFiles();
       await loadTraceView();
 
@@ -401,6 +402,8 @@
     });
 
     it('defers trace position propagation till traces are loaded and visualized', async () => {
+      // ensure converter has been used to create real timestamps
+      tracePipeline.getTimestampConverter().makeTimestampFromRealNs(0n);
       // keep timestamp for later
       await mediator.onWinscopeEvent(
         new RemoteToolTimestampReceived(TIMESTAMP_10.getValueNs()),
diff --git a/tools/winscope/src/app/timeline_data.ts b/tools/winscope/src/app/timeline_data.ts
index 799eaf1..5a1c891 100644
--- a/tools/winscope/src/app/timeline_data.ts
+++ b/tools/winscope/src/app/timeline_data.ts
@@ -15,12 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {
-  INVALID_TIME_NS,
-  TimeRange,
-  Timestamp,
-  TimestampType,
-} from 'common/time';
+import {INVALID_TIME_NS, TimeRange, Timestamp} from 'common/time';
+import {ComponentTimestampConverter} from 'common/timestamp_converter';
 import {TimestampUtils} from 'common/timestamp_utils';
 import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
 import {Trace, TraceEntry} from 'trace/trace';
@@ -33,7 +29,6 @@
 export class TimelineData {
   private traces = new Traces();
   private screenRecordingVideo?: Blob;
-  private timestampType?: TimestampType;
   private firstEntry?: TraceEntry<{}>;
   private lastEntry?: TraceEntry<{}>;
   private explicitlySetPosition?: TracePosition;
@@ -47,10 +42,17 @@
   >();
   private activeViewTraceTypes: TraceType[] = []; // dependencies of current active view
   private transitions: PropertyTreeNode[] = []; // cached trace entries to avoid TP and object creation latencies each time transition timeline is redrawn
+  private timestampConverter: ComponentTimestampConverter | undefined;
 
-  async initialize(traces: Traces, screenRecordingVideo: Blob | undefined) {
+  async initialize(
+    traces: Traces,
+    screenRecordingVideo: Blob | undefined,
+    timestampConverter: ComponentTimestampConverter,
+  ) {
     this.clear();
 
+    this.timestampConverter = timestampConverter;
+
     this.traces = new Traces();
     traces.forEachTrace((trace, type) => {
       // Filter out dumps with invalid timestamp (would mess up the timeline)
@@ -74,7 +76,6 @@
     this.screenRecordingVideo = screenRecordingVideo;
     this.firstEntry = this.findFirstEntry();
     this.lastEntry = this.findLastEntry();
-    this.timestampType = this.firstEntry?.getTimestamp().getType();
 
     const types = traces
       .mapTrace((trace, type) => type)
@@ -93,6 +94,10 @@
     return this.transitions;
   }
 
+  getTimestampConverter(): ComponentTimestampConverter | undefined {
+    return this.timestampConverter;
+  }
+
   getCurrentPosition(): TracePosition | undefined {
     if (this.explicitlySetPosition) {
       return this.explicitlySetPosition;
@@ -127,19 +132,6 @@
       return;
     }
 
-    if (position) {
-      if (this.timestampType === undefined) {
-        throw Error(
-          'Attempted to set explicit position but no timestamp type is available',
-        );
-      }
-      if (position.timestamp.getType() !== this.timestampType) {
-        throw Error(
-          'Attempted to set explicit position with incompatible timestamp type',
-        );
-      }
-    }
-
     this.explicitlySetPosition = position;
   }
 
@@ -165,10 +157,6 @@
     this.activeViewTraceTypes = types;
   }
 
-  getTimestampType(): TimestampType | undefined {
-    return this.timestampType;
-  }
-
   getFullTimeRange(): TimeRange {
     if (!this.firstEntry || !this.lastEntry) {
       throw Error('Trying to get full time range when there are no timestamps');
@@ -239,8 +227,8 @@
     }
 
     return ScreenRecordingUtils.timestampToVideoTimeSeconds(
-      firstTimestamp,
-      entry.getTimestamp(),
+      firstTimestamp.getValueNs(),
+      entry.getTimestamp().getValueNs(),
     );
   }
 
@@ -328,7 +316,6 @@
     this.firstEntry = undefined;
     this.lastEntry = undefined;
     this.explicitlySetPosition = undefined;
-    this.timestampType = undefined;
     this.explicitlySetSelection = undefined;
     this.lastReturnedCurrentPosition = undefined;
     this.screenRecordingVideo = undefined;
diff --git a/tools/winscope/src/app/timeline_data_test.ts b/tools/winscope/src/app/timeline_data_test.ts
index 87b625f..6e2eae1 100644
--- a/tools/winscope/src/app/timeline_data_test.ts
+++ b/tools/winscope/src/app/timeline_data_test.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
@@ -24,11 +24,11 @@
 describe('TimelineData', () => {
   let timelineData: TimelineData;
 
-  const timestamp0 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n);
-  const timestamp5 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(5n);
-  const timestamp9 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(9n);
-  const timestamp10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-  const timestamp11 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n);
+  const timestamp0 = TimestampConverterUtils.makeRealTimestamp(0n);
+  const timestamp5 = TimestampConverterUtils.makeRealTimestamp(5n);
+  const timestamp9 = TimestampConverterUtils.makeRealTimestamp(9n);
+  const timestamp10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const timestamp11 = TimestampConverterUtils.makeRealTimestamp(11n);
 
   const traces = new TracesBuilder()
     .setTimestamps(TraceType.PROTO_LOG, [timestamp9])
@@ -47,7 +47,7 @@
     assertDefined(traces.getTrace(TraceType.WINDOW_MANAGER)).getEntry(0),
   );
   const position1000 = TracePosition.fromTimestamp(
-    NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1000n),
+    TimestampConverterUtils.makeRealTimestamp(1000n),
   );
 
   beforeEach(() => {
@@ -57,7 +57,11 @@
   it('can be initialized', () => {
     expect(timelineData.getCurrentPosition()).toBeUndefined();
 
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     expect(timelineData.getCurrentPosition()).toBeDefined();
   });
 
@@ -68,7 +72,11 @@
       .build();
 
     it('drops trace if it is a dump (will not display in timeline UI)', () => {
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(
         timelineData.getTraces().getTrace(TraceType.WINDOW_MANAGER),
       ).toBeUndefined();
@@ -77,7 +85,11 @@
     });
 
     it('is robust to prev/next entry request of a dump', () => {
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(
         timelineData.getPreviousEntryFor(TraceType.WINDOW_MANAGER),
       ).toBeUndefined();
@@ -88,12 +100,20 @@
   });
 
   it('uses first entry of first active trace by default', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     expect(timelineData.getCurrentPosition()).toEqual(position10);
   });
 
   it('uses explicit position if set', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     expect(timelineData.getCurrentPosition()).toEqual(position10);
 
     timelineData.setPosition(position1000);
@@ -107,7 +127,11 @@
   });
 
   it('sets active trace types and update current position accordingly', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     timelineData.setActiveViewTraceTypes([]);
     expect(timelineData.getCurrentPosition()).toEqual(position9);
@@ -131,7 +155,11 @@
     // no trace
     {
       const traces = new TracesBuilder().build();
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(timelineData.hasTimestamps()).toBeFalse();
     }
     // trace without timestamps
@@ -139,7 +167,11 @@
       const traces = new TracesBuilder()
         .setTimestamps(TraceType.SURFACE_FLINGER, [])
         .build();
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(timelineData.hasTimestamps()).toBeFalse();
     }
     // trace with timestamps
@@ -147,7 +179,11 @@
       const traces = new TracesBuilder()
         .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
         .build();
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(timelineData.hasTimestamps()).toBeTrue();
     }
   });
@@ -158,7 +194,11 @@
     // no trace
     {
       const traces = new TracesBuilder().build();
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
     }
     // no distinct timestamps
@@ -167,7 +207,11 @@
         .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
         .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp10])
         .build();
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeFalse();
     }
     // distinct timestamps
@@ -176,13 +220,21 @@
         .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10])
         .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp11])
         .build();
-      timelineData.initialize(traces, undefined);
+      timelineData.initialize(
+        traces,
+        undefined,
+        TimestampConverterUtils.TIMESTAMP_CONVERTER,
+      );
       expect(timelineData.hasMoreThanOneDistinctTimestamp()).toBeTrue();
     }
   });
 
   it('getCurrentPosition() returns same object if no change to range', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     expect(timelineData.getCurrentPosition()).toBe(
       timelineData.getCurrentPosition(),
@@ -196,8 +248,12 @@
   });
 
   it('makePositionFromActiveTrace()', () => {
-    timelineData.initialize(traces, undefined);
-    const time100 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
+    const time100 = TimestampConverterUtils.makeRealTimestamp(100n);
 
     {
       timelineData.setActiveViewTraceTypes([TraceType.SURFACE_FLINGER]);
@@ -219,7 +275,11 @@
   });
 
   it('getFullTimeRange() returns same object if no change to range', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     expect(timelineData.getFullTimeRange()).toBe(
       timelineData.getFullTimeRange(),
@@ -227,7 +287,11 @@
   });
 
   it('getSelectionTimeRange() returns same object if no change to range', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     expect(timelineData.getSelectionTimeRange()).toBe(
       timelineData.getSelectionTimeRange(),
@@ -244,7 +308,11 @@
   });
 
   it('getZoomRange() returns same object if no change to range', () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
 
     expect(timelineData.getZoomRange()).toBe(timelineData.getZoomRange());
 
@@ -257,7 +325,11 @@
   });
 
   it("getCurrentPosition() prioritizes active trace's first entry", () => {
-    timelineData.initialize(traces, undefined);
+    timelineData.initialize(
+      traces,
+      undefined,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
+    );
     timelineData.setActiveViewTraceTypes([TraceType.WINDOW_MANAGER]);
 
     expect(timelineData.getCurrentPosition()?.timestamp).toBe(timestamp11);
diff --git a/tools/winscope/src/app/trace_file_filter.ts b/tools/winscope/src/app/trace_file_filter.ts
index 91a1dc8..45d187e 100644
--- a/tools/winscope/src/app/trace_file_filter.ts
+++ b/tools/winscope/src/app/trace_file_filter.ts
@@ -50,9 +50,6 @@
     const bugreportMainEntry = files.find((file) =>
       file.file.name.endsWith('main_entry.txt'),
     );
-    const bugReportDumpstateBoard = files.find((file) =>
-      file.file.name.endsWith('dumpstate_board.txt'),
-    );
 
     const perfettoFiles = files.filter((file) => this.isPerfettoFile(file));
     const legacyFiles = files.filter((file) => !this.isPerfettoFile(file));
@@ -67,8 +64,9 @@
       };
     }
 
-    const timezoneInfo = await this.processDumpstateBoard(
-      bugReportDumpstateBoard,
+    const timezoneInfo = await this.processRawBugReport(
+      assertDefined(bugreportMainEntry),
+      files,
     );
 
     return await this.filterBugreport(
@@ -79,36 +77,40 @@
     );
   }
 
-  private async processDumpstateBoard(
-    bugReportDumpstateBoard: TraceFile | undefined,
+  private async processRawBugReport(
+    bugreportMainEntry: TraceFile,
+    files: TraceFile[],
   ): Promise<TimezoneInfo | undefined> {
-    if (!bugReportDumpstateBoard) {
+    const bugreportName = (await bugreportMainEntry.file.text()).trim();
+    const rawBugReport = files.find((file) => file.file.name === bugreportName);
+    if (!rawBugReport) {
       return undefined;
     }
 
-    const traceBuffer = new Uint8Array(
-      await bugReportDumpstateBoard.file.arrayBuffer(),
-    );
+    const traceBuffer = new Uint8Array(await rawBugReport.file.arrayBuffer());
     const fileData = new TextDecoder().decode(traceBuffer);
-    const localeStartIndex = fileData.indexOf('[persist.sys.locale]');
-    const timezoneStartIndex = fileData.indexOf('[persist.sys.timezone]');
 
-    if (localeStartIndex === -1 || timezoneStartIndex === -1) {
+    const timezoneStartIndex = fileData.indexOf('[persist.sys.timezone]');
+    if (timezoneStartIndex === -1) {
       return undefined;
     }
-
-    const locale = this.extractValueFromDumpstateBoard(
-      fileData,
-      localeStartIndex,
-    );
-    const timezone = this.extractValueFromDumpstateBoard(
+    const timezone = this.extractValueFromRawBugReport(
       fileData,
       timezoneStartIndex,
     );
-    return {timezone, locale};
+
+    let utcOffsetMs = undefined;
+    const timeOffsetIndex = fileData.indexOf('[persist.sys.time.offset]');
+    if (timeOffsetIndex !== -1) {
+      utcOffsetMs = Number(
+        this.extractValueFromRawBugReport(fileData, timeOffsetIndex),
+      );
+    }
+
+    return {timezone, locale: 'en-US', utcOffsetMs};
   }
 
-  private extractValueFromDumpstateBoard(
+  private extractValueFromRawBugReport(
     fileData: string,
     startIndex: number,
   ): string {
diff --git a/tools/winscope/src/app/trace_file_filter_test.ts b/tools/winscope/src/app/trace_file_filter_test.ts
index 6ef8319..d259844 100644
--- a/tools/winscope/src/app/trace_file_filter_test.ts
+++ b/tools/winscope/src/app/trace_file_filter_test.ts
@@ -132,7 +132,7 @@
       expect(warnings).toEqual([]);
     });
 
-    it('identifies dumpstate_board.txt file', async () => {
+    it('identifies timezone information from bugreport codename file', async () => {
       const legacyFile = makeTraceFile(
         'proto/window_CRITICAL.proto',
         bugreportArchive,
@@ -140,7 +140,6 @@
       const bugreportFiles = [
         await makeBugreportMainEntryTraceFile(),
         await makeBugreportCodenameTraceFile(),
-        await makeBugreportDumpstateBoardTextFile(),
         legacyFile,
       ];
       const result = await filter.filter(bugreportFiles, notificationListener);
@@ -149,6 +148,28 @@
       expect(result.timezoneInfo).toEqual({
         timezone: 'Asia/Kolkata',
         locale: 'en-US',
+        utcOffsetMs: 19800000,
+      });
+      expect(warnings).toEqual([]);
+    });
+
+    it('identifies timezone information from bugreport codename file without time offset', async () => {
+      const legacyFile = makeTraceFile(
+        'proto/window_CRITICAL.proto',
+        bugreportArchive,
+      );
+      const bugreportFiles = [
+        await makeBugreportMainEntryTraceFile(),
+        await makeBugreportCodenameNoTimeOffsetTraceFile(),
+        legacyFile,
+      ];
+      const result = await filter.filter(bugreportFiles, notificationListener);
+      expect(result.legacy).toEqual([legacyFile]);
+      expect(result.perfetto).toBeUndefined();
+      expect(result.timezoneInfo).toEqual({
+        timezone: 'Asia/Kolkata',
+        locale: 'en-US',
+        utcOffsetMs: undefined,
       });
       expect(warnings).toEqual([]);
     });
@@ -233,13 +254,13 @@
     filename: string,
     parentArchive?: File,
     size?: number,
-  ) {
+  ): TraceFile {
     size = size ?? 0;
     const file = new File([new ArrayBuffer(size)], filename);
     return new TraceFile(file as unknown as File, parentArchive);
   }
 
-  async function makeBugreportMainEntryTraceFile() {
+  async function makeBugreportMainEntryTraceFile(): Promise<TraceFile> {
     const file = await UnitTestUtils.getFixtureFile(
       'bugreports/main_entry.txt',
       'main_entry.txt',
@@ -247,15 +268,7 @@
     return new TraceFile(file, bugreportArchive);
   }
 
-  async function makeBugreportDumpstateBoardTextFile() {
-    const file = await UnitTestUtils.getFixtureFile(
-      'bugreports/dumpstate_board.txt',
-      'dumpstate_board.txt',
-    );
-    return new TraceFile(file, bugreportArchive);
-  }
-
-  async function makeBugreportCodenameTraceFile() {
+  async function makeBugreportCodenameTraceFile(): Promise<TraceFile> {
     const file = await UnitTestUtils.getFixtureFile(
       'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
       'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
@@ -263,11 +276,19 @@
     return new TraceFile(file, bugreportArchive);
   }
 
-  async function makeZippedTraceFile() {
+  async function makeZippedTraceFile(): Promise<TraceFile> {
     const file = await UnitTestUtils.getFixtureFile(
       'traces/winscope.zip',
       'FS/data/misc/wmtrace/winscope.zip',
     );
     return new TraceFile(file, bugreportArchive);
   }
+
+  async function makeBugreportCodenameNoTimeOffsetTraceFile(): Promise<TraceFile> {
+    const file = await UnitTestUtils.getFixtureFile(
+      'bugreports/bugreport-codename_beta-no-time-offset-UPB2.230407.019-2023-05-30-14-33-48.txt',
+      'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
+    );
+    return new TraceFile(file, bugreportArchive);
+  }
 });
diff --git a/tools/winscope/src/app/trace_pipeline.ts b/tools/winscope/src/app/trace_pipeline.ts
index c7d7842..db5eb94 100644
--- a/tools/winscope/src/app/trace_pipeline.ts
+++ b/tools/winscope/src/app/trace_pipeline.ts
@@ -15,18 +15,15 @@
  */
 
 import {FileUtils} from 'common/file_utils';
+import {INVALID_TIME_NS} from 'common/time';
 import {
-  NO_TIMEZONE_OFFSET_FACTORY,
-  TimestampFactory,
-} from 'common/timestamp_factory';
+  TimestampConverter,
+  UTC_TIMEZONE_INFO,
+} from 'common/timestamp_converter';
 import {Analytics} from 'logging/analytics';
 import {ProgressListener} from 'messaging/progress_listener';
 import {UserNotificationsListener} from 'messaging/user_notifications_listener';
-import {
-  CorruptedArchive,
-  NoCommonTimestampType,
-  NoInputFiles,
-} from 'messaging/user_warnings';
+import {CorruptedArchive, NoInputFiles} from 'messaging/user_warnings';
 import {FileAndParsers} from 'parsers/file_and_parsers';
 import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory';
 import {TracesParserFactory} from 'parsers/legacy/traces_parser_factory';
@@ -48,7 +45,7 @@
   private tracesParserFactory = new TracesParserFactory();
   private traces = new Traces();
   private downloadArchiveFilename?: string;
-  private timestampFactory = NO_TIMEZONE_OFFSET_FACTORY;
+  private timestampConverter = new TimestampConverter(UTC_TIMEZONE_INFO);
 
   async loadFiles(
     files: File[],
@@ -83,24 +80,19 @@
 
       this.traces = new Traces();
 
-      const commonTimestampType = this.loadedParsers.findCommonTimestampType();
-      if (commonTimestampType === undefined) {
-        notificationListener.onNotifications([new NoCommonTimestampType()]);
-        return;
-      }
-
       this.loadedParsers.getParsers().forEach((parser) => {
-        const trace = Trace.fromParser(parser, commonTimestampType);
+        const trace = Trace.fromParser(parser);
         this.traces.setTrace(parser.getTraceType(), trace);
         Analytics.Tracing.logTraceLoaded(parser);
       });
 
       const tracesParsers = await this.tracesParserFactory.createParsers(
         this.traces,
+        this.timestampConverter,
       );
 
       tracesParsers.forEach((tracesParser) => {
-        const trace = Trace.fromParser(tracesParser, commonTimestampType);
+        const trace = Trace.fromParser(tracesParser);
         this.traces.setTrace(trace.type, trace);
       });
 
@@ -141,6 +133,16 @@
   }
 
   async buildTraces() {
+    for (const trace of this.traces) {
+      if (trace.lengthEntries === 0) {
+        continue;
+      }
+      const timestamp = trace.getEntry(0).getTimestamp();
+      if (timestamp.getValueNs() !== INVALID_TIME_NS) {
+        this.timestampConverter.initializeUTCOffset(timestamp);
+        break;
+      }
+    }
     await new FrameMapper(this.traces).computeMapping();
   }
 
@@ -152,8 +154,8 @@
     return this.downloadArchiveFilename ?? 'winscope';
   }
 
-  getTimestampFactory(): TimestampFactory {
-    return this.timestampFactory;
+  getTimestampConverter(): TimestampConverter {
+    return this.timestampConverter;
   }
 
   async getScreenRecordingVideo(): Promise<undefined | Blob> {
@@ -170,7 +172,7 @@
   clear() {
     this.loadedParsers.clear();
     this.traces = new Traces();
-    this.timestampFactory = NO_TIMEZONE_OFFSET_FACTORY;
+    this.timestampConverter = new TimestampConverter(UTC_TIMEZONE_INFO);
     this.downloadArchiveFilename = undefined;
   }
 
@@ -184,7 +186,9 @@
       notificationListener,
     );
     if (filterResult.timezoneInfo) {
-      this.timestampFactory = new TimestampFactory(filterResult.timezoneInfo);
+      this.timestampConverter = new TimestampConverter(
+        filterResult.timezoneInfo,
+      );
     }
 
     if (!filterResult.perfetto && filterResult.legacy.length === 0) {
@@ -194,7 +198,7 @@
 
     const legacyParsers = await new LegacyParserFactory().createParsers(
       filterResult.legacy,
-      this.timestampFactory,
+      this.timestampConverter,
       progressListener,
       notificationListener,
     );
@@ -204,13 +208,41 @@
     if (filterResult.perfetto) {
       const parsers = await new PerfettoParserFactory().createParsers(
         filterResult.perfetto,
-        this.timestampFactory,
+        this.timestampConverter,
         progressListener,
         notificationListener,
       );
       perfettoParsers = new FileAndParsers(filterResult.perfetto, parsers);
     }
 
+    const monotonicTimeOffset =
+      this.loadedParsers.getLatestRealToMonotonicOffset(
+        legacyParsers
+          .map((fileAndParser) => fileAndParser.parser)
+          .concat(perfettoParsers?.parsers ?? []),
+      );
+
+    const realToBootTimeOffset =
+      this.loadedParsers.getLatestRealToBootTimeOffset(
+        legacyParsers
+          .map((fileAndParser) => fileAndParser.parser)
+          .concat(perfettoParsers?.parsers ?? []),
+      );
+
+    if (monotonicTimeOffset !== undefined) {
+      this.timestampConverter.setRealToMonotonicTimeOffsetNs(
+        monotonicTimeOffset,
+      );
+    }
+    if (realToBootTimeOffset !== undefined) {
+      this.timestampConverter.setRealToBootTimeOffsetNs(realToBootTimeOffset);
+    }
+
+    perfettoParsers?.parsers.forEach((p) => p.createTimestamps());
+    legacyParsers.forEach((fileAndParser) =>
+      fileAndParser.parser.createTimestamps(),
+    );
+
     this.loadedParsers.addParsers(
       legacyParsers,
       perfettoParsers,
diff --git a/tools/winscope/src/app/trace_pipeline_test.ts b/tools/winscope/src/app/trace_pipeline_test.ts
index 4858ae8..1989296 100644
--- a/tools/winscope/src/app/trace_pipeline_test.ts
+++ b/tools/winscope/src/app/trace_pipeline_test.ts
@@ -16,7 +16,6 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {FileUtils} from 'common/file_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {ProgressListenerStub} from 'messaging/progress_listener_stub';
 import {UserWarning} from 'messaging/user_warning';
 import {
@@ -26,6 +25,7 @@
   TraceOverridden,
   UnsupportedFileFormat,
 } from 'messaging/user_warnings';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesUtils} from 'test/unit/traces_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {TraceType} from 'trace/trace_type';
@@ -152,40 +152,15 @@
     expect(traces.getTrace(TraceType.INPUT_METHOD_CLIENTS)).toBeDefined();
   });
 
-  it('detects bugreports and extracts timezone info from dumpstate_board.txt', async () => {
-    const bugreportFiles = [
-      await UnitTestUtils.getFixtureFile(
-        'bugreports/main_entry.txt',
-        'main_entry.txt',
-      ),
-      await UnitTestUtils.getFixtureFile(
-        'bugreports/dumpstate_board.txt',
-        'dumpstate_board.txt',
-      ),
-      await UnitTestUtils.getFixtureFile(
-        'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
-        'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
-      ),
-      await UnitTestUtils.getFixtureFile(
-        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
-        'FS/data/misc/wmtrace/surface_flinger.bp',
-      ),
-    ];
-    const bugreportArchive = new File(
-      [await FileUtils.createZipArchive(bugreportFiles)],
-      'bugreport.zip',
+  it('detects bugreports and extracts utc offset directly', async () => {
+    await testTimezoneOffsetExtraction(
+      'bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
     );
+  });
 
-    await loadFiles([bugreportArchive]);
-    await expectLoadResult(1, []);
-
-    const timestampFactory = tracePipeline.getTimestampFactory();
-    expect(timestampFactory).not.toEqual(NO_TIMEZONE_OFFSET_FACTORY);
-
-    const expectedTimestamp =
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126889102062832n);
-    expect(timestampFactory.makeRealTimestamp(1659107089102062832n)).toEqual(
-      expectedTimestamp,
+  it('detects bugreports and extracts timezone info, then calculates utc offset', async () => {
+    await testTimezoneOffsetExtraction(
+      'bugreports/bugreport-codename_beta-no-time-offset-UPB2.230407.019-2023-05-30-14-33-48.txt',
     );
   });
 
@@ -410,4 +385,40 @@
     expect(warnings).toEqual(expectedWarnings);
     expect(tracePipeline.getTraces().getSize()).toEqual(numberOfTraces);
   }
+
+  async function testTimezoneOffsetExtraction(codenameFileName: string) {
+    const bugreportFiles = [
+      await UnitTestUtils.getFixtureFile(
+        'bugreports/main_entry.txt',
+        'main_entry.txt',
+      ),
+      await UnitTestUtils.getFixtureFile(
+        codenameFileName,
+        'bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt',
+      ),
+      await UnitTestUtils.getFixtureFile(
+        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
+        'FS/data/misc/wmtrace/surface_flinger.bp',
+      ),
+    ];
+    const bugreportArchive = new File(
+      [await FileUtils.createZipArchive(bugreportFiles)],
+      'bugreport.zip',
+    );
+
+    await loadFiles([bugreportArchive]);
+    await expectLoadResult(1, []);
+
+    const timestampConverter = tracePipeline.getTimestampConverter();
+    expect(timestampConverter);
+    expect(timestampConverter.getUTCOffset()).toEqual('UTC+05:30');
+
+    const expectedTimestamp =
+      TimestampConverterUtils.makeRealTimestampWithUTCOffset(
+        1659107089102062832n,
+      );
+    expect(
+      timestampConverter.makeTimestampFromMonotonicNs(14500282843n),
+    ).toEqual(expectedTimestamp);
+  }
 });
diff --git a/tools/winscope/src/common/time.ts b/tools/winscope/src/common/time.ts
index e41f345..5209660 100644
--- a/tools/winscope/src/common/time.ts
+++ b/tools/winscope/src/common/time.ts
@@ -20,81 +20,57 @@
   constructor(readonly from: Timestamp, readonly to: Timestamp) {}
 }
 
-export enum TimestampType {
-  ELAPSED = 'ELAPSED',
-  REAL = 'REAL',
-}
-
 export interface TimezoneInfo {
   timezone: string;
   locale: string;
+  utcOffsetMs: number | undefined;
+}
+
+export interface TimestampFormatter {
+  format(timestamp: Timestamp, hideNs?: boolean): string;
 }
 
 export class Timestamp {
-  private readonly type: TimestampType;
-  private readonly valueNs: bigint;
-  private readonly timezoneOffset: bigint;
+  private readonly utcValueNs: bigint;
+  private readonly formatter: TimestampFormatter;
 
-  constructor(type: TimestampType, valueNs: bigint, timezoneOffset = 0n) {
-    this.type = type;
-    this.valueNs = valueNs;
-    this.timezoneOffset = timezoneOffset;
-  }
-
-  getType(): TimestampType {
-    return this.type;
+  constructor(valueNs: bigint, formatter: TimestampFormatter) {
+    this.utcValueNs = valueNs;
+    this.formatter = formatter;
   }
 
   getValueNs(): bigint {
-    return this.valueNs;
-  }
-
-  toUTC(): Timestamp {
-    return new Timestamp(this.type, this.valueNs - this.timezoneOffset);
+    return this.utcValueNs;
   }
 
   valueOf(): bigint {
-    return this.getValueNs();
+    return this.utcValueNs;
   }
 
   in(range: TimeRange): boolean {
-    if (range.from.type !== this.type || range.to.type !== this.type) {
-      throw new Error('Mismatching timestamp types');
-    }
-
     return (
       range.from.getValueNs() <= this.getValueNs() &&
       this.getValueNs() <= range.to.getValueNs()
     );
   }
 
-  add(nanoseconds: bigint): Timestamp {
-    return new Timestamp(this.type, this.getValueNs() + nanoseconds);
+  add(n: bigint): Timestamp {
+    return new Timestamp(this.getValueNs() + n, this.formatter);
   }
 
-  plus(timestamp: Timestamp): Timestamp {
-    this.validateTimestampArithmetic(timestamp);
-    return new Timestamp(this.type, timestamp.getValueNs() + this.getValueNs());
-  }
-
-  minus(timestamp: Timestamp): Timestamp {
-    this.validateTimestampArithmetic(timestamp);
-    return new Timestamp(this.type, this.getValueNs() - timestamp.getValueNs());
+  minus(n: bigint): Timestamp {
+    return new Timestamp(this.getValueNs() - n, this.formatter);
   }
 
   times(n: bigint): Timestamp {
-    return new Timestamp(this.type, this.getValueNs() * n);
+    return new Timestamp(this.getValueNs() * n, this.formatter);
   }
 
   div(n: bigint): Timestamp {
-    return new Timestamp(this.type, this.getValueNs() / n);
+    return new Timestamp(this.getValueNs() / n, this.formatter);
   }
 
-  private validateTimestampArithmetic(timestamp: Timestamp) {
-    if (timestamp.type !== this.type) {
-      throw new Error(
-        'Attemping to do timestamp arithmetic on different timestamp types',
-      );
-    }
+  format(hideNs = false): string {
+    return this.formatter.format(this, hideNs);
   }
 }
diff --git a/tools/winscope/src/common/time_duration.ts b/tools/winscope/src/common/time_duration.ts
new file mode 100644
index 0000000..13339e3
--- /dev/null
+++ b/tools/winscope/src/common/time_duration.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TimestampUtils} from './timestamp_utils';
+
+export class TimeDuration {
+  constructor(private timeDiffNs: bigint) {}
+  getValueNs(): bigint {
+    return this.timeDiffNs;
+  }
+
+  format(hideNs = false): string {
+    return TimestampUtils.formatElapsedNs(this.timeDiffNs, hideNs);
+  }
+}
diff --git a/tools/winscope/src/common/time_test.ts b/tools/winscope/src/common/time_test.ts
new file mode 100644
index 0000000..b258721
--- /dev/null
+++ b/tools/winscope/src/common/time_test.ts
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {TIME_UNIT_TO_NANO} from './time_units';
+
+describe('Timestamp', () => {
+  describe('arithmetic', () => {
+    const REAL_TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n);
+    const REAL_TIMESTAMP_20 = TimestampConverterUtils.makeRealTimestamp(20n);
+    const ELAPSED_TIMESTAMP_10 =
+      TimestampConverterUtils.makeElapsedTimestamp(10n);
+    const ELAPSED_TIMESTAMP_20 =
+      TimestampConverterUtils.makeElapsedTimestamp(20n);
+
+    it('can add', () => {
+      let timestamp = REAL_TIMESTAMP_10.add(REAL_TIMESTAMP_20.getValueNs());
+      expect(timestamp.getValueNs()).toBe(30n);
+
+      timestamp = ELAPSED_TIMESTAMP_10.add(ELAPSED_TIMESTAMP_20.getValueNs());
+      expect(timestamp.getValueNs()).toBe(30n);
+    });
+
+    it('can subtract', () => {
+      let timestamp = REAL_TIMESTAMP_20.minus(REAL_TIMESTAMP_10.getValueNs());
+      expect(timestamp.getValueNs()).toBe(10n);
+
+      timestamp = ELAPSED_TIMESTAMP_20.minus(ELAPSED_TIMESTAMP_10.getValueNs());
+      expect(timestamp.getValueNs()).toBe(10n);
+    });
+
+    it('can divide', () => {
+      let timestamp = TimestampConverterUtils.makeRealTimestamp(10n).div(2n);
+      expect(timestamp.getValueNs()).toBe(5n);
+
+      timestamp = ELAPSED_TIMESTAMP_10.div(2n);
+      expect(timestamp.getValueNs()).toBe(5n);
+    });
+  });
+
+  describe('formatting', () => {
+    const MILLISECOND = BigInt(TIME_UNIT_TO_NANO.ms);
+    const SECOND = BigInt(TIME_UNIT_TO_NANO.s);
+    const MINUTE = BigInt(TIME_UNIT_TO_NANO.m);
+    const HOUR = BigInt(TIME_UNIT_TO_NANO.h);
+    const DAY = BigInt(TIME_UNIT_TO_NANO.d);
+
+    it('elapsed timestamps', () => {
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(0n).format(true),
+      ).toEqual('0ms');
+      expect(TimestampConverterUtils.makeElapsedTimestamp(0n).format()).toEqual(
+        '0ns',
+      );
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(1000n).format(true),
+      ).toEqual('0ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(1000n).format(),
+      ).toEqual('1000ns');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(MILLISECOND - 1n).format(
+          true,
+        ),
+      ).toEqual('0ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(MILLISECOND).format(true),
+      ).toEqual('1ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(10n * MILLISECOND).format(
+          true,
+        ),
+      ).toEqual('10ms');
+
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(SECOND - 1n).format(true),
+      ).toEqual('999ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(SECOND).format(true),
+      ).toEqual('1s0ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          SECOND + MILLISECOND,
+        ).format(true),
+      ).toEqual('1s1ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          SECOND + MILLISECOND,
+        ).format(),
+      ).toEqual('1s1ms0ns');
+
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(MINUTE - 1n).format(true),
+      ).toEqual('59s999ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(MINUTE).format(true),
+      ).toEqual('1m0s0ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          MINUTE + SECOND + MILLISECOND,
+        ).format(true),
+      ).toEqual('1m1s1ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          MINUTE + SECOND + MILLISECOND + 1n,
+        ).format(true),
+      ).toEqual('1m1s1ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          MINUTE + SECOND + MILLISECOND + 1n,
+        ).format(),
+      ).toEqual('1m1s1ms1ns');
+
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(HOUR - 1n).format(true),
+      ).toEqual('59m59s999ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(HOUR - 1n).format(),
+      ).toEqual('59m59s999ms999999ns');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(HOUR).format(true),
+      ).toEqual('1h0m0s0ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          HOUR + MINUTE + SECOND + MILLISECOND,
+        ).format(true),
+      ).toEqual('1h1m1s1ms');
+
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(DAY - 1n).format(true),
+      ).toEqual('23h59m59s999ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(DAY).format(true),
+      ).toEqual('1d0h0m0s0ms');
+      expect(
+        TimestampConverterUtils.makeElapsedTimestamp(
+          DAY + HOUR + MINUTE + SECOND + MILLISECOND,
+        ).format(true),
+      ).toEqual('1d1h1m1s1ms');
+    });
+
+    it('real timestamps without timezone info', () => {
+      const NOV_10_2022 = 1668038400000n * MILLISECOND;
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(0n).format(true),
+      ).toEqual('1970-01-01T00:00:00.000');
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(
+          NOV_10_2022 +
+            22n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            186n * MILLISECOND +
+            123212n,
+        ).format(true),
+      ).toEqual('2022-11-10T22:04:54.186');
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(NOV_10_2022).format(true),
+      ).toEqual('2022-11-10T00:00:00.000');
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(NOV_10_2022 + 1n).format(
+          true,
+        ),
+      ).toEqual('2022-11-10T00:00:00.000');
+
+      expect(TimestampConverterUtils.makeRealTimestamp(0n).format()).toEqual(
+        '1970-01-01T00:00:00.000000000',
+      );
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(
+          NOV_10_2022 +
+            22n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            186n * MILLISECOND +
+            123212n,
+        ).format(),
+      ).toEqual('2022-11-10T22:04:54.186123212');
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(NOV_10_2022).format(),
+      ).toEqual('2022-11-10T00:00:00.000000000');
+      expect(
+        TimestampConverterUtils.makeRealTimestamp(NOV_10_2022 + 1n).format(),
+      ).toEqual('2022-11-10T00:00:00.000000001');
+    });
+
+    it('real timestamps with timezone info', () => {
+      const NOV_10_2022 = 1668038400000n * MILLISECOND;
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          0n,
+        ).format(true),
+      ).toEqual('1970-01-01T05:30:00.000');
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          NOV_10_2022 +
+            22n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            186n * MILLISECOND +
+            123212n,
+        ).format(true),
+      ).toEqual('2022-11-11T03:34:54.186');
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          NOV_10_2022,
+        ).format(true),
+      ).toEqual('2022-11-10T05:30:00.000');
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          NOV_10_2022 + 1n,
+        ).format(true),
+      ).toEqual('2022-11-10T05:30:00.000');
+
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          0n,
+        ).format(),
+      ).toEqual('1970-01-01T05:30:00.000000000');
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          NOV_10_2022 +
+            22n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            186n * MILLISECOND +
+            123212n,
+        ).format(),
+      ).toEqual('2022-11-11T03:34:54.186123212');
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          NOV_10_2022,
+        ).format(),
+      ).toEqual('2022-11-10T05:30:00.000000000');
+      expect(
+        TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+          NOV_10_2022 + 1n,
+        ).format(),
+      ).toEqual('2022-11-10T05:30:00.000000001');
+    });
+  });
+});
diff --git a/tools/winscope/src/common/time_units.ts b/tools/winscope/src/common/time_units.ts
new file mode 100644
index 0000000..fbe25d7
--- /dev/null
+++ b/tools/winscope/src/common/time_units.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export enum TIME_UNIT_TO_NANO {
+  ns = 1,
+  ms = 1000000,
+  s = 1000000 * 1000,
+  m = 1000000 * 1000 * 60,
+  h = 1000000 * 1000 * 60 * 60,
+  d = 1000000 * 1000 * 60 * 60 * 24,
+}
+
+export const TIME_UNITS = [
+  {nanosInUnit: TIME_UNIT_TO_NANO['ns'], unit: 'ns'},
+  {nanosInUnit: TIME_UNIT_TO_NANO['ms'], unit: 'ms'},
+  {nanosInUnit: TIME_UNIT_TO_NANO['s'], unit: 's'},
+  {nanosInUnit: TIME_UNIT_TO_NANO['m'], unit: 'm'},
+  {nanosInUnit: TIME_UNIT_TO_NANO['h'], unit: 'h'},
+  {nanosInUnit: TIME_UNIT_TO_NANO['d'], unit: 'd'},
+];
diff --git a/tools/winscope/src/common/times_test.ts b/tools/winscope/src/common/times_test.ts
deleted file mode 100644
index b2520f2..0000000
--- a/tools/winscope/src/common/times_test.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {TimestampType} from './time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from './timestamp_factory';
-
-describe('Timestamp', () => {
-  describe('arithmetic', () => {
-    const REAL_TIMESTAMP_10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-    const REAL_TIMESTAMP_20 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(20n);
-    const ELAPSED_TIMESTAMP_10 =
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n);
-    const ELAPSED_TIMESTAMP_20 =
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(20n);
-
-    it('can add', () => {
-      let timestamp = REAL_TIMESTAMP_10.plus(REAL_TIMESTAMP_20);
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(30n);
-
-      timestamp = ELAPSED_TIMESTAMP_10.plus(ELAPSED_TIMESTAMP_20);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(30n);
-    });
-
-    it('can subtract', () => {
-      let timestamp = REAL_TIMESTAMP_20.minus(REAL_TIMESTAMP_10);
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(10n);
-
-      timestamp = ELAPSED_TIMESTAMP_20.minus(ELAPSED_TIMESTAMP_10);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(10n);
-    });
-
-    it('can divide', () => {
-      let timestamp = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n).div(2n);
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(5n);
-
-      timestamp = ELAPSED_TIMESTAMP_10.div(2n);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(5n);
-    });
-
-    it('fails between different timestamp types', () => {
-      const error = new Error(
-        'Attemping to do timestamp arithmetic on different timestamp types',
-      );
-      expect(() => {
-        REAL_TIMESTAMP_20.minus(ELAPSED_TIMESTAMP_10);
-      }).toThrow(error);
-      expect(() => {
-        REAL_TIMESTAMP_20.plus(ELAPSED_TIMESTAMP_10);
-      }).toThrow(error);
-      expect(() => {
-        ELAPSED_TIMESTAMP_20.minus(REAL_TIMESTAMP_10);
-      }).toThrow(error);
-      expect(() => {
-        ELAPSED_TIMESTAMP_20.plus(REAL_TIMESTAMP_10);
-      }).toThrow(error);
-    });
-  });
-});
diff --git a/tools/winscope/src/common/timestamp_converter.ts b/tools/winscope/src/common/timestamp_converter.ts
new file mode 100644
index 0000000..b5a40f5
--- /dev/null
+++ b/tools/winscope/src/common/timestamp_converter.ts
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {assertDefined, assertTrue} from './assert_utils';
+import {
+  INVALID_TIME_NS,
+  Timestamp,
+  TimestampFormatter,
+  TimezoneInfo,
+} from './time';
+import {TimestampUtils} from './timestamp_utils';
+import {TIME_UNITS, TIME_UNIT_TO_NANO} from './time_units';
+import {UTCOffset} from './utc_offset';
+
+// Pre-T traces do not provide real-to-boottime or real-to-monotonic offsets,so
+// we group their timestamps under the "ELAPSED" umbrella term, and hope that
+// the CPU was not suspended before the tracing session, causing them to diverge.
+enum TimestampType {
+  ELAPSED,
+  REAL,
+}
+
+class RealTimestampFormatter implements TimestampFormatter {
+  constructor(private utcOffset: UTCOffset) {}
+
+  setUTCOffset(value: UTCOffset) {
+    this.utcOffset = value;
+  }
+
+  format(timestamp: Timestamp, hideNs?: boolean | undefined): string {
+    const timestampNanos =
+      timestamp.getValueNs() + (this.utcOffset.getValueNs() ?? 0n);
+    const ms = timestampNanos / 1000000n;
+    const extraNanos = timestampNanos % 1000000n;
+    const formattedTimestamp = new Date(Number(ms))
+      .toISOString()
+      .replace('Z', '');
+
+    if (hideNs) {
+      return formattedTimestamp;
+    } else {
+      return `${formattedTimestamp}${extraNanos.toString().padStart(6, '0')}`;
+    }
+  }
+}
+const REAL_TIMESTAMP_FORMATTER_UTC = new RealTimestampFormatter(
+  new UTCOffset(),
+);
+
+class ElapsedTimestampFormatter {
+  format(timestamp: Timestamp, hideNs = false): string {
+    const timestampNanos = timestamp.getValueNs();
+    return TimestampUtils.formatElapsedNs(timestampNanos, hideNs);
+  }
+}
+const ELAPSED_TIMESTAMP_FORMATTER = new ElapsedTimestampFormatter();
+
+export interface ParserTimestampConverter {
+  makeTimestampFromRealNs(valueNs: bigint): Timestamp;
+  makeTimestampFromMonotonicNs(valueNs: bigint): Timestamp;
+  makeTimestampFromBootTimeNs(valueNs: bigint): Timestamp;
+  makeZeroTimestamp(): Timestamp;
+}
+
+export interface ComponentTimestampConverter {
+  makeTimestampFromHuman(timestampHuman: string): Timestamp;
+  getUTCOffset(): string;
+  makeTimestampFromNs(valueNs: bigint): Timestamp;
+  canMakeRealTimestamps(): boolean;
+}
+
+export interface RemoteToolTimestampConverter {
+  tryMakeTimestampForRemoteTool(timestamp: Timestamp): Timestamp | undefined;
+}
+
+export class TimestampConverter
+  implements
+    ParserTimestampConverter,
+    ComponentTimestampConverter,
+    RemoteToolTimestampConverter
+{
+  private readonly utcOffset = new UTCOffset();
+  private readonly realTimestampFormatter = new RealTimestampFormatter(
+    this.utcOffset,
+  );
+  private createdTimestampType: TimestampType | undefined;
+
+  constructor(
+    private timezoneInfo: TimezoneInfo,
+    private realToMonotonicTimeOffsetNs?: bigint,
+    private realToBootTimeOffsetNs?: bigint,
+  ) {
+    if (timezoneInfo.utcOffsetMs !== undefined) {
+      this.utcOffset.initialize(
+        BigInt(timezoneInfo.utcOffsetMs * TIME_UNIT_TO_NANO.ms),
+      );
+    }
+  }
+
+  initializeUTCOffset(timestamp: Timestamp) {
+    if (
+      this.utcOffset.getValueNs() !== undefined ||
+      !this.canMakeRealTimestamps()
+    ) {
+      return;
+    }
+    const utcValueNs = timestamp.getValueNs();
+    const localNs =
+      this.timezoneInfo.timezone !== 'UTC'
+        ? this.addTimezoneOffset(this.timezoneInfo.timezone, utcValueNs)
+        : utcValueNs;
+    const utcOffsetNs = localNs - utcValueNs;
+    this.utcOffset.initialize(utcOffsetNs);
+    console.warn(
+      'Failed to initialized timezone offset due to invalid time difference.',
+    );
+  }
+
+  setRealToMonotonicTimeOffsetNs(ns: bigint) {
+    if (this.realToMonotonicTimeOffsetNs !== undefined) {
+      return;
+    }
+    this.realToMonotonicTimeOffsetNs = ns;
+  }
+
+  setRealToBootTimeOffsetNs(ns: bigint) {
+    if (this.realToBootTimeOffsetNs !== undefined) {
+      return;
+    }
+    this.realToBootTimeOffsetNs = ns;
+  }
+
+  getUTCOffset(): string {
+    return this.utcOffset.format();
+  }
+
+  makeTimestampFromMonotonicNs(valueNs: bigint): Timestamp {
+    if (this.realToMonotonicTimeOffsetNs !== undefined) {
+      return this.makeRealTimestamp(valueNs + this.realToMonotonicTimeOffsetNs);
+    }
+    return this.makeElapsedTimestamp(valueNs);
+  }
+
+  makeTimestampFromBootTimeNs(valueNs: bigint): Timestamp {
+    if (this.realToBootTimeOffsetNs !== undefined) {
+      return this.makeRealTimestamp(valueNs + this.realToBootTimeOffsetNs);
+    }
+    return this.makeElapsedTimestamp(valueNs);
+  }
+
+  makeTimestampFromRealNs(valueNs: bigint): Timestamp {
+    return this.makeRealTimestamp(valueNs);
+  }
+
+  tryMakeTimestampFromRealNs(valueNs: bigint): Timestamp | undefined {
+    if (!this.canMakeRealTimestamps()) {
+      return undefined;
+    }
+    return this.makeRealTimestamp(valueNs);
+  }
+
+  makeTimestampFromHuman(timestampHuman: string): Timestamp {
+    if (TimestampUtils.HUMAN_ELAPSED_TIMESTAMP_REGEX.test(timestampHuman)) {
+      return this.makeTimestampfromHumanElapsed(timestampHuman);
+    }
+
+    if (TimestampUtils.HUMAN_REAL_TIMESTAMP_REGEX.test(timestampHuman)) {
+      return assertDefined(this.makeTimestampFromHumanReal(timestampHuman));
+    }
+
+    throw Error('Invalid timestamp format');
+  }
+
+  makeTimestampFromNs(valueNs: bigint): Timestamp {
+    return new Timestamp(
+      valueNs,
+      this.canMakeRealTimestamps()
+        ? this.realTimestampFormatter
+        : ELAPSED_TIMESTAMP_FORMATTER,
+    );
+  }
+
+  makeZeroTimestamp(): Timestamp {
+    if (this.canMakeRealTimestamps()) {
+      return new Timestamp(INVALID_TIME_NS, REAL_TIMESTAMP_FORMATTER_UTC);
+    } else {
+      return new Timestamp(INVALID_TIME_NS, ELAPSED_TIMESTAMP_FORMATTER);
+    }
+  }
+
+  tryMakeTimestampForRemoteTool(timestamp: Timestamp): Timestamp | undefined {
+    if (this.canMakeRealTimestamps()) {
+      return timestamp;
+    }
+    return undefined;
+  }
+
+  canMakeRealTimestamps(): boolean {
+    return this.createdTimestampType === TimestampType.REAL;
+  }
+
+  private makeRealTimestamp(valueNs: bigint): Timestamp {
+    assertTrue(
+      this.createdTimestampType === undefined ||
+        this.createdTimestampType === TimestampType.REAL,
+    );
+    this.createdTimestampType = TimestampType.REAL;
+    return new Timestamp(valueNs, this.realTimestampFormatter);
+  }
+
+  private makeElapsedTimestamp(valueNs: bigint): Timestamp {
+    assertTrue(
+      this.createdTimestampType === undefined ||
+        this.createdTimestampType === TimestampType.ELAPSED,
+    );
+    this.createdTimestampType = TimestampType.ELAPSED;
+    return new Timestamp(valueNs, ELAPSED_TIMESTAMP_FORMATTER);
+  }
+
+  private makeTimestampFromHumanReal(timestampHuman: string): Timestamp {
+    // Remove trailing Z if present
+    timestampHuman = timestampHuman.replace('Z', '');
+
+    // Date.parse only considers up to millisecond precision,
+    // so only pass in YYYY-MM-DDThh:mm:ss
+    let nanos = 0n;
+    if (timestampHuman.includes('.')) {
+      const [datetime, ns] = timestampHuman.split('.');
+      nanos += BigInt(Math.floor(Number(ns.padEnd(9, '0'))));
+      timestampHuman = datetime;
+    }
+
+    timestampHuman += this.utcOffset.format().slice(3);
+
+    return this.makeTimestampFromRealNs(
+      BigInt(Date.parse(timestampHuman)) * BigInt(TIME_UNIT_TO_NANO['ms']) +
+        BigInt(nanos),
+    );
+  }
+
+  private makeTimestampfromHumanElapsed(timestampHuman: string): Timestamp {
+    const usedUnits = timestampHuman.split(/[0-9]+/).filter((it) => it !== '');
+    const usedValues = timestampHuman
+      .split(/[a-z]+/)
+      .filter((it) => it !== '')
+      .map((it) => Math.floor(Number(it)));
+
+    let ns = BigInt(0);
+
+    for (let i = 0; i < usedUnits.length; i++) {
+      const unit = usedUnits[i];
+      const value = usedValues[i];
+      const unitData = TIME_UNITS.find((it) => it.unit === unit)!;
+      ns += BigInt(unitData.nanosInUnit) * BigInt(value);
+    }
+
+    return this.makeElapsedTimestamp(ns);
+  }
+
+  private addTimezoneOffset(timezone: string, timestampNs: bigint): bigint {
+    const utcDate = new Date(Number(timestampNs / 1000000n));
+    const timezoneDateFormatted = utcDate.toLocaleString('en-US', {
+      timeZone: timezone,
+    });
+    const timezoneDate = new Date(timezoneDateFormatted);
+
+    let daysDiff = timezoneDate.getDay() - utcDate.getDay(); // day of the week
+    if (daysDiff > 1) {
+      // Saturday in timezone, Sunday in UTC
+      daysDiff = -1;
+    } else if (daysDiff < -1) {
+      // Sunday in timezone, Saturday in UTC
+      daysDiff = 1;
+    }
+
+    const hoursDiff =
+      timezoneDate.getHours() - utcDate.getHours() + daysDiff * 24;
+    const minutesDiff = timezoneDate.getMinutes() - utcDate.getMinutes();
+    const localTimezoneOffsetMinutes = utcDate.getTimezoneOffset();
+
+    return (
+      timestampNs +
+      BigInt(hoursDiff * 3.6e12) +
+      BigInt(minutesDiff * 6e10) -
+      BigInt(localTimezoneOffsetMinutes * 6e10)
+    );
+  }
+}
+
+export const UTC_TIMEZONE_INFO = {
+  timezone: 'UTC',
+  locale: 'en-US',
+  utcOffsetMs: 0,
+};
diff --git a/tools/winscope/src/common/timestamp_converter_test.ts b/tools/winscope/src/common/timestamp_converter_test.ts
new file mode 100644
index 0000000..b4c607b
--- /dev/null
+++ b/tools/winscope/src/common/timestamp_converter_test.ts
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {UnitTestUtils} from 'test/unit/utils';
+import {TimestampConverter} from './timestamp_converter';
+import {TIME_UNIT_TO_NANO} from './time_units';
+
+describe('TimestampConverter', () => {
+  const testElapsedNs = 100n;
+  const testRealNs = 1659243341051481088n; // Sun, 31 Jul 2022 04:55:41 GMT to test timestamp conversion between different days
+  const testMonotonicTimeOffsetNs = 500n;
+  const testRealToBootTimeOffsetNs = 1000n;
+
+  const MILLISECOND = BigInt(TIME_UNIT_TO_NANO.ms);
+  const SECOND = BigInt(TIME_UNIT_TO_NANO.s);
+  const MINUTE = BigInt(TIME_UNIT_TO_NANO.m);
+  const HOUR = BigInt(TIME_UNIT_TO_NANO.h);
+  const DAY = BigInt(TIME_UNIT_TO_NANO.d);
+
+  beforeAll(() => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
+  });
+
+  describe('makes timestamps from ns without timezone info', () => {
+    const converterWithMonotonicOffset = new TimestampConverter(
+      TimestampConverterUtils.UTC_TIMEZONE_INFO,
+    );
+    converterWithMonotonicOffset.setRealToMonotonicTimeOffsetNs(
+      testMonotonicTimeOffsetNs,
+    );
+
+    const converterWithBootTimeOffset = new TimestampConverter(
+      TimestampConverterUtils.UTC_TIMEZONE_INFO,
+    );
+    converterWithBootTimeOffset.setRealToBootTimeOffsetNs(
+      testRealToBootTimeOffsetNs,
+    );
+
+    it('can create real-formatted timestamp without offset set', () => {
+      const timestamp = new TimestampConverter(
+        TimestampConverterUtils.UTC_TIMEZONE_INFO,
+      ).makeTimestampFromRealNs(testRealNs);
+      expect(timestamp.getValueNs()).toBe(testRealNs);
+      expect(timestamp.format()).toEqual('2022-07-31T04:55:41.051481088');
+    });
+
+    it('can create real-formatted timestamp with real to monotonic offset', () => {
+      const timestamp =
+        converterWithMonotonicOffset.makeTimestampFromMonotonicNs(testRealNs);
+      expect(timestamp.getValueNs()).toBe(
+        testRealNs + testMonotonicTimeOffsetNs,
+      );
+      expect(timestamp.format()).toEqual('2022-07-31T04:55:41.051481588');
+    });
+
+    it('can create real-formatted timestamp with real to boot time offset', () => {
+      const timestamp =
+        converterWithBootTimeOffset.makeTimestampFromBootTimeNs(testRealNs);
+      expect(timestamp.getValueNs()).toBe(
+        testRealNs + testRealToBootTimeOffsetNs,
+      );
+      expect(timestamp.format()).toEqual('2022-07-31T04:55:41.051482088');
+    });
+
+    it('can create elapsed-formatted timestamp', () => {
+      const timestamp = new TimestampConverter(
+        TimestampConverterUtils.UTC_TIMEZONE_INFO,
+      ).makeTimestampFromMonotonicNs(testElapsedNs);
+      expect(timestamp.getValueNs()).toBe(testElapsedNs);
+      expect(timestamp.format()).toEqual('100ns');
+    });
+
+    it('formats real-formatted timestamp with offset correctly', () => {
+      expect(
+        converterWithMonotonicOffset
+          .makeTimestampFromMonotonicNs(100n)
+          .format(),
+      ).toEqual('1970-01-01T00:00:00.000000600');
+      expect(
+        converterWithMonotonicOffset
+          .makeTimestampFromRealNs(100n * MILLISECOND)
+          .format(true),
+      ).toEqual('1970-01-01T00:00:00.100');
+    });
+  });
+
+  describe('makes timestamps from ns with timezone info', () => {
+    const converterWithMonotonicOffset = new TimestampConverter(
+      TimestampConverterUtils.ASIA_TIMEZONE_INFO,
+    );
+    converterWithMonotonicOffset.setRealToMonotonicTimeOffsetNs(
+      testMonotonicTimeOffsetNs,
+    );
+
+    const converterWithBootTimeOffset = new TimestampConverter(
+      TimestampConverterUtils.ASIA_TIMEZONE_INFO,
+    );
+    converterWithBootTimeOffset.setRealToBootTimeOffsetNs(
+      testRealToBootTimeOffsetNs,
+    );
+
+    it('can create real-formatted timestamp without offset set', () => {
+      const timestamp = new TimestampConverter(
+        TimestampConverterUtils.ASIA_TIMEZONE_INFO,
+      ).makeTimestampFromRealNs(testRealNs);
+      expect(timestamp.getValueNs()).toBe(testRealNs);
+      expect(timestamp.format()).toEqual('2022-07-31T10:25:41.051481088');
+    });
+
+    it('can create real-formatted timestamp with monotonic offset', () => {
+      const timestamp =
+        converterWithMonotonicOffset.makeTimestampFromMonotonicNs(testRealNs);
+      expect(timestamp.getValueNs()).toBe(
+        testRealNs + testMonotonicTimeOffsetNs,
+      );
+      expect(timestamp.format()).toEqual('2022-07-31T10:25:41.051481588');
+    });
+
+    it('can create real-formatted timestamp with real to boot time offset', () => {
+      const timestamp =
+        converterWithBootTimeOffset.makeTimestampFromBootTimeNs(testRealNs);
+      expect(timestamp.getValueNs()).toBe(
+        testRealNs + testRealToBootTimeOffsetNs,
+      );
+      expect(timestamp.format()).toEqual('2022-07-31T10:25:41.051482088');
+    });
+
+    it('can create elapsed-formatted timestamp', () => {
+      const timestamp = new TimestampConverter(
+        TimestampConverterUtils.ASIA_TIMEZONE_INFO,
+      ).makeTimestampFromMonotonicNs(testElapsedNs);
+      expect(timestamp.getValueNs()).toBe(testElapsedNs);
+      expect(timestamp.format()).toEqual('100ns');
+    });
+
+    describe('adds correct offset for different timezones', () => {
+      it('creates correct real-formatted timestamps for different timezones', () => {
+        expect(
+          new TimestampConverter(
+            {
+              timezone: 'Europe/London',
+              locale: 'en-US',
+              utcOffsetMs: 3600000,
+            },
+            0n,
+          )
+            .makeTimestampFromRealNs(testRealNs)
+            .format(),
+        ).toEqual('2022-07-31T05:55:41.051481088');
+        expect(
+          new TimestampConverter(
+            {
+              timezone: 'Europe/Zurich',
+              locale: 'en-US',
+              utcOffsetMs: 7200000,
+            },
+            0n,
+          )
+            .makeTimestampFromRealNs(testRealNs)
+            .format(),
+        ).toEqual('2022-07-31T06:55:41.051481088');
+        expect(
+          new TimestampConverter(
+            {
+              timezone: 'America/Los_Angeles',
+              locale: 'en-US',
+              utcOffsetMs: -25200000,
+            },
+            0n,
+          )
+            .makeTimestampFromRealNs(testRealNs)
+            .format(),
+        ).toEqual('2022-07-30T21:55:41.051481088');
+        expect(
+          new TimestampConverter(
+            {
+              timezone: 'Asia/Kolkata',
+              locale: 'en-US',
+              utcOffsetMs: 19800000,
+            },
+            0n,
+          )
+            .makeTimestampFromRealNs(testRealNs)
+            .format(),
+        ).toEqual('2022-07-31T10:25:41.051481088');
+      });
+    });
+  });
+
+  describe('makes timestamps from string without timezone info', () => {
+    const converterWithoutOffsets = new TimestampConverter(
+      TimestampConverterUtils.UTC_TIMEZONE_INFO,
+    );
+
+    const converterWithMonotonicOffset = new TimestampConverter(
+      TimestampConverterUtils.UTC_TIMEZONE_INFO,
+    );
+    converterWithMonotonicOffset.setRealToMonotonicTimeOffsetNs(
+      testMonotonicTimeOffsetNs,
+    );
+
+    it('makeTimestampfromHumanElapsed', () => {
+      expect(converterWithoutOffsets.makeTimestampFromHuman('0ns')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(0n),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1000ns')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(1000n),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('0ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(0n),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(MILLISECOND),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('10ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(10n * MILLISECOND),
+      );
+
+      expect(converterWithoutOffsets.makeTimestampFromHuman('999ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(
+          999n * MILLISECOND,
+        ),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1s')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(SECOND),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1s0ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(SECOND),
+      );
+      expect(
+        converterWithoutOffsets.makeTimestampFromHuman('1s0ms0ns'),
+      ).toEqual(converterWithoutOffsets.makeTimestampFromMonotonicNs(SECOND));
+      expect(
+        converterWithoutOffsets.makeTimestampFromHuman('1s0ms1ns'),
+      ).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(SECOND + 1n),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('0d1s1ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(
+          SECOND + MILLISECOND,
+        ),
+      );
+
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1m0s0ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(MINUTE),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1m1s1ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(
+          MINUTE + SECOND + MILLISECOND,
+        ),
+      );
+
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1h0m')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(HOUR),
+      );
+      expect(
+        converterWithoutOffsets.makeTimestampFromHuman('1h1m1s1ms'),
+      ).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(
+          HOUR + MINUTE + SECOND + MILLISECOND,
+        ),
+      );
+
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1d0s1ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(DAY + MILLISECOND),
+      );
+      expect(
+        converterWithoutOffsets.makeTimestampFromHuman('1d1h1m1s1ms'),
+      ).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(
+          DAY + HOUR + MINUTE + SECOND + MILLISECOND,
+        ),
+      );
+
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1d')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(DAY),
+      );
+      expect(converterWithoutOffsets.makeTimestampFromHuman('1d1ms')).toEqual(
+        converterWithoutOffsets.makeTimestampFromMonotonicNs(DAY + MILLISECOND),
+      );
+    });
+
+    it('makeTimestampfromHumanElapsed throws on invalid input format', () => {
+      const invalidFormatError = new Error('Invalid timestamp format');
+      expect(() =>
+        converterWithoutOffsets.makeTimestampFromHuman('1d1h1m1s0ns1ms'),
+      ).toThrow(invalidFormatError);
+      expect(() =>
+        converterWithoutOffsets.makeTimestampFromHuman('1dns'),
+      ).toThrow(invalidFormatError);
+      expect(() =>
+        converterWithoutOffsets.makeTimestampFromHuman('100'),
+      ).toThrow(invalidFormatError);
+      expect(() => converterWithoutOffsets.makeTimestampFromHuman('')).toThrow(
+        invalidFormatError,
+      );
+    });
+
+    it('makeTimestampFromHumanReal', () => {
+      const NOV_10_2022 = 1668038400000n * MILLISECOND;
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T22:04:54.186123212',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 + 22n * HOUR + 4n * MINUTE + 54n * SECOND + 186123212n,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T22:04:54.186123212Z',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 +
+            22n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            186n * MILLISECOND +
+            123212n,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T22:04:54.186000212',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 +
+            22n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            186n * MILLISECOND +
+            212n,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T22:04:54.006000002',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 + 22n * HOUR + 4n * MINUTE + 54n * SECOND + 6000002n,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54.006000002',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND + 6000002n,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54.0',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54.0100',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 +
+            6n * HOUR +
+            4n * MINUTE +
+            54n * SECOND +
+            10n * MILLISECOND,
+        ),
+      );
+      expect(
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54.0175328',
+        ),
+      ).toEqual(
+        converterWithMonotonicOffset.makeTimestampFromRealNs(
+          NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND + 17532800n,
+        ),
+      );
+    });
+
+    it('makeTimestampFromHumanReal throws on invalid input format', () => {
+      const invalidFormatError = new Error('Invalid timestamp format');
+      expect(() =>
+        converterWithMonotonicOffset.makeTimestampFromHuman('100'),
+      ).toThrow(invalidFormatError);
+      expect(() =>
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '06h4m54s, 10 Nov 2022',
+        ),
+      ).toThrow(invalidFormatError);
+      expect(() =>
+        converterWithMonotonicOffset.makeTimestampFromHuman(''),
+      ).toThrow(invalidFormatError);
+      expect(() =>
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54.',
+        ),
+      ).toThrow(invalidFormatError);
+      expect(() =>
+        converterWithMonotonicOffset.makeTimestampFromHuman(
+          '2022-11-10T06:04:54.1234567890',
+        ),
+      ).toThrow(invalidFormatError);
+    });
+
+    it('can reverse-date format', () => {
+      expect(
+        converterWithMonotonicOffset
+          .makeTimestampFromHuman('2022-11-10T22:04:54.186123212')
+          .format(),
+      ).toEqual('2022-11-10T22:04:54.186123212');
+    });
+  });
+
+  describe('makes timestamps from string with timezone info', () => {
+    const converter = new TimestampConverter(
+      TimestampConverterUtils.ASIA_TIMEZONE_INFO,
+    );
+    converter.setRealToMonotonicTimeOffsetNs(testMonotonicTimeOffsetNs);
+
+    it('makeTimestampFromHumanReal', () => {
+      const NOV_10_2022 = 1668038400000n * MILLISECOND;
+      testMakeTimestampFromHumanReal(
+        '2022-11-11T03:34:54.186123212',
+        NOV_10_2022 +
+          22n * HOUR +
+          4n * MINUTE +
+          54n * SECOND +
+          186n * MILLISECOND +
+          123212n,
+        '2022-11-11T03:34:54.186123212',
+      );
+
+      testMakeTimestampFromHumanReal(
+        '2022-11-11T03:34:54.186123212Z',
+        NOV_10_2022 +
+          22n * HOUR +
+          4n * MINUTE +
+          54n * SECOND +
+          186n * MILLISECOND +
+          123212n,
+        '2022-11-11T03:34:54.186123212',
+      );
+
+      testMakeTimestampFromHumanReal(
+        '2022-11-10T11:34:54',
+        NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND,
+        '2022-11-10T11:34:54.000000000',
+      );
+
+      testMakeTimestampFromHumanReal(
+        '2022-11-10T11:34:54.0',
+        NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND,
+        '2022-11-10T11:34:54.000000000',
+      );
+
+      testMakeTimestampFromHumanReal(
+        '2022-11-10T11:34:54.0100',
+        NOV_10_2022 +
+          6n * HOUR +
+          4n * MINUTE +
+          54n * SECOND +
+          10n * MILLISECOND,
+        '2022-11-10T11:34:54.010000000',
+      );
+
+      testMakeTimestampFromHumanReal(
+        '2022-11-10T11:34:54.0175328',
+        NOV_10_2022 + 6n * HOUR + 4n * MINUTE + 54n * SECOND + 17532800n,
+        '2022-11-10T11:34:54.017532800',
+      );
+    });
+
+    it('can reverse-date format', () => {
+      expect(
+        converter
+          .makeTimestampFromHuman('2022-11-11T03:34:54.186123212')
+          .format(),
+      ).toEqual('2022-11-11T03:34:54.186123212');
+    });
+
+    function testMakeTimestampFromHumanReal(
+      timestampHuman: string,
+      expectedNs: bigint,
+      expectedFormattedTimestamp: string,
+    ) {
+      const timestamp = converter.makeTimestampFromHuman(timestampHuman);
+      expect(timestamp.getValueNs()).toEqual(expectedNs);
+      expect(timestamp.format()).toEqual(expectedFormattedTimestamp);
+    }
+  });
+});
diff --git a/tools/winscope/src/common/timestamp_factory.ts b/tools/winscope/src/common/timestamp_factory.ts
deleted file mode 100644
index c267502..0000000
--- a/tools/winscope/src/common/timestamp_factory.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {Timestamp, TimestampType, TimezoneInfo} from './time';
-
-export class TimestampFactory {
-  constructor(
-    public timezoneInfo: TimezoneInfo = {timezone: 'UTC', locale: 'en-US'},
-  ) {}
-
-  makeRealTimestamp(
-    valueNs: bigint,
-    realToElapsedTimeOffsetNs?: bigint,
-  ): Timestamp {
-    const valueWithRealtimeOffset = valueNs + (realToElapsedTimeOffsetNs ?? 0n);
-    const localNs =
-      this.timezoneInfo.timezone !== 'UTC'
-        ? this.addTimezoneOffset(
-            this.timezoneInfo.timezone,
-            valueWithRealtimeOffset,
-          )
-        : valueWithRealtimeOffset;
-    return new Timestamp(
-      TimestampType.REAL,
-      localNs,
-      localNs - valueWithRealtimeOffset,
-    );
-  }
-
-  makeElapsedTimestamp(valueNs: bigint): Timestamp {
-    return new Timestamp(TimestampType.ELAPSED, valueNs);
-  }
-
-  canMakeTimestampFromType(
-    type: TimestampType,
-    realToElapsedTimeOffsetNs: bigint | undefined,
-  ) {
-    return (
-      type === TimestampType.ELAPSED ||
-      (type === TimestampType.REAL && realToElapsedTimeOffsetNs !== undefined)
-    );
-  }
-
-  makeTimestampFromType(
-    type: TimestampType,
-    valueNs: bigint,
-    realToElapsedTimeOffsetNs?: bigint,
-  ): Timestamp {
-    switch (type) {
-      case TimestampType.REAL:
-        if (realToElapsedTimeOffsetNs === undefined) {
-          throw new Error(
-            "realToElapsedTimeOffsetNs can't be undefined to use real timestamp",
-          );
-        }
-        return this.makeRealTimestamp(valueNs, realToElapsedTimeOffsetNs);
-      case TimestampType.ELAPSED:
-        return this.makeElapsedTimestamp(valueNs);
-      default:
-        throw new Error('Unhandled timestamp type');
-    }
-  }
-
-  private addTimezoneOffset(timezone: string, timestampNs: bigint): bigint {
-    const utcDate = new Date(Number(timestampNs / 1000000n));
-    const timezoneDateFormatted = utcDate.toLocaleString('en-US', {
-      timeZone: timezone,
-    });
-    const timezoneDate = new Date(timezoneDateFormatted);
-
-    let daysDiff = timezoneDate.getDay() - utcDate.getDay(); // day of the week
-    if (daysDiff > 1) {
-      // Saturday in timezone, Sunday in UTC
-      daysDiff = -1;
-    } else if (daysDiff < -1) {
-      // Sunday in timezone, Saturday in UTC
-      daysDiff = 1;
-    }
-
-    const hoursDiff =
-      timezoneDate.getHours() - utcDate.getHours() + daysDiff * 24;
-    const minutesDiff = timezoneDate.getMinutes() - utcDate.getMinutes();
-    const localTimezoneOffsetMinutes = utcDate.getTimezoneOffset();
-
-    return (
-      timestampNs +
-      BigInt(hoursDiff * 3.6e12) +
-      BigInt(minutesDiff * 6e10) -
-      BigInt(localTimezoneOffsetMinutes * 6e10)
-    );
-  }
-}
-
-export const NO_TIMEZONE_OFFSET_FACTORY = new TimestampFactory();
diff --git a/tools/winscope/src/common/timestamp_factory_test.ts b/tools/winscope/src/common/timestamp_factory_test.ts
deleted file mode 100644
index 8b2850c..0000000
--- a/tools/winscope/src/common/timestamp_factory_test.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {TimestampType} from './time';
-import {TimestampFactory} from './timestamp_factory';
-
-describe('TimestampFactory', () => {
-  const testElapsedNs = 100n;
-  const testRealNs = 1659243341051481088n; // Sun, 31 Jul 2022 04:55:41 GMT to test timestamp conversion between different days
-  const testRealToElapsedOffsetNs = 500n;
-
-  describe('without timezone info', () => {
-    const factory = new TimestampFactory();
-    it('can create real timestamp', () => {
-      const timestamp = factory.makeRealTimestamp(testRealNs);
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(testRealNs);
-    });
-
-    it('can create real timestamp with offset', () => {
-      const timestamp = factory.makeRealTimestamp(
-        testRealNs,
-        testRealToElapsedOffsetNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(
-        testRealNs + testRealToElapsedOffsetNs,
-      );
-    });
-
-    it('can create elapsed timestamp', () => {
-      const timestamp = factory.makeElapsedTimestamp(testElapsedNs);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(testElapsedNs);
-    });
-
-    it('can create real timestamp from type', () => {
-      const timestamp = factory.makeTimestampFromType(
-        TimestampType.REAL,
-        testRealNs,
-        testRealToElapsedOffsetNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(
-        testRealNs + testRealToElapsedOffsetNs,
-      );
-    });
-
-    it('can create elapsed timestamp from type', () => {
-      const timestamp = factory.makeTimestampFromType(
-        TimestampType.ELAPSED,
-        testElapsedNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(testElapsedNs);
-    });
-
-    it('can create elapsed timestamp from type ignoring offset', () => {
-      const timestamp = factory.makeTimestampFromType(
-        TimestampType.ELAPSED,
-        testElapsedNs,
-        testRealToElapsedOffsetNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(testElapsedNs);
-    });
-
-    it('throws error if creating real timestamp from type without offset', () => {
-      expect(() =>
-        factory.makeTimestampFromType(TimestampType.REAL, testRealNs),
-      ).toThrow();
-    });
-  });
-
-  describe('with timezone info', () => {
-    const factory = new TimestampFactory({
-      timezone: 'Asia/Kolkata',
-      locale: 'en-US',
-    });
-    const expectedUtcOffsetNs = 19800000000000n;
-
-    it('can create real timestamp', () => {
-      const timestamp = factory.makeRealTimestamp(testRealNs);
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(testRealNs + expectedUtcOffsetNs);
-      expect(timestamp.toUTC().getValueNs()).toBe(testRealNs);
-    });
-
-    it('can create real timestamp with offset', () => {
-      const timestamp = factory.makeRealTimestamp(
-        testRealNs,
-        testRealToElapsedOffsetNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(
-        testRealNs + testRealToElapsedOffsetNs + expectedUtcOffsetNs,
-      );
-      expect(timestamp.toUTC().getValueNs()).toBe(
-        testRealNs + testRealToElapsedOffsetNs,
-      );
-    });
-
-    it('can create elapsed timestamp', () => {
-      const timestamp = factory.makeElapsedTimestamp(testElapsedNs);
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(testElapsedNs);
-      expect(timestamp.toUTC().getValueNs()).toBe(testElapsedNs);
-    });
-
-    it('can create real timestamp from type', () => {
-      const timestamp = factory.makeTimestampFromType(
-        TimestampType.REAL,
-        testRealNs,
-        testRealToElapsedOffsetNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.REAL);
-      expect(timestamp.getValueNs()).toBe(
-        testRealNs + testRealToElapsedOffsetNs + expectedUtcOffsetNs,
-      );
-      expect(timestamp.toUTC().getValueNs()).toBe(
-        testRealNs + testRealToElapsedOffsetNs,
-      );
-    });
-
-    it('can create elapsed timestamp from type', () => {
-      const timestamp = factory.makeTimestampFromType(
-        TimestampType.ELAPSED,
-        testElapsedNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(testElapsedNs);
-      expect(timestamp.toUTC().getValueNs()).toBe(testElapsedNs);
-    });
-
-    it('can create elapsed timestamp from type ignoring offset', () => {
-      const timestamp = factory.makeTimestampFromType(
-        TimestampType.ELAPSED,
-        testElapsedNs,
-        testRealToElapsedOffsetNs,
-      );
-      expect(timestamp.getType()).toBe(TimestampType.ELAPSED);
-      expect(timestamp.getValueNs()).toBe(testElapsedNs);
-      expect(timestamp.toUTC().getValueNs()).toBe(testElapsedNs);
-    });
-
-    it('throws error if creating real timestamp from type without offset', () => {
-      expect(() =>
-        factory.makeTimestampFromType(TimestampType.REAL, testRealNs),
-      ).toThrow();
-    });
-  });
-
-  describe('adds correct offset for different timezones', () => {
-    it('creates correct real timestamps for different timezones', () => {
-      expect(
-        new TimestampFactory({timezone: 'Europe/London', locale: 'en-US'})
-          .makeRealTimestamp(testRealNs)
-          .getValueNs(),
-      ).toEqual(testRealNs + BigInt(1 * 3.6e12));
-      expect(
-        new TimestampFactory({timezone: 'Europe/Zurich', locale: 'en-US'})
-          .makeRealTimestamp(testRealNs)
-          .getValueNs(),
-      ).toEqual(testRealNs + BigInt(2 * 3.6e12));
-      expect(
-        new TimestampFactory({timezone: 'America/Los_Angeles', locale: 'en-US'})
-          .makeRealTimestamp(testRealNs)
-          .getValueNs(),
-      ).toEqual(testRealNs - BigInt(7 * 3.6e12));
-      expect(
-        new TimestampFactory({timezone: 'Asia/Kolkata', locale: 'en-US'})
-          .makeRealTimestamp(testRealNs)
-          .getValueNs(),
-      ).toEqual(testRealNs + BigInt(5.5 * 3.6e12));
-    });
-
-    it('throws error for invalid timezone', () => {
-      expect(() =>
-        new TimestampFactory({
-          timezone: 'Invalid/Timezone',
-          locale: 'en-US',
-        }).makeRealTimestamp(testRealNs),
-      ).toThrow();
-    });
-  });
-});
diff --git a/tools/winscope/src/common/timestamp_utils.ts b/tools/winscope/src/common/timestamp_utils.ts
index c4f19ae..f41d16e 100644
--- a/tools/winscope/src/common/timestamp_utils.ts
+++ b/tools/winscope/src/common/timestamp_utils.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from './timestamp_factory';
+import {Timestamp} from 'common/time';
+import {TIME_UNITS} from './time_units';
 
 export class TimestampUtils {
   // (?=.) checks there is at least one character with a lookahead match
@@ -25,106 +25,11 @@
     /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])(\.[0-9]{1,9})?Z?$/;
   static readonly NS_TIMESTAMP_REGEX = /^\s*[0-9]+(\s?ns)?\s*$/;
 
-  static TO_NANO = {
-    ns: 1,
-    ms: 1000000,
-    s: 1000000 * 1000,
-    m: 1000000 * 1000 * 60,
-    h: 1000000 * 1000 * 60 * 60,
-    d: 1000000 * 1000 * 60 * 60 * 24,
-  };
-
-  static units = [
-    {nanosInUnit: TimestampUtils.TO_NANO['ns'], unit: 'ns'},
-    {nanosInUnit: TimestampUtils.TO_NANO['ms'], unit: 'ms'},
-    {nanosInUnit: TimestampUtils.TO_NANO['s'], unit: 's'},
-    {nanosInUnit: TimestampUtils.TO_NANO['m'], unit: 'm'},
-    {nanosInUnit: TimestampUtils.TO_NANO['h'], unit: 'h'},
-    {nanosInUnit: TimestampUtils.TO_NANO['d'], unit: 'd'},
-  ];
-
   static compareFn(a: Timestamp, b: Timestamp): number {
-    if (a.getType() !== b.getType()) {
-      throw new Error(
-        'Attempted to compare two timestamps with different type',
-      );
-    }
     return Number(a.getValueNs() - b.getValueNs());
   }
 
-  static format(timestamp: Timestamp, hideNs = false): string {
-    switch (timestamp.getType()) {
-      case TimestampType.ELAPSED: {
-        return TimestampUtils.nanosecondsToHumanElapsed(
-          timestamp.getValueNs(),
-          hideNs,
-        );
-      }
-      case TimestampType.REAL: {
-        return TimestampUtils.nanosecondsToHumanReal(
-          timestamp.getValueNs(),
-          hideNs,
-        );
-      }
-      default: {
-        throw Error('Unhandled timestamp type');
-      }
-    }
-  }
-
-  static parseHumanElapsed(timestampHuman: string): Timestamp {
-    if (!TimestampUtils.HUMAN_ELAPSED_TIMESTAMP_REGEX.test(timestampHuman)) {
-      throw Error('Invalid elapsed timestamp format');
-    }
-
-    const units = TimestampUtils.units;
-
-    const usedUnits = timestampHuman.split(/[0-9]+/).filter((it) => it !== '');
-    const usedValues = timestampHuman
-      .split(/[a-z]+/)
-      .filter((it) => it !== '')
-      .map((it) => Math.floor(Number(it)));
-
-    let ns = BigInt(0);
-
-    for (let i = 0; i < usedUnits.length; i++) {
-      const unit = usedUnits[i];
-      const value = usedValues[i];
-      const unitData = units.find((it) => it.unit === unit)!;
-      ns += BigInt(unitData.nanosInUnit) * BigInt(value);
-    }
-
-    return NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(ns);
-  }
-
-  static parseHumanReal(timestampHuman: string): Timestamp {
-    if (!TimestampUtils.HUMAN_REAL_TIMESTAMP_REGEX.test(timestampHuman)) {
-      throw Error('Invalid real timestamp format');
-    }
-
-    // Add trailing Z if it isn't there yet
-    if (timestampHuman[timestampHuman.length - 1] !== 'Z') {
-      timestampHuman += 'Z';
-    }
-
-    // Date.parse only considers up to millisecond precision
-    let nanoSeconds = 0;
-    if (timestampHuman.includes('.')) {
-      const milliseconds = timestampHuman.split('.')[1].replace('Z', '');
-      nanoSeconds = Math.floor(Number(milliseconds.padEnd(9, '0').slice(3)));
-    }
-
-    return NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(
-      BigInt(Date.parse(timestampHuman)) *
-        BigInt(TimestampUtils.TO_NANO['ms']) +
-        BigInt(nanoSeconds),
-    );
-  }
-
   static min(ts1: Timestamp, ts2: Timestamp): Timestamp {
-    if (ts1.getType() !== ts2.getType()) {
-      throw new Error("Can't compare timestamps of different types");
-    }
     if (ts2.getValueNs() < ts1.getValueNs()) {
       return ts2;
     }
@@ -133,9 +38,6 @@
   }
 
   static max(ts1: Timestamp, ts2: Timestamp): Timestamp {
-    if (ts1.getType() !== ts2.getType()) {
-      throw new Error("Can't compare timestamps of different types");
-    }
     if (ts2.getValueNs() > ts1.getValueNs()) {
       return ts2;
     }
@@ -143,16 +45,9 @@
     return ts1;
   }
 
-  private static nanosecondsToHumanElapsed(
-    timestampNanos: number | bigint,
-    hideNs = true,
-  ): string {
-    timestampNanos = BigInt(timestampNanos);
-    const units = TimestampUtils.units;
-
+  static formatElapsedNs(timestampNanos: bigint, hideNs = false): string {
     let leftNanos = timestampNanos;
-    const parts: Array<{value: bigint; unit: string}> = units
-      .slice()
+    const parts: Array<{value: bigint; unit: string}> = TIME_UNITS.slice()
       .reverse()
       .map(({nanosInUnit, unit}) => {
         let amountOfUnit = BigInt(0);
@@ -174,22 +69,4 @@
 
     return parts.map((part) => `${part.value}${part.unit}`).join('');
   }
-
-  private static nanosecondsToHumanReal(
-    timestampNanos: number | bigint,
-    hideNs = true,
-  ): string {
-    timestampNanos = BigInt(timestampNanos);
-    const ms = timestampNanos / 1000000n;
-    const extraNanos = timestampNanos % 1000000n;
-    const formattedTimestamp = new Date(Number(ms))
-      .toISOString()
-      .replace('Z', '');
-
-    if (hideNs) {
-      return formattedTimestamp;
-    } else {
-      return `${formattedTimestamp}${extraNanos.toString().padStart(6, '0')}`;
-    }
-  }
 }
diff --git a/tools/winscope/src/common/timestamp_utils_test.ts b/tools/winscope/src/common/timestamp_utils_test.ts
index c7659ab..ae4178e 100644
--- a/tools/winscope/src/common/timestamp_utils_test.ts
+++ b/tools/winscope/src/common/timestamp_utils_test.ts
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from './timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {UnitTestUtils} from 'test/unit/utils';
 import {TimestampUtils} from './timestamp_utils';
 
 describe('TimestampUtils', () => {
@@ -22,436 +23,33 @@
   const SECOND = BigInt(1000) * MILLISECOND;
   const MINUTE = BigInt(60) * SECOND;
   const HOUR = BigInt(60) * MINUTE;
-  const DAY = BigInt(24) * HOUR;
+
+  beforeAll(() => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
+  });
 
   describe('compareFn', () => {
-    it('throws if timestamps have different type', () => {
-      const real = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-      const elapsed = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n);
-
-      expect(() => {
-        TimestampUtils.compareFn(real, elapsed);
-      }).toThrow();
-    });
-
     it('allows to sort arrays', () => {
       const array = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n),
+        TimestampConverterUtils.makeRealTimestamp(100n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(12n),
+        TimestampConverterUtils.makeRealTimestamp(110n),
+        TimestampConverterUtils.makeRealTimestamp(11n),
       ];
       array.sort(TimestampUtils.compareFn);
 
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(110n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(11n),
+        TimestampConverterUtils.makeRealTimestamp(12n),
+        TimestampConverterUtils.makeRealTimestamp(100n),
+        TimestampConverterUtils.makeRealTimestamp(110n),
       ];
       expect(array).toEqual(expected);
     });
   });
 
-  it('nanosecondsToHuman', () => {
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-        true,
-      ),
-    ).toEqual('0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-        false,
-      ),
-    ).toEqual('0ns');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1000n),
-        true,
-      ),
-    ).toEqual('0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1000n),
-        false,
-      ),
-    ).toEqual('1000ns');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(MILLISECOND - 1n),
-        true,
-      ),
-    ).toEqual('0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(MILLISECOND),
-        true,
-      ),
-    ).toEqual('1ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n * MILLISECOND),
-        true,
-      ),
-    ).toEqual('10ms');
-
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND - 1n),
-        true,
-      ),
-    ).toEqual('999ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND),
-        true,
-      ),
-    ).toEqual('1s0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND + MILLISECOND),
-        true,
-      ),
-    ).toEqual('1s1ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND + MILLISECOND),
-        false,
-      ),
-    ).toEqual('1s1ms0ns');
-
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(MINUTE - 1n),
-        true,
-      ),
-    ).toEqual('59s999ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(MINUTE),
-        true,
-      ),
-    ).toEqual('1m0s0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-          MINUTE + SECOND + MILLISECOND,
-        ),
-        true,
-      ),
-    ).toEqual('1m1s1ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-          MINUTE + SECOND + MILLISECOND + 1n,
-        ),
-        true,
-      ),
-    ).toEqual('1m1s1ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-          MINUTE + SECOND + MILLISECOND + 1n,
-        ),
-        false,
-      ),
-    ).toEqual('1m1s1ms1ns');
-
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(HOUR - 1n),
-        true,
-      ),
-    ).toEqual('59m59s999ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(HOUR - 1n),
-        false,
-      ),
-    ).toEqual('59m59s999ms999999ns');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(HOUR),
-        true,
-      ),
-    ).toEqual('1h0m0s0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-          HOUR + MINUTE + SECOND + MILLISECOND,
-        ),
-        true,
-      ),
-    ).toEqual('1h1m1s1ms');
-
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(DAY - 1n),
-        true,
-      ),
-    ).toEqual('23h59m59s999ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(DAY),
-        true,
-      ),
-    ).toEqual('1d0h0m0s0ms');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-          DAY + HOUR + MINUTE + SECOND + MILLISECOND,
-        ),
-        true,
-      ),
-    ).toEqual('1d1h1m1s1ms');
-  });
-
-  it('humanElapsedToNanoseconds', () => {
-    expect(TimestampUtils.parseHumanElapsed('0ns')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1000ns')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1000n),
-    );
-    expect(TimestampUtils.parseHumanElapsed('0ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(MILLISECOND),
-    );
-    expect(TimestampUtils.parseHumanElapsed('10ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n * MILLISECOND),
-    );
-
-    expect(TimestampUtils.parseHumanElapsed('999ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(999n * MILLISECOND),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1s')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1s0ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1s0ms0ns')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1s0ms1ns')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND + 1n),
-    );
-    expect(TimestampUtils.parseHumanElapsed('0d1s1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(SECOND + MILLISECOND),
-    );
-
-    expect(TimestampUtils.parseHumanElapsed('1m0s0ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(MINUTE),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1m1s1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-        MINUTE + SECOND + MILLISECOND,
-      ),
-    );
-
-    expect(TimestampUtils.parseHumanElapsed('1h0m')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(HOUR),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1h1m1s1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-        HOUR + MINUTE + SECOND + MILLISECOND,
-      ),
-    );
-
-    expect(TimestampUtils.parseHumanElapsed('1d0s1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(DAY + MILLISECOND),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1d1h1m1s1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-        DAY + HOUR + MINUTE + SECOND + MILLISECOND,
-      ),
-    );
-
-    expect(TimestampUtils.parseHumanElapsed('1d')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(DAY),
-    );
-    expect(TimestampUtils.parseHumanElapsed('1d1ms')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(DAY + MILLISECOND),
-    );
-  });
-
-  it('humanToNanoseconds throws on invalid input format', () => {
-    const invalidFormatError = new Error('Invalid elapsed timestamp format');
-    expect(() => TimestampUtils.parseHumanElapsed('1d1h1m1s0ns1ms')).toThrow(
-      invalidFormatError,
-    );
-    expect(() => TimestampUtils.parseHumanElapsed('1dns')).toThrow(
-      invalidFormatError,
-    );
-    expect(() => TimestampUtils.parseHumanElapsed('100')).toThrow(
-      invalidFormatError,
-    );
-    expect(() => TimestampUtils.parseHumanElapsed('')).toThrow(
-      invalidFormatError,
-    );
-  });
-
-  it('nanosecondsToHumanReal', () => {
-    const NOV_10_2022 = 1668038400000n * MILLISECOND;
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n),
-        true,
-      ),
-    ).toEqual('1970-01-01T00:00:00.000');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(
-          NOV_10_2022 +
-            22n * HOUR +
-            4n * MINUTE +
-            54n * SECOND +
-            186n * MILLISECOND +
-            123212n,
-        ),
-        true,
-      ),
-    ).toEqual('2022-11-10T22:04:54.186');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(NOV_10_2022),
-        true,
-      ),
-    ).toEqual('2022-11-10T00:00:00.000');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(NOV_10_2022 + 1n),
-        true,
-      ),
-    ).toEqual('2022-11-10T00:00:00.000');
-
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n),
-        false,
-      ),
-    ).toEqual('1970-01-01T00:00:00.000000000');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(
-          NOV_10_2022 +
-            22n * HOUR +
-            4n * MINUTE +
-            54n * SECOND +
-            186n * MILLISECOND +
-            123212n,
-        ),
-        false,
-      ),
-    ).toEqual('2022-11-10T22:04:54.186123212');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(NOV_10_2022),
-        false,
-      ),
-    ).toEqual('2022-11-10T00:00:00.000000000');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(NOV_10_2022 + 1n),
-        false,
-      ),
-    ).toEqual('2022-11-10T00:00:00.000000001');
-  });
-
-  it('humanRealToNanoseconds', () => {
-    const NOV_10_2022 = 1668038400000n * MILLISECOND;
-    expect(
-      TimestampUtils.parseHumanReal('2022-11-10T22:04:54.186123212'),
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(
-        NOV_10_2022 +
-          22n * HOUR +
-          4n * MINUTE +
-          54n * SECOND +
-          186n * MILLISECOND +
-          123212n,
-      ),
-    );
-    expect(
-      TimestampUtils.parseHumanReal('2022-11-10T22:04:54.186123212Z'),
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668117894186123212n),
-    );
-    expect(
-      TimestampUtils.parseHumanReal('2022-11-10T22:04:54.186000212'),
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668117894186000212n),
-    );
-    expect(
-      TimestampUtils.parseHumanReal('2022-11-10T22:04:54.006000002'),
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668117894006000002n),
-    );
-    expect(
-      TimestampUtils.parseHumanReal('2022-11-10T06:04:54.006000002'),
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668060294006000002n),
-    );
-    expect(TimestampUtils.parseHumanReal('2022-11-10T06:04:54')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668060294000000000n),
-    );
-    expect(TimestampUtils.parseHumanReal('2022-11-10T06:04:54.0')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668060294000000000n),
-    );
-    expect(TimestampUtils.parseHumanReal('2022-11-10T06:04:54.0100')).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668060294010000000n),
-    );
-    expect(
-      TimestampUtils.parseHumanReal('2022-11-10T06:04:54.0175328'),
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668060294017532800n),
-    );
-  });
-
-  it('canReverseDateFormatting', () => {
-    let timestamp =
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1668117894186123212n);
-    expect(
-      TimestampUtils.parseHumanReal(TimestampUtils.format(timestamp)),
-    ).toEqual(timestamp);
-
-    timestamp = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-      DAY + HOUR + MINUTE + SECOND + MILLISECOND + 1n,
-    );
-    expect(
-      TimestampUtils.parseHumanElapsed(TimestampUtils.format(timestamp)),
-    ).toEqual(timestamp);
-  });
-
-  it('humanToNanoseconds throws on invalid input format', () => {
-    const invalidFormatError = new Error('Invalid real timestamp format');
-    expect(() => TimestampUtils.parseHumanReal('23h59m59s999ms5ns')).toThrow(
-      invalidFormatError,
-    );
-    expect(() => TimestampUtils.parseHumanReal('1d')).toThrow(
-      invalidFormatError,
-    );
-    expect(() => TimestampUtils.parseHumanReal('100')).toThrow(
-      invalidFormatError,
-    );
-    expect(() =>
-      TimestampUtils.parseHumanReal('06h4m54s, 10 Nov 2022'),
-    ).toThrow(invalidFormatError);
-    expect(() => TimestampUtils.parseHumanReal('')).toThrow(invalidFormatError);
-    expect(() => TimestampUtils.parseHumanReal('2022-11-10T06:04:54.')).toThrow(
-      invalidFormatError,
-    );
-    expect(() =>
-      TimestampUtils.parseHumanReal('2022-11-10T06:04:54.1234567890'),
-    ).toThrow(invalidFormatError);
-  });
-
   it('nano second regex accept all expected inputs', () => {
     expect(TimestampUtils.NS_TIMESTAMP_REGEX.test('123')).toBeTrue();
     expect(TimestampUtils.NS_TIMESTAMP_REGEX.test('123ns')).toBeTrue();
@@ -463,26 +61,4 @@
     expect(TimestampUtils.NS_TIMESTAMP_REGEX.test('a123 ns')).toBeFalse();
     expect(TimestampUtils.NS_TIMESTAMP_REGEX.test('')).toBeFalse();
   });
-
-  it('format real', () => {
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n, 500n),
-      ),
-    ).toEqual('1970-01-01T00:00:00.000000600');
-    expect(
-      TimestampUtils.format(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(100n * MILLISECOND, 500n),
-        true,
-      ),
-    ).toEqual('1970-01-01T00:00:00.100');
-  });
-
-  it('format elapsed', () => {
-    const timestamp = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-      100n * MILLISECOND,
-    );
-    expect(TimestampUtils.format(timestamp, true)).toEqual('100ms');
-    expect(TimestampUtils.format(timestamp)).toEqual('100ms0ns');
-  });
 });
diff --git a/tools/winscope/src/common/utc_offset.ts b/tools/winscope/src/common/utc_offset.ts
new file mode 100644
index 0000000..4a903c3
--- /dev/null
+++ b/tools/winscope/src/common/utc_offset.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TIME_UNIT_TO_NANO} from './time_units';
+
+export class UTCOffset {
+  private valueNs: bigint | undefined;
+
+  getValueNs(): bigint | undefined {
+    return this.valueNs;
+  }
+
+  format(): string {
+    if (this.valueNs === undefined) {
+      return 'UTC+00:00';
+    }
+    const valueHours = Number(this.valueNs / BigInt(TIME_UNIT_TO_NANO.m)) / 60;
+    const valueHoursAbs = Math.abs(valueHours);
+    const hh = Math.floor(valueHoursAbs);
+    const mm = (valueHoursAbs - hh) * 60;
+    const timeDiff = `${hh}`.padStart(2, '0') + ':' + `${mm}`.padStart(2, '0');
+    return `UTC${this.valueNs < 0 ? '-' : '+'}${timeDiff}`;
+  }
+
+  initialize(valueNs: bigint) {
+    if (valueNs > BigInt(14 * TIME_UNIT_TO_NANO.h)) {
+      console.warn('Failed to set timezone offset greater than UTC+14:00');
+      return;
+    }
+    if (valueNs < BigInt(-12 * TIME_UNIT_TO_NANO.h)) {
+      console.warn('Failed to set timezone offset greater than UTC-12:00');
+      return;
+    }
+    this.valueNs = valueNs;
+  }
+}
diff --git a/tools/winscope/src/common/utc_offset_test.ts b/tools/winscope/src/common/utc_offset_test.ts
new file mode 100644
index 0000000..568a719
--- /dev/null
+++ b/tools/winscope/src/common/utc_offset_test.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {TIME_UNIT_TO_NANO} from './time_units';
+import {UTCOffset} from './utc_offset';
+
+describe('UTCOffset', () => {
+  const utcOffset = new UTCOffset();
+
+  it('sets positive offset for whole single-digit number hours', () => {
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * 2));
+    expect(utcOffset.format()).toEqual('UTC+02:00');
+  });
+
+  it('sets positive offset for whole double-digit number hours', () => {
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * 11));
+    expect(utcOffset.format()).toEqual('UTC+11:00');
+  });
+
+  it('sets positive offset for fractional hours', () => {
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * 5.5));
+    expect(utcOffset.format()).toEqual('UTC+05:30');
+  });
+
+  it('sets negative offset for whole single-digit number hours', () => {
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * -8));
+    expect(utcOffset.format()).toEqual('UTC-08:00');
+  });
+
+  it('sets negative offset for whole double-digit number hours', () => {
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * -10));
+    expect(utcOffset.format()).toEqual('UTC-10:00');
+  });
+
+  it('sets negative offset for fractional hours', () => {
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * -4.5));
+    expect(utcOffset.format()).toEqual('UTC-04:30');
+  });
+
+  it('does not set offset for invalid value', () => {
+    const utcOffset = new UTCOffset();
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * 15)); // later than UTC+14:00
+    expect(utcOffset.getValueNs()).toBeUndefined();
+    utcOffset.initialize(BigInt(TIME_UNIT_TO_NANO.h * -13)); // earlier than UTC-12:00
+    expect(utcOffset.getValueNs()).toBeUndefined();
+  });
+});
diff --git a/tools/winscope/src/cross_tool/cross_tool_protocol.ts b/tools/winscope/src/cross_tool/cross_tool_protocol.ts
index 3e48e4d..d27bbce 100644
--- a/tools/winscope/src/cross_tool/cross_tool_protocol.ts
+++ b/tools/winscope/src/cross_tool/cross_tool_protocol.ts
@@ -15,7 +15,7 @@
  */
 
 import {FunctionUtils} from 'common/function_utils';
-import {TimestampType} from 'common/time';
+import {RemoteToolTimestampConverter} from 'common/timestamp_converter';
 import {
   RemoteToolFilesReceived,
   RemoteToolTimestampReceived,
@@ -46,6 +46,7 @@
 {
   private remoteTool?: RemoteTool;
   private emitEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
+  private timestampConverter: RemoteToolTimestampConverter | undefined;
 
   constructor() {
     window.addEventListener('message', async (event) => {
@@ -57,6 +58,10 @@
     this.emitEvent = callback;
   }
 
+  setTimestampConverter(converter: RemoteToolTimestampConverter | undefined) {
+    this.timestampConverter = converter;
+  }
+
   async onWinscopeEvent(event: WinscopeEvent) {
     await event.visit(
       WinscopeEventType.TRACE_POSITION_UPDATE,
@@ -65,13 +70,11 @@
           return;
         }
 
-        const timestamp = event.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()}.`,
+        const timestamp =
+          this.timestampConverter?.tryMakeTimestampForRemoteTool(
+            event.position.timestamp,
           );
+        if (timestamp === undefined) {
           return;
         }
 
diff --git a/tools/winscope/src/messaging/user_warnings.ts b/tools/winscope/src/messaging/user_warnings.ts
index 8f2cdac..59b8410 100644
--- a/tools/winscope/src/messaging/user_warnings.ts
+++ b/tools/winscope/src/messaging/user_warnings.ts
@@ -15,8 +15,7 @@
  */
 
 import {TimeRange} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
-import {TimestampUtils} from 'common/timestamp_utils';
+import {TimeDuration} from 'common/time_duration';
 import {TraceType} from 'trace/trace_type';
 import {UserWarning} from './user_warning';
 
@@ -34,16 +33,6 @@
   }
 }
 
-export class NoCommonTimestampType extends UserWarning {
-  getDescriptor(): string {
-    return 'no common timestamp';
-  }
-
-  getMessage(): string {
-    return 'Failed to load traces because no common timestamp type could be found';
-  }
-}
-
 export class NoInputFiles extends UserWarning {
   getDescriptor(): string {
     return 'no input';
@@ -57,7 +46,7 @@
 export class TraceHasOldData extends UserWarning {
   constructor(
     private readonly descriptor: string,
-    private readonly timeGap: TimeRange,
+    private readonly timeGap?: TimeRange,
   ) {
     super();
   }
@@ -67,15 +56,15 @@
   }
 
   getMessage(): string {
-    const elapsedTime = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-      this.timeGap.to.getValueNs() - this.timeGap.from.getValueNs(),
+    const elapsedTime = this.timeGap
+      ? new TimeDuration(
+          this.timeGap.to.getValueNs() - this.timeGap.from.getValueNs(),
+        )
+      : undefined;
+    return (
+      `${this.descriptor}: discarded because data is old` +
+      (this.timeGap ? `der than ${elapsedTime?.format(true)}` : '')
     );
-    return `${
-      this.descriptor
-    }: discarded because data is older than ${TimestampUtils.format(
-      elapsedTime,
-      true,
-    )}`;
   }
 }
 
diff --git a/tools/winscope/src/parsers/events/parser_eventlog.ts b/tools/winscope/src/parsers/events/parser_eventlog.ts
index 7f18979..6e5923e 100644
--- a/tools/winscope/src/parsers/events/parser_eventlog.ts
+++ b/tools/winscope/src/parsers/events/parser_eventlog.ts
@@ -15,7 +15,7 @@
  */
 
 import {StringUtils} from 'common/string_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {TraceType} from 'trace/trace_type';
 import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
@@ -35,6 +35,14 @@
     return ParserEventLog.MAGIC_NUMBER;
   }
 
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(buffer: Uint8Array): Event[] {
     const decodedLogs = this.decodeByteArray(buffer);
     const events = this.parseLogs(decodedLogs);
@@ -43,21 +51,13 @@
     });
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    entry: Event,
-  ): undefined | Timestamp {
-    if (type === TimestampType.REAL) {
-      return this.timestampFactory.makeRealTimestamp(entry.eventTimestamp);
-    }
-    return undefined;
+  protected override getTimestamp(entry: Event): Timestamp {
+    return this.timestampConverter.makeTimestampFromRealNs(
+      entry.eventTimestamp,
+    );
   }
 
-  override processDecodedEntry(
-    index: number,
-    timestampType: TimestampType,
-    entry: Event,
-  ): PropertyTreeNode {
+  override processDecodedEntry(index: number, entry: Event): PropertyTreeNode {
     return new PropertyTreeBuilderFromProto()
       .setData(entry)
       .setRootId('EventLogTrace')
diff --git a/tools/winscope/src/parsers/events/parser_eventlog_test.ts b/tools/winscope/src/parsers/events/parser_eventlog_test.ts
index 6e14bf3..04e2f01 100644
--- a/tools/winscope/src/parsers/events/parser_eventlog_test.ts
+++ b/tools/winscope/src/parsers/events/parser_eventlog_test.ts
@@ -15,9 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -45,26 +44,20 @@
     });
 
     it('has expected timestamps', () => {
-      const timestamps = assertDefined(
-        parser.getTimestamps(TimestampType.REAL),
-      );
+      const timestamps = assertDefined(parser.getTimestamps());
 
       expect(timestamps.length).toEqual(184);
 
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207047981157174n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207047991161039n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207047991310494n),
+        TimestampConverterUtils.makeRealTimestamp(1681207047981157174n),
+        TimestampConverterUtils.makeRealTimestamp(1681207047991161039n),
+        TimestampConverterUtils.makeRealTimestamp(1681207047991310494n),
       ];
       expect(timestamps.slice(0, 3)).toEqual(expected);
     });
 
-    it("doesn't provide elapsed timestamps", () => {
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(undefined);
-    });
-
     it('contains parsed jank CUJ events', async () => {
-      const entry = await parser.getEntry(18, TimestampType.REAL);
+      const entry = await parser.getEntry(18);
 
       const expected = new PropertyTreeBuilder()
         .setRootId('EventLogTrace')
@@ -85,27 +78,6 @@
 
       expect(entry).toEqual(expected);
     });
-
-    it('applies timezone info to real timestamps', async () => {
-      const parserWithTimezoneInfo = assertDefined(
-        await UnitTestUtils.getParser('traces/eventlog.winscope', true),
-      ) as Parser<PropertyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.EVENT_LOG,
-      );
-
-      const timestamps = assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      );
-      expect(timestamps.length).toEqual(184);
-
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681226847981157174n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681226847991161039n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681226847991310494n),
-      ];
-      expect(timestamps.slice(0, 3)).toEqual(expected);
-    });
   });
 
   describe('trace with timestamps not monotonically increasing', () => {
@@ -121,22 +93,20 @@
     });
 
     it('sorts entries to make timestamps monotonically increasing', () => {
-      const timestamps = assertDefined(
-        parser.getTimestamps(TimestampType.REAL),
-      );
+      const timestamps = assertDefined(parser.getTimestamps());
 
       expect(timestamps.length).toEqual(3);
 
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207047981157174n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207047991161039n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207047991310494n),
+        TimestampConverterUtils.makeRealTimestamp(1681207047981157174n),
+        TimestampConverterUtils.makeRealTimestamp(1681207047991161039n),
+        TimestampConverterUtils.makeRealTimestamp(1681207047991310494n),
       ];
       expect(timestamps).toEqual(expected);
     });
 
     it('contains parsed events', async () => {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
 
       const expected = new PropertyTreeBuilder()
         .setRootId('EventLogTrace')
@@ -154,29 +124,5 @@
 
       expect(entry).toEqual(expected);
     });
-
-    it('applies timezone info to real timestamps', async () => {
-      const parserWithTimezoneInfo = assertDefined(
-        await UnitTestUtils.getParser(
-          'traces/eventlog_timestamps_not_monotonically_increasing.winscope',
-          true,
-        ),
-      ) as Parser<PropertyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.EVENT_LOG,
-      );
-
-      const timestamps = assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      );
-      expect(timestamps.length).toEqual(3);
-
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681226847981157174n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681226847991161039n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681226847991310494n),
-      ];
-      expect(timestamps).toEqual(expected);
-    });
   });
 });
diff --git a/tools/winscope/src/parsers/events/traces_parser_cujs.ts b/tools/winscope/src/parsers/events/traces_parser_cujs.ts
index d25809c..677595d 100644
--- a/tools/winscope/src/parsers/events/traces_parser_cujs.ts
+++ b/tools/winscope/src/parsers/events/traces_parser_cujs.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {AbstractTracesParser} from 'parsers/legacy/abstract_traces_parser';
 import {Trace} from 'trace/trace';
 import {Traces} from 'trace/traces';
@@ -33,8 +33,8 @@
   private readonly descriptors: string[];
   private decodedEntries: PropertyTreeNode[] | undefined;
 
-  constructor(traces: Traces) {
-    super();
+  constructor(traces: Traces, timestampConverter: ParserTimestampConverter) {
+    super(timestampConverter);
 
     const eventlogTrace = traces.getTrace(TraceType.EVENT_LOG);
     if (eventlogTrace !== undefined) {
@@ -63,17 +63,14 @@
       );
     });
     this.decodedEntries = this.makeCujsFromEvents(cujEvents);
-    await this.parseTimestamps();
+    await this.createTimestamps();
   }
 
   getLengthEntries(): number {
     return assertDefined(this.decodedEntries).length;
   }
 
-  getEntry(
-    index: number,
-    timestampType: TimestampType,
-  ): Promise<PropertyTreeNode> {
+  getEntry(index: number): Promise<PropertyTreeNode> {
     const entry = assertDefined(this.decodedEntries)[index];
     return Promise.resolve(entry);
   }
@@ -86,22 +83,18 @@
     return TraceType.CUJS;
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    cuj: PropertyTreeNode,
-  ): undefined | Timestamp {
+  protected override getTimestamp(cuj: PropertyTreeNode): Timestamp {
     const cujTimestamp = assertDefined(cuj.getChildByName('startTimestamp'));
-    if (type === TimestampType.ELAPSED) {
-      const elapsedNanos = assertDefined(
-        cujTimestamp.getChildByName('elapsedNanos'),
-      ).getValue();
-      return NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(elapsedNanos);
-    } else if (type === TimestampType.REAL) {
-      const unixNanos = assertDefined(
-        cujTimestamp.getChildByName('unixNanos'),
-      ).getValue();
-      return NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(unixNanos);
-    }
+    return this.timestampConverter.makeTimestampFromRealNs(
+      assertDefined(cujTimestamp.getChildByName('unixNanos')).getValue(),
+    );
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
     return undefined;
   }
 
diff --git a/tools/winscope/src/parsers/events/traces_parser_cujs_test.ts b/tools/winscope/src/parsers/events/traces_parser_cujs_test.ts
index 11d489f..724bde4 100644
--- a/tools/winscope/src/parsers/events/traces_parser_cujs_test.ts
+++ b/tools/winscope/src/parsers/events/traces_parser_cujs_test.ts
@@ -15,9 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {TraceType} from 'trace/trace_type';
@@ -28,6 +27,7 @@
   let parser: Parser<PropertyTreeNode>;
 
   beforeAll(async () => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
     parser = (await UnitTestUtils.getTracesParser([
       'traces/eventlog.winscope',
     ])) as Parser<PropertyTreeNode>;
@@ -37,37 +37,20 @@
     expect(parser.getTraceType()).toEqual(TraceType.CUJS);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
-
-    expect(timestamps.length).toEqual(16);
-
+  it('provides timestamps', () => {
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2661012770462n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2661012874914n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2661012903966n),
-    ];
-    expect(timestamps.slice(0, 3)).toEqual(expected);
-  });
-
-  it('provides real timestamps', () => {
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207048025446000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207048025551000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1681207048025580000n),
+      TimestampConverterUtils.makeRealTimestamp(1681207048025446000n),
+      TimestampConverterUtils.makeRealTimestamp(1681207048025551000n),
+      TimestampConverterUtils.makeRealTimestamp(1681207048025580000n),
     ];
 
-    const timestamps = parser.getTimestamps(TimestampType.REAL)!;
-
+    const timestamps = assertDefined(parser.getTimestamps());
     expect(timestamps.length).toEqual(16);
-
     expect(timestamps.slice(0, 3)).toEqual(expected);
   });
 
   it('contains parsed CUJ events', async () => {
-    const entry = await parser.getEntry(2, TimestampType.REAL);
+    const entry = await parser.getEntry(2);
 
     const expected = new PropertyTreeBuilder()
       .setRootId('CujTrace')
diff --git a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients.ts b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients.ts
index 49f515e..1544b73 100644
--- a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients.ts
+++ b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {HierarchyTreeBuilderInputMethod} from 'parsers/input_method/hierarchy_tree_builder_input_method';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {AddDefaults} from 'parsers/operations/add_defaults';
@@ -86,7 +86,7 @@
     ),
   };
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToBootTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.INPUT_METHOD_CLIENTS;
@@ -96,6 +96,14 @@
     return ParserInputMethodClients.MAGIC_NUMBER;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): android.view.inputmethod.IInputMethodClientsTraceProto[] {
@@ -106,35 +114,20 @@
     const timeOffset = BigInt(
       decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToBootTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
     return decoded.entry ?? [];
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: android.view.inputmethod.IInputMethodClientsTraceProto,
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(
-      assertDefined(entry.elapsedRealtimeNanos).toString(),
+  ): Timestamp {
+    return this.timestampConverter.makeTimestampFromBootTimeNs(
+      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
-    if (
-      this.timestampFactory.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return this.timestampFactory.makeTimestampFromType(
-        type,
-        elapsedRealtimeNanos,
-        this.realToElapsedTimeOffsetNs,
-      );
-    }
-    return undefined;
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: android.view.inputmethod.IInputMethodClientsTraceProto,
   ): HierarchyTreeNode {
     if (
diff --git a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients_test.ts b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients_test.ts
index fb51b8e..7f32568 100644
--- a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients_test.ts
+++ b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_clients_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -23,7 +23,7 @@
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 
 describe('ParserInputMethodClients', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  describe('trace with real timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -41,70 +41,25 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
-      expect(parser.getTimestamps(TimestampType.ELAPSED)!.length).toEqual(13);
-
+    it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15613638434n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15647516364n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15677650967n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090215405395n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090249283325n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090279417928n),
       ];
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3),
-      ).toEqual(expected);
-    });
-
-    it('provides real timestamps', () => {
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090215405395n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090249283325n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090279417928n),
-      ];
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3),
-      ).toEqual(expected);
+      expect(assertDefined(parser.getTimestamps()).slice(0, 3)).toEqual(
+        expected,
+      );
     });
 
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(1, TimestampType.REAL);
+      const entry = await parser.getEntry(1);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('InputMethodClients entry');
     });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_and_real_timestamp/InputMethodClients.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.INPUT_METHOD_CLIENTS,
-      );
-
-      const expectedElapsed = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15613638434n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15647516364n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15677650967n),
-      ];
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        ).slice(0, 3),
-      ).toEqual(expectedElapsed);
-
-      const expectedReal = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126890215405395n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126890249283325n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126890279417928n),
-      ];
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        ).slice(0, 3),
-      ).toEqual(expectedReal);
-    });
   });
 
-  describe('trace with elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -118,40 +73,16 @@
       expect(parser.getTraceType()).toEqual(TraceType.INPUT_METHOD_CLIENTS);
     });
 
-    it('provides elapsed timestamps', () => {
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED))[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1149083651642n),
+    it('provides timestamps', () => {
+      expect(assertDefined(parser.getTimestamps())[0]).toEqual(
+        TimestampConverterUtils.makeElapsedTimestamp(1149083651642n),
       );
     });
 
-    it("doesn't provide real timestamps", () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toBeUndefined();
-    });
-
-    it('retrieves trace entry from elapsed timestamp', async () => {
-      const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+    it('retrieves trace entry from timestamp', async () => {
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('InputMethodClients entry');
     });
-
-    it('does not apply timezone info to elapsed timestamps', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_timestamp/InputMethodClients.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.INPUT_METHOD_CLIENTS,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1149083651642n),
-      );
-    });
   });
 });
diff --git a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service.ts b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service.ts
index 2daa57f..51d73c9 100644
--- a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service.ts
+++ b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {HierarchyTreeBuilderInputMethod} from 'parsers/input_method/hierarchy_tree_builder_input_method';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {AddDefaults} from 'parsers/operations/add_defaults';
@@ -92,7 +92,7 @@
     ),
   };
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToBootTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.INPUT_METHOD_MANAGER_SERVICE;
@@ -102,6 +102,14 @@
     return ParserInputMethodManagerService.MAGIC_NUMBER;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): android.view.inputmethod.IInputMethodManagerServiceTraceProto[] {
@@ -112,35 +120,20 @@
     const timeOffset = BigInt(
       decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToBootTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
     return decoded.entry ?? [];
   }
 
   protected override getTimestamp(
-    type: TimestampType,
     entry: android.view.inputmethod.IInputMethodManagerServiceTraceProto,
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(
-      assertDefined(entry.elapsedRealtimeNanos).toString(),
+  ): Timestamp {
+    return this.timestampConverter.makeTimestampFromBootTimeNs(
+      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
-    if (
-      this.timestampFactory.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return this.timestampFactory.makeTimestampFromType(
-        type,
-        elapsedRealtimeNanos,
-        this.realToElapsedTimeOffsetNs,
-      );
-    }
-    return undefined;
   }
 
   protected override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: android.view.inputmethod.IInputMethodManagerServiceTraceProto,
   ): HierarchyTreeNode {
     if (
diff --git a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service_test.ts b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service_test.ts
index 7e767b9..d430198 100644
--- a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service_test.ts
+++ b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_manager_service_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -23,7 +22,7 @@
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 
 describe('ParserInputMethodManagerService', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  describe('trace with real timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -42,46 +41,20 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual([
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15963782518n),
-      ]);
-    });
-
-    it('provides real timestamps', () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual([
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090565549479n),
+    it('provides timestamps', () => {
+      expect(parser.getTimestamps()).toEqual([
+        TimestampConverterUtils.makeRealTimestamp(1659107090565549479n),
       ]);
     });
 
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('InputMethodManagerService entry');
     });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_and_real_timestamp/InputMethodManagerService.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.INPUT_METHOD_MANAGER_SERVICE,
-      );
-
-      expect(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      ).toEqual([
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15963782518n),
-      ]);
-
-      expect(parserWithTimezoneInfo.getTimestamps(TimestampType.REAL)).toEqual([
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126890565549479n),
-      ]);
-    });
   });
 
-  describe('trace with elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -96,40 +69,16 @@
       );
     });
 
-    it('provides elapsed timestamps', () => {
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED))[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1149226290110n),
+    it('provides timestamps', () => {
+      expect(assertDefined(parser.getTimestamps())[0]).toEqual(
+        TimestampConverterUtils.makeElapsedTimestamp(1149226290110n),
       );
     });
 
-    it("doesn't provide real timestamps", () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
-    });
-
-    it('retrieves trace entry from elapsed timestamp', async () => {
-      const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+    it('retrieves trace entry from timestamp', async () => {
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('InputMethodManagerService entry');
     });
-
-    it('does not apply timezone info to elapsed timestamps', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_timestamp/InputMethodManagerService.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.INPUT_METHOD_MANAGER_SERVICE,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1149226290110n),
-      );
-    });
   });
 });
diff --git a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service.ts b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service.ts
index 7d7c415..631d4a1 100644
--- a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service.ts
+++ b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {HierarchyTreeBuilderInputMethod} from 'parsers/input_method/hierarchy_tree_builder_input_method';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {AddDefaults} from 'parsers/operations/add_defaults';
@@ -86,7 +86,7 @@
     ),
   };
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToBootTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.INPUT_METHOD_SERVICE;
@@ -96,6 +96,14 @@
     return ParserInputMethodService.MAGIC_NUMBER;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): android.view.inputmethod.IInputMethodServiceTraceProto[] {
@@ -106,35 +114,20 @@
     const timeOffset = BigInt(
       decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToBootTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
     return decoded.entry ?? [];
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: android.view.inputmethod.IInputMethodServiceTraceProto,
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(
-      assertDefined(entry.elapsedRealtimeNanos).toString(),
+  ): Timestamp {
+    return this.timestampConverter.makeTimestampFromBootTimeNs(
+      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
-    if (
-      this.timestampFactory.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return this.timestampFactory.makeTimestampFromType(
-        type,
-        elapsedRealtimeNanos,
-        this.realToElapsedTimeOffsetNs,
-      );
-    }
-    return undefined;
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: android.view.inputmethod.IInputMethodServiceTraceProto,
   ): HierarchyTreeNode {
     if (
diff --git a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service_test.ts b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service_test.ts
index 5a6ad7a..e8b3991 100644
--- a/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service_test.ts
+++ b/tools/winscope/src/parsers/input_method/legacy/parser_input_method_service_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -23,7 +22,7 @@
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 
 describe('ParserInputMethodService', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  describe('trace with real timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -36,52 +35,26 @@
     it('has expected trace type', () => {
       expect(parser.getTraceType()).toEqual(TraceType.INPUT_METHOD_SERVICE);
     });
+
     it('has expected coarse version', () => {
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
+    it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(16578752896n),
+        TimestampConverterUtils.makeRealTimestamp(1659107091180519857n),
       ];
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(expected);
-    });
-
-    it('provides real timestamps', () => {
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107091180519857n),
-      ];
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(expected);
+      expect(parser.getTimestamps()).toEqual(expected);
     });
 
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('InputMethodService entry');
     });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_and_real_timestamp/InputMethodService.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.INPUT_METHOD_SERVICE,
-      );
-
-      expect(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      ).toEqual([
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(16578752896n),
-      ]);
-
-      expect(parserWithTimezoneInfo.getTimestamps(TimestampType.REAL)).toEqual([
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126891180519857n),
-      ]);
-    });
   });
 
-  describe('trace with elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -94,40 +67,16 @@
       expect(parser.getTraceType()).toEqual(TraceType.INPUT_METHOD_SERVICE);
     });
 
-    it('provides elapsed timestamps', () => {
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED))[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1149230019887n),
+    it('provides timestamps', () => {
+      expect(assertDefined(parser.getTimestamps())[0]).toEqual(
+        TimestampConverterUtils.makeElapsedTimestamp(1149230019887n),
       );
     });
 
-    it("doesn't provide real timestamps", () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
-    });
-
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('InputMethodService entry');
     });
-
-    it('does not apply timezone info to elapsed timestamps', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_timestamp/InputMethodService.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.INPUT_METHOD_SERVICE,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1149230019887n),
-      );
-    });
   });
 });
diff --git a/tools/winscope/src/parsers/legacy/abstract_parser.ts b/tools/winscope/src/parsers/legacy/abstract_parser.ts
index 7daa776..cf14d45 100644
--- a/tools/winscope/src/parsers/legacy/abstract_parser.ts
+++ b/tools/winscope/src/parsers/legacy/abstract_parser.ts
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {CoarseVersion} from 'trace/coarse_version';
 import {
   CustomQueryParamTypeMap,
@@ -31,26 +31,19 @@
 export abstract class AbstractParser<T extends object = object>
   implements Parser<T>
 {
-  private timestamps = new Map<TimestampType, Timestamp[]>();
+  private timestamps: Timestamp[] | undefined;
   protected traceFile: TraceFile;
   protected decodedEntries: any[] = [];
-  protected timestampFactory: TimestampFactory;
+  protected timestampConverter: ParserTimestampConverter;
 
   protected abstract getMagicNumber(): undefined | number[];
   protected abstract decodeTrace(trace: Uint8Array): any[];
-  protected abstract getTimestamp(
-    type: TimestampType,
-    decodedEntry: any,
-  ): undefined | Timestamp;
-  protected abstract processDecodedEntry(
-    index: number,
-    timestampType: TimestampType,
-    decodedEntry: any,
-  ): any;
+  protected abstract getTimestamp(decodedEntry: any): Timestamp;
+  protected abstract processDecodedEntry(index: number, decodedEntry: any): any;
 
-  constructor(trace: TraceFile, timestampFactory: TimestampFactory) {
+  constructor(trace: TraceFile, timestampConverter: ParserTimestampConverter) {
     this.traceFile = trace;
-    this.timestampFactory = timestampFactory;
+    this.timestampConverter = timestampConverter;
   }
 
   async parse() {
@@ -60,7 +53,6 @@
       this.getMagicNumber(),
     );
     this.decodedEntries = this.decodeTrace(traceBuffer);
-    this.timestamps = this.decodeTimestamps();
   }
 
   getDescriptors(): string[] {
@@ -71,23 +63,20 @@
     return this.decodedEntries.length;
   }
 
-  getTimestamps(type: TimestampType): undefined | Timestamp[] {
-    return this.timestamps.get(type);
+  createTimestamps() {
+    this.timestamps = this.decodeTimestamps();
+  }
+
+  getTimestamps(): undefined | Timestamp[] {
+    return this.timestamps;
   }
 
   getCoarseVersion(): CoarseVersion {
     return CoarseVersion.LEGACY;
   }
 
-  getEntry(
-    index: AbsoluteEntryIndex,
-    timestampType: TimestampType,
-  ): Promise<T> {
-    const entry = this.processDecodedEntry(
-      index,
-      timestampType,
-      this.decodedEntries[index],
-    );
+  getEntry(index: AbsoluteEntryIndex): Promise<T> {
+    const entry = this.processDecodedEntry(index, this.decodedEntries[index]);
     return Promise.resolve(entry);
   }
 
@@ -99,27 +88,11 @@
     throw new Error('Not implemented');
   }
 
-  private decodeTimestamps(): Map<TimestampType, Timestamp[]> {
-    const timeStampMap = new Map<TimestampType, Timestamp[]>();
-    for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
-      const timestamps: Timestamp[] = [];
-      let areTimestampsValid = true;
-
-      for (const entry of this.decodedEntries) {
-        const timestamp = this.getTimestamp(type, entry);
-        if (timestamp === undefined) {
-          areTimestampsValid = false;
-          break;
-        }
-        timestamps.push(timestamp);
-      }
-
-      if (areTimestampsValid) {
-        timeStampMap.set(type, timestamps);
-      }
-    }
-    return timeStampMap;
+  private decodeTimestamps(): Timestamp[] {
+    return this.decodedEntries.map((entry) => this.getTimestamp(entry));
   }
 
   abstract getTraceType(): TraceType;
+  abstract getRealToBootTimeOffsetNs(): bigint | undefined;
+  abstract getRealToMonotonicTimeOffsetNs(): bigint | undefined;
 }
diff --git a/tools/winscope/src/parsers/legacy/abstract_traces_parser.ts b/tools/winscope/src/parsers/legacy/abstract_traces_parser.ts
index ac02f5c..3194c89 100644
--- a/tools/winscope/src/parsers/legacy/abstract_traces_parser.ts
+++ b/tools/winscope/src/parsers/legacy/abstract_traces_parser.ts
@@ -14,7 +14,8 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {CoarseVersion} from 'trace/coarse_version';
 import {
   CustomQueryParserResultTypeMap,
@@ -25,18 +26,14 @@
 import {TraceType} from 'trace/trace_type';
 
 export abstract class AbstractTracesParser<T> implements Parser<T> {
-  private timestamps = new Map<TimestampType, Timestamp[]>();
+  private timestamps: Timestamp[] | undefined;
+  protected timestampConverter: ParserTimestampConverter;
 
-  abstract parse(): Promise<void>;
+  protected abstract getTimestamp(decodedEntry: any): Timestamp;
 
-  abstract getDescriptors(): string[];
-
-  abstract getTraceType(): TraceType;
-
-  abstract getEntry(
-    index: AbsoluteEntryIndex,
-    timestampType: TimestampType,
-  ): Promise<T>;
+  constructor(timestampConverter: ParserTimestampConverter) {
+    this.timestampConverter = timestampConverter;
+  }
 
   getCoarseVersion(): CoarseVersion {
     return CoarseVersion.LEGACY;
@@ -49,35 +46,29 @@
     throw new Error('Not implemented');
   }
 
-  abstract getLengthEntries(): number;
-
-  getTimestamps(type: TimestampType): Timestamp[] | undefined {
-    return this.timestamps.get(type);
+  getTimestamps(): Timestamp[] | undefined {
+    return this.timestamps;
   }
 
-  protected async parseTimestamps() {
-    for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
-      const timestamps: Timestamp[] = [];
-      let areTimestampsValid = true;
+  async createTimestamps() {
+    this.timestamps = await this.decodeTimestamps();
+  }
 
-      for (let index = 0; index < this.getLengthEntries(); index++) {
-        const entry = await this.getEntry(index, type);
-        const timestamp = this.getTimestamp(type, entry);
-        if (timestamp === undefined) {
-          areTimestampsValid = false;
-          break;
-        }
-        timestamps.push(timestamp);
-      }
-
-      if (areTimestampsValid) {
-        this.timestamps.set(type, timestamps);
-      }
+  private async decodeTimestamps() {
+    const timestampsNs = [];
+    for (let index = 0; index < this.getLengthEntries(); index++) {
+      const entry = await this.getEntry(index);
+      const timestamp = this.getTimestamp(entry);
+      timestampsNs.push(timestamp);
     }
+    return timestampsNs;
   }
 
-  protected abstract getTimestamp(
-    type: TimestampType,
-    decodedEntry: any,
-  ): undefined | Timestamp;
+  abstract parse(): Promise<void>;
+  abstract getDescriptors(): string[];
+  abstract getTraceType(): TraceType;
+  abstract getEntry(index: AbsoluteEntryIndex): Promise<T>;
+  abstract getLengthEntries(): number;
+  abstract getRealToMonotonicTimeOffsetNs(): bigint | undefined;
+  abstract getRealToBootTimeOffsetNs(): bigint | undefined;
 }
diff --git a/tools/winscope/src/parsers/legacy/parser_common_test.ts b/tools/winscope/src/parsers/legacy/parser_common_test.ts
index 76213ec..48945db 100644
--- a/tools/winscope/src/parsers/legacy/parser_common_test.ts
+++ b/tools/winscope/src/parsers/legacy/parser_common_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
@@ -24,6 +23,10 @@
 import {ParserFactory} from './parser_factory';
 
 describe('Parser', () => {
+  beforeAll(() => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
+  });
+
   it('is robust to empty trace file', async () => {
     const trace = new TraceFile(
       await UnitTestUtils.getFixtureFile('traces/empty.pb'),
@@ -31,7 +34,7 @@
     );
     const parsers = await new ParserFactory().createParsers(
       [trace],
-      NO_TIMEZONE_OFFSET_FACTORY,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER,
     );
     expect(parsers.length).toEqual(0);
   });
@@ -42,8 +45,7 @@
     );
 
     expect(parser.getTraceType()).toEqual(TraceType.INPUT_METHOD_CLIENTS);
-    expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual([]);
-    expect(parser.getTimestamps(TimestampType.REAL)).toEqual([]);
+    expect(parser.getTimestamps()).toEqual([]);
   });
 
   describe('real timestamp', () => {
@@ -57,25 +59,22 @@
 
     it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089075566202n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089999048990n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090010194213n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089075566202n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089999048990n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090010194213n),
       ];
-      expect(parser.getTimestamps(TimestampType.REAL)!.slice(0, 3)).toEqual(
+      expect(assertDefined(parser.getTimestamps()).slice(0, 3)).toEqual(
         expected,
       );
     });
 
     it('retrieves trace entries', async () => {
-      let entry = await parser.getEntry(0, TimestampType.REAL);
+      let entry = await parser.getEntry(0);
       expect(
         assertDefined(entry.getEagerPropertyByName('focusedApp')).getValue(),
       ).toEqual('com.google.android.apps.nexuslauncher/.NexusLauncherActivity');
 
-      entry = await parser.getEntry(
-        parser.getLengthEntries() - 1,
-        TimestampType.REAL,
-      );
+      entry = await parser.getEntry(parser.getLengthEntries() - 1);
       expect(
         assertDefined(entry.getEagerPropertyByName('focusedApp')).getValue(),
       ).toEqual('com.google.android.apps.nexuslauncher/.NexusLauncherActivity');
@@ -93,23 +92,20 @@
 
     it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850254319343n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850763506110n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850782750048n),
+        TimestampConverterUtils.makeElapsedTimestamp(850254319343n),
+        TimestampConverterUtils.makeElapsedTimestamp(850763506110n),
+        TimestampConverterUtils.makeElapsedTimestamp(850782750048n),
       ];
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(expected);
+      expect(parser.getTimestamps()).toEqual(expected);
     });
 
     it('retrieves trace entries', async () => {
-      let entry = await parser.getEntry(0, TimestampType.ELAPSED);
+      let entry = await parser.getEntry(0);
       expect(
         assertDefined(entry.getEagerPropertyByName('focusedApp')).getValue(),
       ).toEqual('com.google.android.apps.nexuslauncher/.NexusLauncherActivity');
 
-      entry = await parser.getEntry(
-        parser.getLengthEntries() - 1,
-        TimestampType.ELAPSED,
-      );
+      entry = await parser.getEntry(parser.getLengthEntries() - 1);
       expect(
         assertDefined(entry.getEagerPropertyByName('focusedApp')).getValue(),
       ).toEqual('com.google.android.apps.nexuslauncher/.NexusLauncherActivity');
diff --git a/tools/winscope/src/parsers/legacy/parser_factory.ts b/tools/winscope/src/parsers/legacy/parser_factory.ts
index c9d1ca4..b829607 100644
--- a/tools/winscope/src/parsers/legacy/parser_factory.ts
+++ b/tools/winscope/src/parsers/legacy/parser_factory.ts
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {TimestampFactory} from 'common/timestamp_factory';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {ProgressListener} from 'messaging/progress_listener';
 import {UserNotificationsListener} from 'messaging/user_notifications_listener';
 import {UnsupportedFileFormat} from 'messaging/user_warnings';
@@ -57,7 +57,7 @@
 
   async createParsers(
     traceFiles: TraceFile[],
-    timestampFactory: TimestampFactory,
+    timestampConverter: ParserTimestampConverter,
     progressListener?: ProgressListener,
     notificationListener?: UserNotificationsListener,
   ): Promise<FileAndParser[]> {
@@ -73,7 +73,7 @@
 
       for (const ParserType of ParserFactory.PARSERS) {
         try {
-          const p = new ParserType(traceFile, timestampFactory);
+          const p = new ParserType(traceFile, timestampConverter);
           await p.parse();
           hasFoundParser = true;
 
diff --git a/tools/winscope/src/parsers/legacy/traces_parser_factory.ts b/tools/winscope/src/parsers/legacy/traces_parser_factory.ts
index 428e8bb..b8cca79 100644
--- a/tools/winscope/src/parsers/legacy/traces_parser_factory.ts
+++ b/tools/winscope/src/parsers/legacy/traces_parser_factory.ts
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {TracesParserCujs} from 'parsers/events/traces_parser_cujs';
 import {TracesParserTransitions} from 'parsers/transitions/legacy/traces_parser_transitions';
 import {Parser} from 'trace/parser';
@@ -22,12 +23,15 @@
 export class TracesParserFactory {
   static readonly PARSERS = [TracesParserCujs, TracesParserTransitions];
 
-  async createParsers(traces: Traces): Promise<Array<Parser<object>>> {
+  async createParsers(
+    traces: Traces,
+    timestampConverter: ParserTimestampConverter,
+  ): Promise<Array<Parser<object>>> {
     const parsers: Array<Parser<object>> = [];
 
     for (const ParserType of TracesParserFactory.PARSERS) {
       try {
-        const parser = new ParserType(traces);
+        const parser = new ParserType(traces, timestampConverter);
         await parser.parse();
         parsers.push(parser);
       } catch (error) {
diff --git a/tools/winscope/src/parsers/operations/transform_to_timestamp_test.ts b/tools/winscope/src/parsers/operations/transform_to_timestamp_test.ts
index dc6ddcd..c769ab1 100644
--- a/tools/winscope/src/parsers/operations/transform_to_timestamp_test.ts
+++ b/tools/winscope/src/parsers/operations/transform_to_timestamp_test.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {MockLong} from 'test/unit/mock_long';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {TransformToTimestamp} from './transform_to_timestamp';
 
@@ -46,7 +46,7 @@
     operation.apply(propertyRoot);
     expect(
       assertDefined(propertyRoot.getChildByName('timestamp')).getValue(),
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n));
+    ).toEqual(TimestampConverterUtils.makeRealTimestamp(10n));
     expect(
       assertDefined(propertyRoot.getChildByName('otherTimestamp')).getValue(),
     ).toEqual(longTimestamp);
@@ -60,17 +60,17 @@
     operation.apply(propertyRoot);
     expect(
       assertDefined(propertyRoot.getChildByName('timestamp')).getValue(),
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n));
+    ).toEqual(TimestampConverterUtils.makeElapsedTimestamp(10n));
     expect(
       assertDefined(propertyRoot.getChildByName('otherTimestamp')).getValue(),
     ).toEqual(longTimestamp);
   });
 
   function makeRealTimestampStrategy(valueNs: bigint) {
-    return NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(valueNs);
+    return TimestampConverterUtils.makeRealTimestamp(valueNs);
   }
 
   function makeElapsedTimestampStrategy(valueNs: bigint) {
-    return NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(valueNs);
+    return TimestampConverterUtils.makeElapsedTimestamp(valueNs);
   }
 });
diff --git a/tools/winscope/src/parsers/perfetto/abstract_parser.ts b/tools/winscope/src/parsers/perfetto/abstract_parser.ts
index 3c5fdb7..75aab1a 100644
--- a/tools/winscope/src/parsers/perfetto/abstract_parser.ts
+++ b/tools/winscope/src/parsers/perfetto/abstract_parser.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined, assertTrue} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {CoarseVersion} from 'trace/coarse_version';
 import {
   CustomQueryParamTypeMap,
@@ -31,77 +31,70 @@
 
 export abstract class AbstractParser<T> implements Parser<T> {
   protected traceProcessor: WasmEngineProxy;
-  protected realToElapsedTimeOffsetNs?: bigint;
-  protected timestampFactory: TimestampFactory;
-  private timestamps = new Map<TimestampType, Timestamp[]>();
+  protected realToBootTimeOffsetNs?: bigint;
+  protected timestampConverter: ParserTimestampConverter;
+  private timestamps: Timestamp[] | undefined;
   private lengthEntries = 0;
   private traceFile: TraceFile;
+  private elapsedTimestampsNs: Array<bigint> = [];
+
+  protected abstract queryEntry(index: AbsoluteEntryIndex): Promise<any>;
+  protected abstract getTableName(): string;
 
   constructor(
     traceFile: TraceFile,
     traceProcessor: WasmEngineProxy,
-    timestampFactory: TimestampFactory,
+    timestampConverter: ParserTimestampConverter,
   ) {
     this.traceFile = traceFile;
     this.traceProcessor = traceProcessor;
-    this.timestampFactory = timestampFactory;
+    this.timestampConverter = timestampConverter;
   }
 
   async parse() {
-    const elapsedTimestamps = await this.queryElapsedTimestamps();
-    this.lengthEntries = elapsedTimestamps.length;
+    this.elapsedTimestampsNs = await this.queryElapsedTimestamps();
+    this.lengthEntries = this.elapsedTimestampsNs.length;
     assertTrue(
       this.lengthEntries > 0,
       () =>
         `Trace processor tables don't contain entries of type ${this.getTraceType()}`,
     );
 
-    this.realToElapsedTimeOffsetNs = await this.queryRealToElapsedTimeOffset(
-      assertDefined(elapsedTimestamps.at(-1)),
-    );
-
-    this.timestamps.set(
-      TimestampType.ELAPSED,
-      elapsedTimestamps.map((value) =>
-        this.timestampFactory.makeElapsedTimestamp(value),
-      ),
-    );
-
-    this.timestamps.set(
-      TimestampType.REAL,
-      elapsedTimestamps.map((value) =>
-        this.timestampFactory.makeRealTimestamp(
-          value,
-          assertDefined(this.realToElapsedTimeOffsetNs),
-        ),
-      ),
+    let finalNonZeroNsIndex = -1;
+    for (let i = this.elapsedTimestampsNs.length - 1; i > -1; i--) {
+      if (this.elapsedTimestampsNs[i] !== 0n) {
+        finalNonZeroNsIndex = i;
+        break;
+      }
+    }
+    this.realToBootTimeOffsetNs = await this.queryRealToElapsedTimeOffset(
+      assertDefined(this.elapsedTimestampsNs.at(finalNonZeroNsIndex)),
     );
 
     if (this.lengthEntries > 0) {
       // Make sure there are trace entries that can be parsed
-      await this.getEntry(0, TimestampType.ELAPSED);
+      await this.queryEntry(0);
     }
   }
 
-  abstract getTraceType(): TraceType;
+  createTimestamps() {
+    this.timestamps = this.elapsedTimestampsNs.map((ns) => {
+      return this.timestampConverter.makeTimestampFromBootTimeNs(ns);
+    });
+  }
 
   getLengthEntries(): number {
     return this.lengthEntries;
   }
 
-  getTimestamps(type: TimestampType): Timestamp[] | undefined {
-    return this.timestamps.get(type);
+  getTimestamps(): Timestamp[] | undefined {
+    return this.timestamps;
   }
 
   getCoarseVersion(): CoarseVersion {
     return CoarseVersion.LATEST;
   }
 
-  abstract getEntry(
-    index: AbsoluteEntryIndex,
-    timestampType: TimestampType,
-  ): Promise<T>;
-
   customQuery<Q extends CustomQueryType>(
     type: Q,
     entriesRange: EntriesRange,
@@ -114,7 +107,13 @@
     return [this.traceFile.getDescriptor()];
   }
 
-  protected abstract getTableName(): string;
+  getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
 
   private async queryElapsedTimestamps(): Promise<Array<bigint>> {
     const sql = `SELECT ts FROM ${this.getTableName()} ORDER BY id;`;
@@ -146,4 +145,7 @@
     const real = result.iter({}).get('realtime') as bigint;
     return real - elapsedTimestamp;
   }
+
+  abstract getEntry(index: AbsoluteEntryIndex): Promise<T>;
+  abstract getTraceType(): TraceType;
 }
diff --git a/tools/winscope/src/parsers/perfetto/parser_factory.ts b/tools/winscope/src/parsers/perfetto/parser_factory.ts
index 5a1568b..350aadd 100644
--- a/tools/winscope/src/parsers/perfetto/parser_factory.ts
+++ b/tools/winscope/src/parsers/perfetto/parser_factory.ts
@@ -15,7 +15,7 @@
  */
 
 import {globalConfig} from 'common/global_config';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {UrlUtils} from 'common/url_utils';
 import {ProgressListener} from 'messaging/progress_listener';
 import {UserNotificationsListener} from 'messaging/user_notifications_listener';
@@ -44,7 +44,7 @@
 
   async createParsers(
     traceFile: TraceFile,
-    timestampFactory: TimestampFactory,
+    timestampConverter: ParserTimestampConverter,
     progressListener?: ProgressListener,
     notificationListener?: UserNotificationsListener,
   ): Promise<Array<Parser<object>>> {
@@ -85,7 +85,7 @@
         const parser = new ParserType(
           traceFile,
           traceProcessor,
-          timestampFactory,
+          timestampConverter,
         );
         await parser.parse();
         parsers.push(parser);
diff --git a/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts b/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts
index 8567947..39516cf 100644
--- a/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts
+++ b/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {LogMessage} from 'parsers/protolog/log_message';
 import {ParserProtologUtils} from 'parsers/protolog/parser_protolog_utils';
@@ -36,7 +36,7 @@
   private static readonly PROTOLOG_32_BIT_VERSION = '1.0.0';
   private static readonly PROTOLOG_64_BIT_VERSION = '2.0.0';
 
-  private realToElapsedTimeOffsetNs: bigint | undefined;
+  private realToBootTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.PROTO_LOG;
@@ -46,6 +46,14 @@
     return ParserProtoLog.MAGIC_NUMBER;
   }
 
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): com.android.internal.protolog.IProtoLogMessage[] {
@@ -71,7 +79,7 @@
       throw new TypeError(message);
     }
 
-    this.realToElapsedTimeOffsetNs =
+    this.realToBootTimeOffsetNs =
       BigInt(
         assertDefined(fileProto.realTimeToElapsedTimeOffsetMillis).toString(),
       ) * 1000000n;
@@ -92,31 +100,16 @@
     return fileProto.log;
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: com.android.internal.protolog.IProtoLogMessage,
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(
-      assertDefined(entry.elapsedRealtimeNanos).toString(),
+  ): Timestamp {
+    return this.timestampConverter.makeTimestampFromBootTimeNs(
+      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
-    if (
-      this.timestampFactory.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return this.timestampFactory.makeTimestampFromType(
-        type,
-        elapsedRealtimeNanos,
-        this.realToElapsedTimeOffsetNs,
-      );
-    }
-    return undefined;
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: com.android.internal.protolog.IProtoLogMessage,
   ): PropertyTreeNode {
     let messageHash = assertDefined(entry.messageHash).toString();
@@ -136,9 +129,8 @@
     const logMessage = this.makeLogMessage(entry, message, tag);
     return ParserProtologUtils.makeMessagePropertiesTree(
       logMessage,
-      timestampType,
-      this.realToElapsedTimeOffsetNs,
-      this.timestampFactory,
+      this.timestampConverter,
+      this.getRealToMonotonicTimeOffsetNs() !== undefined,
     );
   }
 
diff --git a/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts b/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts
index 418affa..5d3f2aa 100644
--- a/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts
+++ b/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -25,7 +25,7 @@
 
 interface ExpectedMessage {
   'message': string;
-  'ts': {'real': string; 'elapsed': string};
+  'ts': string;
   'at': string;
   'level': string;
   'tag': string;
@@ -36,8 +36,6 @@
     traceFile: string,
     timestampCount: number,
     first3ExpectedRealTimestamps: Timestamp[],
-    first3ExpectedElapsedTimestamps: Timestamp[],
-    first3WithTimezoneTimestamps: Timestamp[],
     expectedFirstMessage: ExpectedMessage,
   ) =>
   () => {
@@ -62,51 +60,22 @@
       expect(parser.getLengthEntries()).toEqual(timestampCount);
     });
 
-    it('provides elapsed timestamps', () => {
-      const timestamps = parser.getTimestamps(TimestampType.ELAPSED)!;
-      expect(timestamps.length).toEqual(timestampCount);
-
-      expect(timestamps.slice(0, 3)).toEqual(first3ExpectedElapsedTimestamps);
-    });
-
-    it('provides real timestamps', () => {
-      const timestamps = parser.getTimestamps(TimestampType.REAL)!;
+    it('provides timestamps', () => {
+      const timestamps = assertDefined(parser.getTimestamps());
       expect(timestamps.length).toEqual(timestampCount);
 
       expect(timestamps.slice(0, 3)).toEqual(first3ExpectedRealTimestamps);
     });
 
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        traceFile,
-        true,
-      )) as Parser<PropertyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.PROTO_LOG,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        ).slice(0, 3),
-      ).toEqual(first3ExpectedElapsedTimestamps);
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        ).slice(0, 3),
-      ).toEqual(first3WithTimezoneTimestamps);
-    });
-
-    it('reconstructs human-readable log message (ELAPSED time)', async () => {
-      const message = await parser.getEntry(0, TimestampType.ELAPSED);
+    it('reconstructs human-readable log message', async () => {
+      const message = await parser.getEntry(0);
 
       expect(
         assertDefined(message.getChildByName('text')).formattedValue(),
       ).toEqual(expectedFirstMessage['message']);
       expect(
         assertDefined(message.getChildByName('timestamp')).formattedValue(),
-      ).toEqual(expectedFirstMessage['ts']['elapsed']);
+      ).toEqual(expectedFirstMessage['ts']);
       expect(
         assertDefined(message.getChildByName('tag')).formattedValue(),
       ).toEqual(expectedFirstMessage['tag']);
@@ -119,14 +88,14 @@
     });
 
     it('reconstructs human-readable log message (REAL time)', async () => {
-      const message = await parser.getEntry(0, TimestampType.REAL);
+      const message = await parser.getEntry(0);
 
       expect(
         assertDefined(message.getChildByName('text')).formattedValue(),
       ).toEqual(expectedFirstMessage['message']);
       expect(
         assertDefined(message.getChildByName('timestamp')).formattedValue(),
-      ).toEqual(expectedFirstMessage['ts']['real']);
+      ).toEqual(expectedFirstMessage['ts']);
       expect(
         assertDefined(message.getChildByName('tag')).formattedValue(),
       ).toEqual(expectedFirstMessage['tag']);
@@ -146,27 +115,14 @@
       'traces/elapsed_and_real_timestamp/ProtoLog32.pb',
       50,
       [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1655727125377266486n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1655727125377336718n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1655727125377350430n),
-      ],
-      [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850746266486n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850746336718n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850746350430n),
-      ],
-      [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1655746925377266486n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1655746925377336718n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1655746925377350430n),
+        TimestampConverterUtils.makeRealTimestamp(1655727125377266486n),
+        TimestampConverterUtils.makeRealTimestamp(1655727125377336718n),
+        TimestampConverterUtils.makeRealTimestamp(1655727125377350430n),
       ],
       {
         'message':
           'InsetsSource updateVisibility for ITYPE_IME, serverVisible: false clientVisible: false',
-        'ts': {
-          'real': '2022-06-20T12:12:05.377266486',
-          'elapsed': '14m10s746ms266486ns',
-        },
+        'ts': '2022-06-20T12:12:05.377266486',
         'tag': 'WindowManager',
         'level': 'DEBUG',
         'at': 'com/android/server/wm/InsetsSourceProvider.java',
@@ -179,26 +135,13 @@
       'traces/elapsed_and_real_timestamp/ProtoLog64.pb',
       4615,
       [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1709196806399529939n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1709196806399763866n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1709196806400297151n),
-      ],
-      [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1315553529939n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1315553763866n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1315554297151n),
-      ],
-      [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1709216606399529939n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1709216606399763866n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1709216606400297151n),
+        TimestampConverterUtils.makeRealTimestamp(1709196806399529939n),
+        TimestampConverterUtils.makeRealTimestamp(1709196806399763866n),
+        TimestampConverterUtils.makeRealTimestamp(1709196806400297151n),
       ],
       {
         'message': 'Starting activity when config will change = false',
-        'ts': {
-          'real': '2024-02-29T08:53:26.399529939',
-          'elapsed': '21m55s553ms529939ns',
-        },
+        'ts': '2024-02-29T08:53:26.399529939',
         'tag': 'WindowManager',
         'level': 'VERBOSE',
         'at': 'com/android/server/wm/ActivityStarter.java',
@@ -211,26 +154,13 @@
       'traces/elapsed_and_real_timestamp/ProtoLogMissingConfigMessage.pb',
       7295,
       [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1669053909777144978n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1669053909778011697n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1669053909778800707n),
-      ],
-      [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(24398190144978n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(24398191011697n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(24398191800707n),
-      ],
-      [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1669073709777144978n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1669073709778011697n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1669073709778800707n),
+        TimestampConverterUtils.makeRealTimestamp(1669053909777144978n),
+        TimestampConverterUtils.makeRealTimestamp(1669053909778011697n),
+        TimestampConverterUtils.makeRealTimestamp(1669053909778800707n),
       ],
       {
         'message': 'SURFACE isColorSpaceAgnostic=true: NotificationShade',
-        'ts': {
-          'real': '2022-11-21T18:05:09.777144978',
-          'elapsed': '6h46m38s190ms144978ns',
-        },
+        'ts': '2022-11-21T18:05:09.777144978',
         'tag': 'WindowManager',
         'level': 'INFO',
         'at': 'com/android/server/wm/WindowSurfaceController.java',
diff --git a/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts b/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts
index 5fcc3b6..9d126b6 100644
--- a/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts
+++ b/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-import {TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {SetFormatters} from 'parsers/operations/set_formatters';
 import {
   MakeTimestampStrategyType,
   TransformToTimestamp,
 } from 'parsers/operations/transform_to_timestamp';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {LogMessage} from './log_message';
@@ -29,9 +28,8 @@
 export class ParserProtologUtils {
   static makeMessagePropertiesTree(
     logMessage: LogMessage,
-    timestampType: TimestampType,
-    realToElapsedTimeOffsetNs: bigint | undefined,
-    timestampFactory: TimestampFactory,
+    timestampConverter: ParserTimestampConverter,
+    isMonotonic: boolean,
   ): PropertyTreeNode {
     const tree = new PropertyTreeBuilderFromProto()
       .setData(logMessage)
@@ -40,19 +38,14 @@
       .setVisitPrototype(false)
       .build();
 
-    const customFormatters = new Map([['timestamp', TIMESTAMP_FORMATTER]]);
+    const customFormatters = new Map([['timestamp', TIMESTAMP_NODE_FORMATTER]]);
 
-    let strategy: MakeTimestampStrategyType | undefined;
-    if (timestampType === TimestampType.REAL) {
-      strategy = (valueNs: bigint) => {
-        return timestampFactory.makeRealTimestamp(
-          valueNs,
-          realToElapsedTimeOffsetNs,
-        );
-      };
-    } else {
-      strategy = timestampFactory.makeElapsedTimestamp;
-    }
+    const strategy: MakeTimestampStrategyType = (valueNs: bigint) => {
+      if (isMonotonic) {
+        return timestampConverter.makeTimestampFromMonotonicNs(valueNs);
+      }
+      return timestampConverter.makeTimestampFromBootTimeNs(valueNs);
+    };
 
     new TransformToTimestamp(['timestamp'], strategy).apply(tree);
     new SetFormatters(undefined, customFormatters).apply(tree);
diff --git a/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts b/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts
index a84f549..9ce2138 100644
--- a/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts
+++ b/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-import {TimestampType} from 'common/time';
 import {AbstractParser} from 'parsers/perfetto/abstract_parser';
 import {LogMessage} from 'parsers/protolog/log_message';
 import {ParserProtologUtils} from 'parsers/protolog/parser_protolog_utils';
@@ -41,11 +40,8 @@
     return TraceType.PROTO_LOG;
   }
 
-  override async getEntry(
-    index: number,
-    timestampType: TimestampType,
-  ): Promise<PropertyTreeNode> {
-    const protologEntry = await this.queryProtoLogEntry(index);
+  override async getEntry(index: number): Promise<PropertyTreeNode> {
+    const protologEntry = await this.queryEntry(index);
     const logMessage: LogMessage = {
       text: protologEntry.message,
       tag: protologEntry.tag,
@@ -56,9 +52,8 @@
 
     return ParserProtologUtils.makeMessagePropertiesTree(
       logMessage,
-      timestampType,
-      this.realToElapsedTimeOffsetNs,
-      this.timestampFactory,
+      this.timestampConverter,
+      this.getRealToMonotonicTimeOffsetNs() !== undefined,
     );
   }
 
@@ -66,7 +61,7 @@
     return 'protolog';
   }
 
-  private async queryProtoLogEntry(
+  protected override async queryEntry(
     index: number,
   ): Promise<PerfettoLogMessageTableRow> {
     const sql = `
diff --git a/tools/winscope/src/parsers/protolog/perfetto/parser_protolog_test.ts b/tools/winscope/src/parsers/protolog/perfetto/parser_protolog_test.ts
index a899b7f..cd10ad7 100644
--- a/tools/winscope/src/parsers/protolog/perfetto/parser_protolog_test.ts
+++ b/tools/winscope/src/parsers/protolog/perfetto/parser_protolog_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {TraceType} from 'trace/trace_type';
@@ -36,58 +35,22 @@
     expect(parser.getTraceType()).toEqual(TraceType.PROTO_LOG);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
 
     expect(timestamps.length).toEqual(75);
 
     // TODO: They shouldn't all have the same timestamp...
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(5939002349294n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(5939002349294n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(5939002349294n),
+      TimestampConverterUtils.makeRealTimestamp(1706547264827624563n),
+      TimestampConverterUtils.makeRealTimestamp(1706547264827624563n),
+      TimestampConverterUtils.makeRealTimestamp(1706547264827624563n),
     ];
     expect(timestamps.slice(0, 3)).toEqual(expected);
   });
 
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
-
-    expect(timestamps.length).toEqual(75);
-
-    // TODO: They shouldn't all have the same timestamp...
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1706547264827624563n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1706547264827624563n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1706547264827624563n),
-    ];
-    expect(timestamps.slice(0, 3)).toEqual(expected);
-  });
-
-  it('reconstructs human-readable log message (ELAPSED time)', async () => {
-    const message = await parser.getEntry(0, TimestampType.ELAPSED);
-
-    expect(
-      assertDefined(message.getChildByName('text')).formattedValue(),
-    ).toEqual('Sent Transition (#11) createdAt=01-29 17:54:23.793');
-    expect(
-      assertDefined(message.getChildByName('timestamp')).formattedValue(),
-    ).toEqual('1h38m59s2ms349294ns');
-    expect(
-      assertDefined(message.getChildByName('tag')).formattedValue(),
-    ).toEqual('WindowManager');
-    expect(
-      assertDefined(message.getChildByName('level')).formattedValue(),
-    ).toEqual('VERBOSE');
-    expect(
-      assertDefined(message.getChildByName('at')).formattedValue(),
-    ).toEqual('<NO_LOC>');
-  });
-
   it('reconstructs human-readable log message (REAL time)', async () => {
-    const message = await parser.getEntry(0, TimestampType.REAL);
+    const message = await parser.getEntry(0);
 
     expect(
       assertDefined(message.getChildByName('text')).formattedValue(),
@@ -105,26 +68,4 @@
       assertDefined(message.getChildByName('at')).formattedValue(),
     ).toEqual('<NO_LOC>');
   });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = await UnitTestUtils.getPerfettoParser(
-      TraceType.PROTO_LOG,
-      'traces/perfetto/protolog.perfetto-trace',
-      true,
-    );
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(TraceType.PROTO_LOG);
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      )[0],
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(5939002349294n));
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1706567064827624563n),
-    );
-  });
 });
diff --git a/tools/winscope/src/parsers/screen_recording/parser_screen_recording.ts b/tools/winscope/src/parsers/screen_recording/parser_screen_recording.ts
index a10a4a8..89c0b73 100644
--- a/tools/winscope/src/parsers/screen_recording/parser_screen_recording.ts
+++ b/tools/winscope/src/parsers/screen_recording/parser_screen_recording.ts
@@ -15,22 +15,16 @@
  */
 
 import {ArrayUtils} from 'common/array_utils';
-import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {CoarseVersion} from 'trace/coarse_version';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
 import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
 import {TraceType} from 'trace/trace_type';
 
-class ScreenRecordingMetadataEntry {
-  constructor(
-    public timestampElapsedNs: bigint,
-    public timestampRealtimeNs: bigint,
-  ) {}
-}
-
 class ParserScreenRecording extends AbstractParser {
+  private realToBootTimeOffsetNs: bigint | undefined;
+
   override getTraceType(): TraceType {
     return TraceType.SCREEN_RECORDING;
   }
@@ -40,10 +34,18 @@
   }
 
   override getMagicNumber(): number[] {
-    return ParserScreenRecording.MPEG4_MAGIC_NMBER;
+    return ParserScreenRecording.MPEG4_MAGIC_NUMBER;
   }
 
-  override decodeTrace(videoData: Uint8Array): ScreenRecordingMetadataEntry[] {
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override decodeTrace(videoData: Uint8Array): Array<bigint> {
     const posVersion = this.searchMagicString(videoData);
     const [posTimeOffset, metadataVersion] = this.parseMetadataVersion(
       videoData,
@@ -68,10 +70,11 @@
         other traces. Metadata contains monotonic time instead of elapsed.`);
     }
 
-    const [posCount, timeOffsetNs] = this.parseRealToElapsedTimeOffsetNs(
+    const [posCount, timeOffsetNs] = this.parserealToBootTimeOffsetNs(
       videoData,
       posTimeOffset,
     );
+    this.realToBootTimeOffsetNs = timeOffsetNs;
     const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
     const timestampsElapsedNs = this.parseTimestampsElapsedNs(
       videoData,
@@ -79,47 +82,20 @@
       count,
     );
 
-    return timestampsElapsedNs.map((timestampElapsedNs: bigint) => {
-      return new ScreenRecordingMetadataEntry(
-        timestampElapsedNs,
-        timestampElapsedNs + timeOffsetNs,
-      );
-    });
+    return timestampsElapsedNs;
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    decodedEntry: ScreenRecordingMetadataEntry,
-  ): undefined | Timestamp {
-    if (type !== TimestampType.ELAPSED && type !== TimestampType.REAL) {
-      return undefined;
-    }
-    if (type === TimestampType.ELAPSED) {
-      return this.timestampFactory.makeElapsedTimestamp(
-        decodedEntry.timestampElapsedNs,
-      );
-    } else if (type === TimestampType.REAL) {
-      return this.timestampFactory.makeRealTimestamp(
-        decodedEntry.timestampRealtimeNs,
-      );
-    }
-    return undefined;
+  protected override getTimestamp(decodedEntry: bigint): Timestamp {
+    return this.timestampConverter.makeTimestampFromBootTimeNs(decodedEntry);
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
-    entry: ScreenRecordingMetadataEntry,
+    entry: bigint,
   ): ScreenRecordingTraceEntry {
-    const initialTimestamp = assertDefined(
-      this.getTimestamps(TimestampType.ELAPSED),
-    )[0];
-    const currentTimestamp = this.timestampFactory.makeElapsedTimestamp(
-      entry.timestampElapsedNs,
-    );
     const videoTimeSeconds = ScreenRecordingUtils.timestampToVideoTimeSeconds(
-      initialTimestamp,
-      currentTimestamp,
+      this.decodedEntries[0],
+      entry,
     );
     const videoData = this.traceFile.file;
     return new ScreenRecordingTraceEntry(videoTimeSeconds, videoData);
@@ -153,7 +129,7 @@
     return [pos, version];
   }
 
-  private parseRealToElapsedTimeOffsetNs(
+  private parserealToBootTimeOffsetNs(
     videoData: Uint8Array,
     pos: number,
   ): [number, bigint] {
@@ -202,7 +178,7 @@
     return timestamps;
   }
 
-  private static readonly MPEG4_MAGIC_NMBER = [
+  private static readonly MPEG4_MAGIC_NUMBER = [
     0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32,
   ]; // ....ftypmp42
   private static readonly WINSCOPE_META_MAGIC_STRING = [
diff --git a/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy.ts b/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy.ts
index 1def2a9..f58c567 100644
--- a/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy.ts
+++ b/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy.ts
@@ -15,10 +15,10 @@
  */
 
 import {ArrayUtils} from 'common/array_utils';
-import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
+import {ScreenRecordingUtils} from 'trace/screen_recording_utils';
 import {TraceType} from 'trace/trace_type';
 
 class ParserScreenRecordingLegacy extends AbstractParser {
@@ -27,38 +27,35 @@
   }
 
   override getMagicNumber(): number[] {
-    return ParserScreenRecordingLegacy.MPEG4_MAGIC_NMBER;
+    return ParserScreenRecordingLegacy.MPEG4_MAGIC_NUMBER;
   }
 
-  override decodeTrace(videoData: Uint8Array): Timestamp[] {
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override decodeTrace(videoData: Uint8Array): Array<bigint> {
     const posCount = this.searchMagicString(videoData);
     const [posTimestamps, count] = this.parseFramesCount(videoData, posCount);
-    return this.parseTimestamps(videoData, posTimestamps, count);
+    return this.parseVideoData(videoData, posTimestamps, count);
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    decodedEntry: Timestamp,
-  ): undefined | Timestamp {
-    if (type !== TimestampType.ELAPSED) {
-      return undefined;
-    }
-    return decodedEntry;
+  protected override getTimestamp(decodedEntry: bigint): Timestamp {
+    return this.timestampConverter.makeTimestampFromMonotonicNs(decodedEntry);
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
-    entry: Timestamp,
+    entry: bigint,
   ): ScreenRecordingTraceEntry {
-    const currentTimestamp = entry;
-    const initialTimestamp = assertDefined(
-      this.getTimestamps(TimestampType.ELAPSED),
-    )[0];
-    const videoTimeSeconds =
-      Number(currentTimestamp.getValueNs() - initialTimestamp.getValueNs()) /
-        1000000000 +
-      ParserScreenRecordingLegacy.EPSILON;
+    const videoTimeSeconds = ScreenRecordingUtils.timestampToVideoTimeSeconds(
+      this.decodedEntries[0],
+      entry,
+    );
     const videoData = this.traceFile.file;
     return new ScreenRecordingTraceEntry(videoTimeSeconds, videoData);
   }
@@ -91,27 +88,27 @@
     return [pos, framesCount];
   }
 
-  private parseTimestamps(
+  private parseVideoData(
     videoData: Uint8Array,
     pos: number,
     count: number,
-  ): Timestamp[] {
+  ): Array<bigint> {
     if (pos + count * 8 > videoData.length) {
       throw new TypeError(
         'Failed to parse timestamps. Video data is too short.',
       );
     }
-    const timestamps: Timestamp[] = [];
+    const timestamps: Array<bigint> = [];
     for (let i = 0; i < count; ++i) {
       const value =
         ArrayUtils.toUintLittleEndian(videoData, pos, pos + 8) * 1000n;
       pos += 8;
-      timestamps.push(this.timestampFactory.makeElapsedTimestamp(value));
+      timestamps.push(value);
     }
     return timestamps;
   }
 
-  private static readonly MPEG4_MAGIC_NMBER = [
+  private static readonly MPEG4_MAGIC_NUMBER = [
     0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x6d, 0x70, 0x34, 0x32,
   ]; // ....ftypmp42
   private static readonly WINSCOPE_META_MAGIC_STRING = [
diff --git a/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy_test.ts b/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy_test.ts
index b3c32f9..16cf41d 100644
--- a/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy_test.ts
+++ b/tools/winscope/src/parsers/screen_recording/parser_screen_recording_legacy_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -39,66 +39,36 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
 
     expect(timestamps.length).toEqual(85);
 
     let expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19446131807000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19446158500000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19446167117000n),
+      TimestampConverterUtils.makeElapsedTimestamp(19446131807000n),
+      TimestampConverterUtils.makeElapsedTimestamp(19446158500000n),
+      TimestampConverterUtils.makeElapsedTimestamp(19446167117000n),
     ];
     expect(timestamps.slice(0, 3)).toEqual(expected);
 
     expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19448470076000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19448487525000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19448501007000n),
+      TimestampConverterUtils.makeElapsedTimestamp(19448470076000n),
+      TimestampConverterUtils.makeElapsedTimestamp(19448487525000n),
+      TimestampConverterUtils.makeElapsedTimestamp(19448501007000n),
     ];
     expect(timestamps.slice(timestamps.length - 3, timestamps.length)).toEqual(
       expected,
     );
   });
 
-  it("doesn't provide real timestamps", () => {
-    expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
-  });
-
-  it('does not apply timezone info', async () => {
-    const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-      'traces/elapsed_timestamp/screen_recording.mp4',
-      true,
-    )) as Parser<ScreenRecordingTraceEntry>;
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-      TraceType.SCREEN_RECORDING,
-    );
-
-    const expectedElapsed = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19446131807000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19446158500000n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(19446167117000n),
-    ];
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      ).slice(0, 3),
-    ).toEqual(expectedElapsed);
-  });
-
   it('retrieves trace entry', async () => {
     {
-      const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
       expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
     }
     {
-      const entry = await parser.getEntry(
-        parser.getLengthEntries() - 1,
-        TimestampType.ELAPSED,
-      );
+      const entry = await parser.getEntry(parser.getLengthEntries() - 1);
       expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
       expect(Number(entry.videoTimeSeconds)).toBeCloseTo(2.37, 0.001);
     }
diff --git a/tools/winscope/src/parsers/screen_recording/parser_screen_recording_test.ts b/tools/winscope/src/parsers/screen_recording/parser_screen_recording_test.ts
index 8688488..ff9c279 100644
--- a/tools/winscope/src/parsers/screen_recording/parser_screen_recording_test.ts
+++ b/tools/winscope/src/parsers/screen_recording/parser_screen_recording_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -40,77 +39,27 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LATEST);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
 
     expect(timestamps.length).toEqual(123);
 
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(211827840430n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(211842401430n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(211862172430n),
+      TimestampConverterUtils.makeRealTimestamp(1666361048792787045n),
+      TimestampConverterUtils.makeRealTimestamp(1666361048807348045n),
+      TimestampConverterUtils.makeRealTimestamp(1666361048827119045n),
     ];
     expect(timestamps.slice(0, 3)).toEqual(expected);
   });
 
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
-
-    expect(timestamps.length).toEqual(123);
-
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1666361048792787045n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1666361048807348045n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1666361048827119045n),
-    ];
-    expect(timestamps.slice(0, 3)).toEqual(expected);
-  });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-      'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4',
-      true,
-    )) as Parser<ScreenRecordingTraceEntry>;
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-      TraceType.SCREEN_RECORDING,
-    );
-
-    const expectedElapsed = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(211827840430n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(211842401430n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(211862172430n),
-    ];
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      ).slice(0, 3),
-    ).toEqual(expectedElapsed);
-
-    const expectedReal = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1666380848792787045n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1666380848807348045n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1666380848827119045n),
-    ];
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      ).slice(0, 3),
-    ).toEqual(expectedReal);
-  });
-
   it('retrieves trace entry', async () => {
     {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
       expect(Number(entry.videoTimeSeconds)).toBeCloseTo(0);
     }
     {
-      const entry = await parser.getEntry(
-        parser.getLengthEntries() - 1,
-        TimestampType.REAL,
-      );
+      const entry = await parser.getEntry(parser.getLengthEntries() - 1);
       expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
       expect(Number(entry.videoTimeSeconds)).toBeCloseTo(1.371077, 0.001);
     }
diff --git a/tools/winscope/src/parsers/screen_recording/parser_screenshot.ts b/tools/winscope/src/parsers/screen_recording/parser_screenshot.ts
index 194c824..06dfc9b 100644
--- a/tools/winscope/src/parsers/screen_recording/parser_screenshot.ts
+++ b/tools/winscope/src/parsers/screen_recording/parser_screenshot.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {CoarseVersion} from 'trace/coarse_version';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
@@ -38,23 +37,24 @@
     return ParserScreenshot.MAGIC_NUMBER;
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    decodedEntry: number,
-  ): Timestamp | undefined {
-    if (NO_TIMEZONE_OFFSET_FACTORY.canMakeTimestampFromType(type, 0n)) {
-      return NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(type, 0n, 0n);
-    }
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
     return undefined;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  protected override getTimestamp(decodedEntry: number): Timestamp {
+    return this.timestampConverter.makeZeroTimestamp();
+  }
+
   override decodeTrace(screenshotData: Uint8Array): number[] {
     return [0]; // require a non-empty array to be returned so trace can provide timestamps
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: number,
   ): ScreenRecordingTraceEntry {
     const screenshotData = this.traceFile.file;
diff --git a/tools/winscope/src/parsers/screen_recording/parser_screenshot_test.ts b/tools/winscope/src/parsers/screen_recording/parser_screenshot_test.ts
index 1172456..e8bccd8 100644
--- a/tools/winscope/src/parsers/screen_recording/parser_screenshot_test.ts
+++ b/tools/winscope/src/parsers/screen_recording/parser_screenshot_test.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverter} from 'common/timestamp_converter';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {ScreenRecordingTraceEntry} from 'trace/screen_recording';
@@ -29,12 +29,14 @@
   let file: File;
 
   beforeAll(async () => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
     file = await UnitTestUtils.getFixtureFile('traces/screenshot.png');
     parser = new ParserScreenshot(
       new TraceFile(file),
-      NO_TIMEZONE_OFFSET_FACTORY,
+      new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO, 0n),
     );
     await parser.parse();
+    parser.createTimestamps();
   });
 
   it('has expected trace type', () => {
@@ -45,42 +47,28 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LATEST);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
 
-    const expected = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n);
-    timestamps.forEach((timestamp) => expect(timestamp).toEqual(expected));
-  });
-
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
-
-    const expected = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n);
+    const expected = TimestampConverterUtils.makeElapsedTimestamp(0n);
     timestamps.forEach((timestamp) => expect(timestamp).toEqual(expected));
   });
 
   it('does not apply timezone info', async () => {
     const parserWithTimezoneInfo = new ParserScreenshot(
       new TraceFile(file),
-      UnitTestUtils.TIMESTAMP_FACTORY_WITH_TIMEZONE,
+      TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET,
     );
     await parserWithTimezoneInfo.parse();
 
-    const expectedElapsed = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n);
-    assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).forEach(
-      (timestamp) => expect(timestamp).toEqual(expectedElapsed),
-    );
-
-    const expectedReal = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n);
-    assertDefined(parser.getTimestamps(TimestampType.REAL)).forEach(
-      (timestamp) => expect(timestamp).toEqual(expectedReal),
+    const expectedReal = TimestampConverterUtils.makeElapsedTimestamp(0n);
+    assertDefined(parser.getTimestamps()).forEach((timestamp) =>
+      expect(timestamp).toEqual(expectedReal),
     );
   });
 
   it('retrieves entry', async () => {
-    const entry = await parser.getEntry(0, TimestampType.REAL);
+    const entry = await parser.getEntry(0);
     expect(entry).toBeInstanceOf(ScreenRecordingTraceEntry);
     expect(entry.isImage).toBeTrue();
   });
diff --git a/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger.ts b/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger.ts
index 8ad7291..068a252 100644
--- a/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger.ts
+++ b/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger.ts
@@ -15,8 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {AddDefaults} from 'parsers/operations/add_defaults';
 import {SetFormatters} from 'parsers/operations/set_formatters';
@@ -94,7 +93,8 @@
     ),
   };
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToMonotonicTimeOffsetNs: bigint | undefined;
+  private isDump = false;
 
   override getTraceType(): TraceType {
     return TraceType.SURFACE_FLINGER;
@@ -104,6 +104,14 @@
     return ParserSurfaceFlinger.MAGIC_NUMBER;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return this.realToMonotonicTimeOffsetNs;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): android.surfaceflinger.ILayersTraceProto[] {
@@ -113,52 +121,30 @@
     const timeOffset = BigInt(
       decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToMonotonicTimeOffsetNs =
+      timeOffset !== 0n ? timeOffset : undefined;
+    this.isDump =
+      decoded.entry?.length === 1 &&
+      !Object.prototype.hasOwnProperty.call(
+        decoded.entry[0],
+        'elapsedRealtimeNanos',
+      );
     return decoded.entry ?? [];
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: android.surfaceflinger.ILayersTraceProto,
-  ): undefined | Timestamp {
-    const isDump = !Object.prototype.hasOwnProperty.call(
-      entry,
-      'elapsedRealtimeNanos',
+  ): Timestamp {
+    if (this.isDump) {
+      return this.timestampConverter.makeZeroTimestamp();
+    }
+    return this.timestampConverter.makeTimestampFromMonotonicNs(
+      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
-    if (
-      isDump &&
-      NO_TIMEZONE_OFFSET_FACTORY.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(type, 0n, 0n);
-    }
-
-    if (!isDump) {
-      const elapsedRealtimeNanos = BigInt(
-        assertDefined(entry.elapsedRealtimeNanos).toString(),
-      );
-      if (
-        this.timestampFactory.canMakeTimestampFromType(
-          type,
-          this.realToElapsedTimeOffsetNs,
-        )
-      ) {
-        return this.timestampFactory.makeTimestampFromType(
-          type,
-          elapsedRealtimeNanos,
-          this.realToElapsedTimeOffsetNs,
-        );
-      }
-    }
-
-    return undefined;
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: android.surfaceflinger.ILayersTraceProto,
   ): HierarchyTreeNode {
     return this.makeHierarchyTree(entry);
diff --git a/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_dump_test.ts b/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_dump_test.ts
index 72f37c6..0b62e05 100644
--- a/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_dump_test.ts
+++ b/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_dump_test.ts
@@ -13,8 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -22,7 +22,11 @@
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 
 describe('ParserSurfaceFlingerDump', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  beforeAll(() => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
+  });
+
+  describe('trace with real timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -39,42 +43,28 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamp', () => {
-      const expected = [NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n)];
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(expected);
-    });
-
-    it('provides real timestamp (always zero)', () => {
-      const expected = [NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n)];
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(expected);
+    it('provides timestamps (always zero)', () => {
+      const expected = [TimestampConverterUtils.makeElapsedTimestamp(0n)];
+      expect(parser.getTimestamps()).toEqual(expected);
     });
 
     it('does not apply timezone info', async () => {
       const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
         'traces/elapsed_and_real_timestamp/dump_SurfaceFlinger.pb',
-        true,
+        UnitTestUtils.getTimestampConverter(true),
       )) as Parser<HierarchyTreeNode>;
 
-      const expectedElapsed = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-      ];
-      expect(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      ).toEqual(expectedElapsed);
-
-      const expectedReal = [NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n)];
-      expect(parserWithTimezoneInfo.getTimestamps(TimestampType.REAL)).toEqual(
-        expectedReal,
-      );
+      const expected = [TimestampConverterUtils.makeElapsedTimestamp(0n)];
+      expect(parserWithTimezoneInfo.getTimestamps()).toEqual(expected);
     });
 
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeTruthy();
     });
   });
 
-  describe('trace with elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -91,13 +81,9 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamp (always zero)', () => {
-      const expected = [NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n)];
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(expected);
-    });
-
-    it("doesn't provide real timestamp", () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
+    it('provides timestamp (always zero)', () => {
+      const expected = [TimestampConverterUtils.makeElapsedTimestamp(0n)];
+      expect(parser.getTimestamps()).toEqual(expected);
     });
   });
 });
diff --git a/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_test.ts
index 79515ae..d925ad7 100644
--- a/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_test.ts
+++ b/tools/winscope/src/parsers/surface_flinger/legacy/parser_surface_flinger_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -27,7 +26,7 @@
 import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
 
 describe('ParserSurfaceFlinger', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  describe('trace with real timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
     let trace: Trace<HierarchyTreeNode>;
 
@@ -50,68 +49,25 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
+    it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14500282843n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14631249355n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15403446377n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089102062832n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089233029344n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090005226366n),
       ];
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3),
-      ).toEqual(expected);
-    });
-
-    it('provides real timestamps', () => {
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089102062832n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089233029344n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090005226366n),
-      ];
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3),
-      ).toEqual(expected);
-    });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.SURFACE_FLINGER,
+      expect(assertDefined(parser.getTimestamps()).slice(0, 3)).toEqual(
+        expected,
       );
-
-      const expectedElapsed = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14500282843n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14631249355n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15403446377n),
-      ];
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        ).slice(0, 3),
-      ).toEqual(expectedElapsed);
-
-      const expectedReal = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126889102062832n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126889233029344n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126890005226366n),
-      ];
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        ).slice(0, 3),
-      ).toEqual(expectedReal);
     });
 
     it('provides correct root entry node', async () => {
-      const entry = await parser.getEntry(1, TimestampType.REAL);
+      const entry = await parser.getEntry(1);
       expect(entry.id).toEqual('LayerTraceEntry root');
       expect(entry.name).toEqual('root');
     });
 
     it('decodes layer state flags', async () => {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       {
         const layer = assertDefined(
           entry.findDfs(UiTreeUtils.makeIdMatchFilter('27 Leaf:24:25#27')),
@@ -184,7 +140,7 @@
       const parser = (await UnitTestUtils.getParser(
         'traces/elapsed_and_real_timestamp/SurfaceFlinger_with_duplicated_ids.pb',
       )) as Parser<HierarchyTreeNode>;
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeTruthy();
 
       const layer = assertDefined(
@@ -214,7 +170,7 @@
     });
   });
 
-  describe('trace with elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -231,34 +187,14 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED))[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850335483446n));
-    });
-
-    it("doesn't provide real timestamps", () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
-    });
-
-    it('does not apply timezone info', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_timestamp/SurfaceFlinger.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.SURFACE_FLINGER,
+    it('provides timestamps', () => {
+      expect(assertDefined(parser.getTimestamps())[0]).toEqual(
+        TimestampConverterUtils.makeElapsedTimestamp(850335483446n),
       );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850335483446n));
     });
 
     it('provides correct root entry node', async () => {
-      const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+      const entry = await parser.getEntry(0);
       expect(entry.id).toEqual('LayerTraceEntry root');
       expect(entry.name).toEqual('root');
     });
diff --git a/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger.ts b/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger.ts
index 86a8eb9..22d032c 100644
--- a/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger.ts
+++ b/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger.ts
@@ -15,13 +15,12 @@
  */
 
 import {assertDefined, assertTrue} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {AddDefaults} from 'parsers/operations/add_defaults';
 import {SetFormatters} from 'parsers/operations/set_formatters';
 import {TranslateIntDef} from 'parsers/operations/translate_intdef';
 import {AbstractParser} from 'parsers/perfetto/abstract_parser';
-import {FakeProtoBuilder} from 'parsers/perfetto/fake_proto_builder';
+import {FakeProto, FakeProtoBuilder} from 'parsers/perfetto/fake_proto_builder';
 import {FakeProtoTransformer} from 'parsers/perfetto/fake_proto_transformer';
 import {Utils} from 'parsers/perfetto/utils';
 import {RectsComputation} from 'parsers/surface_flinger/computations/rects_computation';
@@ -102,9 +101,9 @@
   constructor(
     traceFile: TraceFile,
     traceProcessor: WasmEngineProxy,
-    timestampFactory: TimestampFactory,
+    timestampConverter: ParserTimestampConverter,
   ) {
-    super(traceFile, traceProcessor, timestampFactory);
+    super(traceFile, traceProcessor, timestampConverter);
     this.layersSnapshotProtoTransformer = new FakeProtoTransformer(
       assertDefined(ParserSurfaceFlinger.entryField.tamperedMessageType),
     );
@@ -117,17 +116,8 @@
     return TraceType.SURFACE_FLINGER;
   }
 
-  override async getEntry(
-    index: number,
-    timestampType: TimestampType,
-  ): Promise<HierarchyTreeNode> {
-    let snapshotProto = await Utils.queryEntry(
-      this.traceProcessor,
-      this.getTableName(),
-      index,
-    );
-    snapshotProto =
-      this.layersSnapshotProtoTransformer.transform(snapshotProto);
+  override async getEntry(index: number): Promise<HierarchyTreeNode> {
+    const snapshotProto = await this.queryEntry(index);
     const layerProtos = (await this.querySnapshotLayers(index)).map(
       (layerProto) => this.layerProtoTransformer.transform(layerProto),
     );
@@ -181,6 +171,15 @@
     return 'surfaceflinger_layers_snapshot';
   }
 
+  protected override async queryEntry(index: number): Promise<FakeProto> {
+    const snapshotProto = await Utils.queryEntry(
+      this.traceProcessor,
+      this.getTableName(),
+      index,
+    );
+    return this.layersSnapshotProtoTransformer.transform(snapshotProto);
+  }
+
   private makeHierarchyTree(
     snapshotProto: perfetto.protos.ILayersSnapshotProto,
     layerProtos: perfetto.protos.ILayerProto[],
diff --git a/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger_test.ts b/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger_test.ts
index 3dc5a7c..a93fafb 100644
--- a/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger_test.ts
+++ b/tools/winscope/src/parsers/surface_flinger/perfetto/parser_surface_flinger_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -51,62 +50,24 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LATEST);
     });
 
-    it('provides elapsed timestamps', () => {
+    it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14500282843n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14631249355n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15403446377n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089102062832n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089233029344n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090005226366n),
       ];
-      const actual = assertDefined(
-        parser.getTimestamps(TimestampType.ELAPSED),
-      ).slice(0, 3);
+      const actual = assertDefined(parser.getTimestamps()).slice(0, 3);
       expect(actual).toEqual(expected);
     });
 
-    it('provides real timestamps', () => {
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089102062832n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089233029344n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090005226366n),
-      ];
-      const actual = assertDefined(
-        parser.getTimestamps(TimestampType.REAL),
-      ).slice(0, 3);
-      expect(actual).toEqual(expected);
-    });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = await UnitTestUtils.getPerfettoParser(
-        TraceType.SURFACE_FLINGER,
-        'traces/perfetto/layers_trace.perfetto-trace',
-        true,
-      );
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.SURFACE_FLINGER,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14500282843n));
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        )[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126889102062832n),
-      );
-    });
-
     it('provides correct root entry node', async () => {
-      const entry = await parser.getEntry(1, TimestampType.REAL);
+      const entry = await parser.getEntry(1);
       expect(entry.id).toEqual('LayerTraceEntry root');
       expect(entry.name).toEqual('root');
     });
 
     it('decodes layer state flags', async () => {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       {
         const layer = assertDefined(
           entry.findDfs(UiTreeUtils.makeIdMatchFilter('27 Leaf:24:25#27')),
@@ -182,7 +143,7 @@
         TraceType.SURFACE_FLINGER,
         'traces/perfetto/layers_trace_with_duplicated_ids.perfetto-trace',
       );
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       expect(entry).toBeTruthy();
 
       const layer = assertDefined(
diff --git a/tools/winscope/src/parsers/transactions/legacy/parser_transactions.ts b/tools/winscope/src/parsers/transactions/legacy/parser_transactions.ts
index f4132cc..85ec513 100644
--- a/tools/winscope/src/parsers/transactions/legacy/parser_transactions.ts
+++ b/tools/winscope/src/parsers/transactions/legacy/parser_transactions.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {AddDefaults} from 'parsers/operations/add_defaults';
 import {SetFormatters} from 'parsers/operations/set_formatters';
@@ -51,7 +51,7 @@
     new TranslateChanges(),
   ];
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToMonotonicTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.TRANSACTIONS;
@@ -61,6 +61,14 @@
     return ParserTransactions.MAGIC_NUMBER;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return this.realToMonotonicTimeOffsetNs;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): android.surfaceflinger.proto.ITransactionTraceEntry[] {
@@ -71,36 +79,22 @@
     const timeOffset = BigInt(
       decodedProto.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToMonotonicTimeOffsetNs =
+      timeOffset !== 0n ? timeOffset : undefined;
 
     return decodedProto.entry ?? [];
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entryProto: android.surfaceflinger.proto.ITransactionTraceEntry,
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(
-      assertDefined(entryProto.elapsedRealtimeNanos).toString(),
+  ): Timestamp {
+    return this.timestampConverter.makeTimestampFromMonotonicNs(
+      BigInt(assertDefined(entryProto.elapsedRealtimeNanos).toString()),
     );
-    if (
-      this.timestampFactory.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return this.timestampFactory.makeTimestampFromType(
-        type,
-        elapsedRealtimeNanos,
-        this.realToElapsedTimeOffsetNs,
-      );
-    }
-    return undefined;
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entryProto: android.surfaceflinger.proto.ITransactionTraceEntry,
   ): PropertyTreeNode {
     return this.makePropertiesTree(entryProto);
diff --git a/tools/winscope/src/parsers/transactions/legacy/parser_transactions_test.ts b/tools/winscope/src/parsers/transactions/legacy/parser_transactions_test.ts
index c8f2678..6eb1818 100644
--- a/tools/winscope/src/parsers/transactions/legacy/parser_transactions_test.ts
+++ b/tools/winscope/src/parsers/transactions/legacy/parser_transactions_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -25,7 +24,7 @@
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 
 describe('ParserTransactions', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  describe('trace with real timestamps', () => {
     let parser: Parser<PropertyTreeNode>;
 
     beforeAll(async () => {
@@ -43,76 +42,27 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
-      const timestamps = assertDefined(
-        parser.getTimestamps(TimestampType.ELAPSED),
-      );
+    it('provides timestamps', () => {
+      const timestamps = assertDefined(parser.getTimestamps());
 
       expect(timestamps.length).toEqual(712);
 
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2450981445n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2517952515n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(4021151449n),
+        TimestampConverterUtils.makeRealTimestamp(1659507541051480997n),
+        TimestampConverterUtils.makeRealTimestamp(1659507541118452067n),
+        TimestampConverterUtils.makeRealTimestamp(1659507542621651001n),
       ];
       expect(timestamps.slice(0, 3)).toEqual(expected);
     });
 
-    it('provides real timestamps', () => {
-      const timestamps = assertDefined(
-        parser.getTimestamps(TimestampType.REAL),
-      );
-
-      expect(timestamps.length).toEqual(712);
-
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659507541051480997n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659507541118452067n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659507542621651001n),
-      ];
-      expect(timestamps.slice(0, 3)).toEqual(expected);
-    });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_and_real_timestamp/Transactions.pb',
-        true,
-      )) as Parser<PropertyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.TRANSACTIONS,
-      );
-
-      const expectedElapsed = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2450981445n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2517952515n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(4021151449n),
-      ];
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        ).slice(0, 3),
-      ).toEqual(expectedElapsed);
-
-      const expectedReal = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659527341051480997n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659527341118452067n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659527342621651001n),
-      ];
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        ).slice(0, 3),
-      ).toEqual(expectedReal);
-    });
-
-    it('retrieves trace entry from real timestamp', async () => {
-      const entry = await parser.getEntry(1, TimestampType.REAL);
+    it('retrieves trace entry from timestamp', async () => {
+      const entry = await parser.getEntry(1);
       expect(entry.id).toEqual('TransactionsTraceEntry entry');
     });
 
     it("decodes 'what' field in proto", async () => {
       {
-        const entry = await parser.getEntry(0, TimestampType.REAL);
+        const entry = await parser.getEntry(0);
         const transactions = assertDefined(
           entry.getChildByName('transactions'),
         );
@@ -136,7 +86,7 @@
         ).toEqual('eFlagsChanged | eDestinationFrameChanged');
       }
       {
-        const entry = await parser.getEntry(222, TimestampType.REAL);
+        const entry = await parser.getEntry(222);
         const transactions = assertDefined(
           entry.getChildByName('transactions'),
         );
@@ -167,7 +117,7 @@
     });
   });
 
-  describe('trace with elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<PropertyTreeNode>;
 
     beforeAll(async () => {
@@ -180,39 +130,17 @@
       expect(parser.getTraceType()).toEqual(TraceType.TRANSACTIONS);
     });
 
-    it('provides elapsed timestamps', () => {
-      const timestamps = assertDefined(
-        parser.getTimestamps(TimestampType.ELAPSED),
-      );
+    it('provides timestamps', () => {
+      const timestamps = assertDefined(parser.getTimestamps());
 
       expect(timestamps.length).toEqual(4997);
 
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14862317023n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14873423549n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14884850511n),
+        TimestampConverterUtils.makeElapsedTimestamp(14862317023n),
+        TimestampConverterUtils.makeElapsedTimestamp(14873423549n),
+        TimestampConverterUtils.makeElapsedTimestamp(14884850511n),
       ];
       expect(timestamps.slice(0, 3)).toEqual(expected);
     });
-
-    it('does not apply timezone info', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_timestamp/Transactions.pb',
-        true,
-      )) as Parser<PropertyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.TRANSACTIONS,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14862317023n));
-    });
-
-    it("doesn't provide real timestamps", () => {
-      expect(parser.getTimestamps(TimestampType.REAL)).toEqual(undefined);
-    });
   });
 });
diff --git a/tools/winscope/src/parsers/transactions/perfetto/parser_transactions.ts b/tools/winscope/src/parsers/transactions/perfetto/parser_transactions.ts
index fbed76d..90bc7f3 100644
--- a/tools/winscope/src/parsers/transactions/perfetto/parser_transactions.ts
+++ b/tools/winscope/src/parsers/transactions/perfetto/parser_transactions.ts
@@ -15,11 +15,11 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {AddDefaults} from 'parsers/operations/add_defaults';
 import {SetFormatters} from 'parsers/operations/set_formatters';
 import {AbstractParser} from 'parsers/perfetto/abstract_parser';
+import {FakeProto} from 'parsers/perfetto/fake_proto_builder';
 import {FakeProtoTransformer} from 'parsers/perfetto/fake_proto_transformer';
 import {Utils} from 'parsers/perfetto/utils';
 import {TamperedMessageType} from 'parsers/tampered_message_type';
@@ -57,9 +57,9 @@
   constructor(
     traceFile: TraceFile,
     traceProcessor: WasmEngineProxy,
-    timestampFactory: TimestampFactory,
+    timestampConverter: ParserTimestampConverter,
   ) {
-    super(traceFile, traceProcessor, timestampFactory);
+    super(traceFile, traceProcessor, timestampConverter);
 
     this.protoTransformer = new FakeProtoTransformer(
       assertDefined(
@@ -72,16 +72,17 @@
     return TraceType.TRANSACTIONS;
   }
 
-  override async getEntry(
-    index: number,
-    timestampType: TimestampType,
-  ): Promise<PropertyTreeNode> {
-    let entryProto = await Utils.queryEntry(
+  override async queryEntry(index: number): Promise<FakeProto> {
+    const entryProto = await Utils.queryEntry(
       this.traceProcessor,
       this.getTableName(),
       index,
     );
-    entryProto = this.protoTransformer.transform(entryProto);
+    return this.protoTransformer.transform(entryProto);
+  }
+
+  override async getEntry(index: number): Promise<PropertyTreeNode> {
+    const entryProto = await this.queryEntry(index);
     return this.makePropertiesTree(entryProto);
   }
 
diff --git a/tools/winscope/src/parsers/transactions/perfetto/parser_transactions_test.ts b/tools/winscope/src/parsers/transactions/perfetto/parser_transactions_test.ts
index 0f124eb..858113f 100644
--- a/tools/winscope/src/parsers/transactions/perfetto/parser_transactions_test.ts
+++ b/tools/winscope/src/parsers/transactions/perfetto/parser_transactions_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -43,66 +42,27 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LATEST);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
 
     expect(timestamps.length).toEqual(712);
 
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2450981445n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2517952515n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(4021151449n),
+      TimestampConverterUtils.makeRealTimestamp(1659507541051480997n),
+      TimestampConverterUtils.makeRealTimestamp(1659507541118452067n),
+      TimestampConverterUtils.makeRealTimestamp(1659507542621651001n),
     ];
     expect(timestamps.slice(0, 3)).toEqual(expected);
   });
 
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
-
-    expect(timestamps.length).toEqual(712);
-
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659507541051480997n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659507541118452067n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659507542621651001n),
-    ];
-    expect(timestamps.slice(0, 3)).toEqual(expected);
-  });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = await UnitTestUtils.getPerfettoParser(
-      TraceType.TRANSACTIONS,
-      'traces/perfetto/transactions_trace.perfetto-trace',
-      true,
-    );
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-      TraceType.TRANSACTIONS,
-    );
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      )[0],
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(2450981445n));
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659527341051480997n),
-    );
-  });
-
-  it('retrieves trace entry from real timestamp', async () => {
-    const entry = await parser.getEntry(1, TimestampType.REAL);
+  it('retrieves trace entry from timestamp', async () => {
+    const entry = await parser.getEntry(1);
     expect(entry.id).toEqual('TransactionsTraceEntry entry');
   });
 
   it('transforms fake proto built from trace processor args', async () => {
-    const entry0 = await parser.getEntry(0, TimestampType.REAL);
-    const entry2 = await parser.getEntry(2, TimestampType.REAL);
+    const entry0 = await parser.getEntry(0);
+    const entry2 = await parser.getEntry(2);
 
     // Add empty arrays
     expect(entry0.getChildByName('addedDisplays')?.getAllChildren()).toEqual(
@@ -162,7 +122,7 @@
 
   it("decodes 'what' field in proto", async () => {
     {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       const transactions = assertDefined(entry.getChildByName('transactions'));
       expect(
         transactions
@@ -183,7 +143,7 @@
       ).toEqual('eFlagsChanged | eDestinationFrameChanged');
     }
     {
-      const entry = await parser.getEntry(222, TimestampType.REAL);
+      const entry = await parser.getEntry(222);
       const transactions = assertDefined(entry.getChildByName('transactions'));
 
       expect(
diff --git a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell.ts b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell.ts
index 350230f..c3ac4a4 100644
--- a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell.ts
+++ b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {INVALID_TIME_NS, Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {ParserTransitionsUtils} from 'parsers/transitions/parser_transitions_utils';
 import root from 'protos/transitions/udc/json';
@@ -28,13 +27,21 @@
     'com.android.wm.shell.WmShellTransitionTraceProto',
   );
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToBootTimeOffsetNs: bigint | undefined;
   private handlerMapping: undefined | {[key: number]: string};
 
   override getTraceType(): TraceType {
     return TraceType.SHELL_TRANSITION;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(
     traceBuffer: Uint8Array,
   ): com.android.wm.shell.ITransition[] {
@@ -46,7 +53,7 @@
     const timeOffset = BigInt(
       decodedProto.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToBootTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
 
     this.handlerMapping = {};
     for (const mapping of decodedProto.handlerMappings ?? []) {
@@ -58,41 +65,20 @@
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entryProto: com.android.wm.shell.ITransition,
   ): PropertyTreeNode {
-    return this.makePropertiesTree(timestampType, entryProto);
+    return this.makePropertiesTree(entryProto);
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: com.android.wm.shell.ITransition,
-  ): undefined | Timestamp {
+  ): Timestamp {
     // for consistency with all transitions, elapsed nanos are defined as shell dispatch time else 0n
-    const decodedEntry = this.processDecodedEntry(0, type, entry);
-    const dispatchTimestamp: Timestamp | undefined = decodedEntry
-      .getChildByName('shellData')
-      ?.getChildByName('dispatchTimeNs')
-      ?.getValue();
-
-    if (type === TimestampType.REAL) {
-      if (dispatchTimestamp) {
-        return NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(
-          dispatchTimestamp.getValueNs(),
-        );
-      } else {
-        return this.timestampFactory.makeRealTimestamp(
-          this.realToElapsedTimeOffsetNs ?? 0n,
-        );
-      }
-    }
-
-    if (type === TimestampType.ELAPSED) {
-      const timestampNs = dispatchTimestamp?.getValueNs() ?? 0n;
-      return NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(timestampNs);
-    }
-
-    return undefined;
+    return entry.dispatchTimeNs
+      ? this.timestampConverter.makeTimestampFromBootTimeNs(
+          BigInt(entry.dispatchTimeNs.toString()),
+        )
+      : this.timestampConverter.makeTimestampFromBootTimeNs(INVALID_TIME_NS);
   }
 
   protected getMagicNumber(): number[] | undefined {
@@ -113,8 +99,8 @@
     ) {
       throw new Error('Requires at least one non-null timestamp');
     }
-    if (this.realToElapsedTimeOffsetNs === undefined) {
-      throw new Error('missing realToElapsedTimeOffsetNs');
+    if (this.realToBootTimeOffsetNs === undefined) {
+      throw new Error('missing realToBootTimeOffsetNs');
     }
     if (this.handlerMapping === undefined) {
       throw new Error('Missing handler mapping');
@@ -122,17 +108,15 @@
   }
 
   private makePropertiesTree(
-    timestampType: TimestampType,
     entryProto: com.android.wm.shell.ITransition,
   ): PropertyTreeNode {
     this.validateShellTransitionEntry(entryProto);
 
     const shellEntryTree = ParserTransitionsUtils.makeShellPropertiesTree({
       entry: entryProto,
-      realToElapsedTimeOffsetNs: this.realToElapsedTimeOffsetNs,
-      timestampType,
+      realToBootTimeOffsetNs: this.realToBootTimeOffsetNs,
       handlerMapping: this.handlerMapping,
-      timestampFactory: this.timestampFactory,
+      timestampConverter: this.timestampConverter,
     });
     const wmEntryTree = ParserTransitionsUtils.makeWmPropertiesTree();
 
diff --git a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell_test.ts b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell_test.ts
index ba566d9..fa76d31 100644
--- a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell_test.ts
+++ b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_shell_test.ts
@@ -15,8 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -41,55 +40,16 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(57649649922341n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(57651299086892n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
+      TimestampConverterUtils.makeRealTimestamp(1683188477607285317n),
+      TimestampConverterUtils.makeRealTimestamp(1683130827957362976n),
+      TimestampConverterUtils.makeRealTimestamp(1683130827957362976n),
+      TimestampConverterUtils.makeRealTimestamp(1683188479256449868n),
+      TimestampConverterUtils.makeRealTimestamp(1683130827957362976n),
+      TimestampConverterUtils.makeRealTimestamp(1683130827957362976n),
     ];
     expect(timestamps).toEqual(expected);
   });
-
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683188477607285317n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827957362976n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827957362976n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683188479256449868n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827957362976n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827957362976n),
-    ];
-    expect(timestamps).toEqual(expected);
-  });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-      'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
-      true,
-    )) as Parser<PropertyTreeNode>;
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-      TraceType.SHELL_TRANSITION,
-    );
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      )[0],
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(57649649922341n));
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683208277607285317n),
-    );
-  });
 });
diff --git a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm.ts b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm.ts
index 1e1eb21..77c4bd3 100644
--- a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm.ts
+++ b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
+import {INVALID_TIME_NS, Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {ParserTransitionsUtils} from 'parsers/transitions/parser_transitions_utils';
 import root from 'protos/transitions/udc/json';
@@ -27,18 +27,25 @@
     'com.android.server.wm.shell.TransitionTraceProto',
   );
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToBootTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.WM_TRANSITION;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entryProto: com.android.server.wm.shell.ITransition,
   ): PropertyTreeNode {
-    return this.makePropertiesTree(timestampType, entryProto);
+    return this.makePropertiesTree(entryProto);
   }
 
   override decodeTrace(
@@ -51,7 +58,7 @@
     const timeOffset = BigInt(
       decodedProto.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToBootTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
 
     return decodedProto.transitions ?? [];
   }
@@ -60,16 +67,11 @@
     return [0x09, 0x54, 0x52, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45]; // .TRNTRACE
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: com.android.server.wm.shell.ITransition,
-  ): undefined | Timestamp {
-    // for consistency with all transitions, elapsed nanos are defined as shell dispatch time else 0n
-    return this.timestampFactory.makeTimestampFromType(
-      type,
-      0n,
-      this.realToElapsedTimeOffsetNs,
-    );
+  ): Timestamp {
+    // for consistency with all transitions, elapsed nanos are defined as shell dispatch time else INVALID_TIME_NS
+    return this.timestampConverter.makeTimestampFromBootTimeNs(INVALID_TIME_NS);
   }
 
   private validateWmTransitionEntry(
@@ -86,13 +88,12 @@
     ) {
       throw new Error('Requires at least one non-null timestamp');
     }
-    if (this.realToElapsedTimeOffsetNs === undefined) {
-      throw new Error('missing realToElapsedTimeOffsetNs');
+    if (this.realToBootTimeOffsetNs === undefined) {
+      throw new Error('missing realToBootTimeOffsetNs');
     }
   }
 
   private makePropertiesTree(
-    timestampType: TimestampType,
     entryProto: com.android.server.wm.shell.ITransition,
   ): PropertyTreeNode {
     this.validateWmTransitionEntry(entryProto);
@@ -100,9 +101,8 @@
     const shellEntryTree = ParserTransitionsUtils.makeShellPropertiesTree();
     const wmEntryTree = ParserTransitionsUtils.makeWmPropertiesTree({
       entry: entryProto,
-      realToElapsedTimeOffsetNs: this.realToElapsedTimeOffsetNs,
-      timestampType,
-      timestampFactory: this.timestampFactory,
+      realToBootTimeOffsetNs: this.realToBootTimeOffsetNs,
+      timestampConverter: this.timestampConverter,
     });
 
     return ParserTransitionsUtils.makeTransitionPropertiesTree(
diff --git a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm_test.ts b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm_test.ts
index f97c400..7bdb84d 100644
--- a/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm_test.ts
+++ b/tools/winscope/src/parsers/transitions/legacy/parser_transitions_wm_test.ts
@@ -15,8 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -40,43 +39,11 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
-    expect(timestamps.length).toEqual(8);
-    const expected = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n);
-    timestamps.forEach((timestamp) => expect(timestamp).toEqual(expected));
-  });
-
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
     expect(timestamps.length).toEqual(8);
     const expected =
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827956652323n);
+      TimestampConverterUtils.makeRealTimestamp(1683130827956652323n);
     timestamps.forEach((timestamp) => expect(timestamp).toEqual(expected));
   });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-      'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
-      true,
-    )) as Parser<PropertyTreeNode>;
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-      TraceType.WM_TRANSITION,
-    );
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      )[0],
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n));
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683150627956652323n),
-    );
-  });
 });
diff --git a/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions.ts b/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions.ts
index cf3029b..edd0880 100644
--- a/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions.ts
+++ b/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {AbstractTracesParser} from 'parsers/legacy/abstract_traces_parser';
 import {ParserTransitionsUtils} from 'parsers/transitions/parser_transitions_utils';
 import {Trace} from 'trace/trace';
@@ -30,8 +30,8 @@
   private readonly descriptors: string[];
   private decodedEntries: PropertyTreeNode[] | undefined;
 
-  constructor(traces: Traces) {
-    super();
+  constructor(traces: Traces, timestampConverter: ParserTimestampConverter) {
+    super(timestampConverter);
     const wmTransitionTrace = traces.getTrace(TraceType.WM_TRANSITION);
     const shellTransitionTrace = traces.getTrace(TraceType.SHELL_TRANSITION);
     if (wmTransitionTrace && shellTransitionTrace) {
@@ -66,17 +66,14 @@
 
     this.decodedEntries = this.compressEntries(allEntries);
 
-    await this.parseTimestamps();
+    await this.createTimestamps();
   }
 
   override getLengthEntries(): number {
     return assertDefined(this.decodedEntries).length;
   }
 
-  override getEntry(
-    index: number,
-    timestampType: TimestampType,
-  ): Promise<PropertyTreeNode> {
+  override getEntry(index: number): Promise<PropertyTreeNode> {
     const entry = assertDefined(this.decodedEntries)[index];
     return Promise.resolve(entry);
   }
@@ -89,35 +86,31 @@
     return TraceType.TRANSITION;
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    decodedEntry: PropertyTreeNode,
-  ): undefined | Timestamp {
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  protected override getTimestamp(decodedEntry: PropertyTreeNode): Timestamp {
     // for consistency with all transitions, elapsed nanos are defined as shell dispatch time else 0n
     const shellData = decodedEntry.getChildByName('shellData');
     const dispatchTimestamp: Timestamp | undefined = shellData
       ?.getChildByName('dispatchTimeNs')
       ?.getValue();
 
-    const realToElapsedTimeOffsetNs: bigint =
+    const realToBootTimeOffsetNs: bigint =
       shellData
-        ?.getChildByName('realToElapsedTimeOffsetTimestamp')
+        ?.getChildByName('realToBootTimeOffsetTimestamp')
         ?.getValue()
         ?.getValueNs() ?? 0n;
 
     const timestampNs: bigint = dispatchTimestamp
       ? dispatchTimestamp.getValueNs()
-      : realToElapsedTimeOffsetNs;
-
-    if (type === TimestampType.ELAPSED) {
-      return NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(
-        timestampNs - realToElapsedTimeOffsetNs,
-      );
-    } else if (type === TimestampType.REAL) {
-      return NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(timestampNs);
-    }
-
-    return undefined;
+      : realToBootTimeOffsetNs;
+    return this.timestampConverter.makeTimestampFromRealNs(timestampNs);
   }
 
   private compressEntries(
diff --git a/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions_test.ts b/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions_test.ts
index 971e64b..8d4927b 100644
--- a/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions_test.ts
+++ b/tools/winscope/src/parsers/transitions/legacy/traces_parser_transitions_test.ts
@@ -15,8 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -42,51 +41,14 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
   });
 
-  it('provides elapsed timestamps', () => {
-    const timestamps = assertDefined(
-      parser.getTimestamps(TimestampType.ELAPSED),
-    );
+  it('provides timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps());
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(57649649922341n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(57651299086892n),
+      TimestampConverterUtils.makeRealTimestamp(1683130827957362976n),
+      TimestampConverterUtils.makeRealTimestamp(1683130827957362976n),
+      TimestampConverterUtils.makeRealTimestamp(1683188477606574664n),
+      TimestampConverterUtils.makeRealTimestamp(1683188479255739215n),
     ];
     expect(timestamps).toEqual(expected);
   });
-
-  it('provides real timestamps', () => {
-    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827957362976n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683130827957362976n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683188477607285317n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683188479256449868n),
-    ];
-    expect(timestamps).toEqual(expected);
-  });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = (await UnitTestUtils.getTracesParser(
-      [
-        'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
-        'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
-      ],
-      true,
-    )) as Parser<PropertyTreeNode>;
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(TraceType.TRANSITION);
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      )[0],
-    ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n));
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1683150627957362976n),
-    );
-  });
 });
diff --git a/tools/winscope/src/parsers/transitions/operations/add_duration.ts b/tools/winscope/src/parsers/transitions/operations/add_duration.ts
index b001453..022325a 100644
--- a/tools/winscope/src/parsers/transitions/operations/add_duration.ts
+++ b/tools/winscope/src/parsers/transitions/operations/add_duration.ts
@@ -16,8 +16,8 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {Timestamp} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TimeDuration} from 'common/time_duration';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {AddOperation} from 'trace/tree_node/operations/add_operation';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory';
@@ -39,10 +39,8 @@
       return [];
     }
 
-    const timeDiffNs = finishTime.minus(sendTime).getValueNs();
-
-    const timeDiff =
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(timeDiffNs);
+    const timeDiffNs = finishTime.minus(sendTime.getValueNs()).getValueNs();
+    const timeDiff = new TimeDuration(timeDiffNs);
 
     const durationNode =
       DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
@@ -50,7 +48,7 @@
         'duration',
         timeDiff,
       );
-    durationNode.setFormatter(TIMESTAMP_FORMATTER);
+    durationNode.setFormatter(TIMESTAMP_NODE_FORMATTER);
 
     return [durationNode];
   }
diff --git a/tools/winscope/src/parsers/transitions/operations/add_duration_test.ts b/tools/winscope/src/parsers/transitions/operations/add_duration_test.ts
index 79485a2..2145274 100644
--- a/tools/winscope/src/parsers/transitions/operations/add_duration_test.ts
+++ b/tools/winscope/src/parsers/transitions/operations/add_duration_test.ts
@@ -14,16 +14,17 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimeDuration} from 'common/time_duration';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {PropertySource} from 'trace/tree_node/property_tree_node';
 import {AddDuration} from './add_duration';
 
 describe('AddDuration', () => {
   let operation: AddDuration;
-  const TIMESTAMP_10 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n);
-  const TIMESTAMP_30 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(30n);
+  const TIMESTAMP_10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const TIMESTAMP_30 = TimestampConverterUtils.makeRealTimestamp(30n);
 
   beforeEach(() => {
     operation = new AddDuration();
@@ -59,9 +60,9 @@
         },
         {
           name: 'duration',
-          value: NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(20n),
+          value: new TimeDuration(20n),
           source: PropertySource.CALCULATED,
-          formatter: TIMESTAMP_FORMATTER,
+          formatter: TIMESTAMP_NODE_FORMATTER,
         },
       ])
       .build();
diff --git a/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp.ts b/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp.ts
index db1a8ae..0b9ba10 100644
--- a/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp.ts
+++ b/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp.ts
@@ -19,9 +19,9 @@
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory';
 
-export class AddRealToElapsedTimeOffsetTimestamp extends AddOperation<PropertyTreeNode> {
+export class AddRealToBootTimeOffsetTimestamp extends AddOperation<PropertyTreeNode> {
   constructor(
-    private readonly realToElapsedTimeOffsetTimestamp: Timestamp | undefined,
+    private readonly realToBootTimeOffsetTimestamp: Timestamp | undefined,
   ) {
     super();
   }
@@ -31,8 +31,8 @@
     const offsetNode =
       DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
         value.id,
-        'realToElapsedTimeOffsetTimestamp',
-        this.realToElapsedTimeOffsetTimestamp,
+        'realToBootTimeOffsetTimestamp',
+        this.realToBootTimeOffsetTimestamp,
       );
 
     return [offsetNode];
diff --git a/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp_test.ts b/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp_test.ts
index a886fc0..15b0484 100644
--- a/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp_test.ts
+++ b/tools/winscope/src/parsers/transitions/operations/add_real_to_elapsed_time_offset_timestamp_test.ts
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {PropertySource} from 'trace/tree_node/property_tree_node';
-import {AddRealToElapsedTimeOffsetTimestamp} from './add_real_to_elapsed_time_offset_timestamp';
+import {AddRealToBootTimeOffsetTimestamp} from './add_real_to_elapsed_time_offset_timestamp';
 
-describe('AddRealToElapsedTimeOffsetTimestamp', () => {
+describe('AddRealToBootTimeOffsetTimestamp', () => {
   it('adds undefined offset', () => {
     const propertyRoot = new PropertyTreeBuilder()
       .setIsRoot(true)
@@ -33,21 +33,23 @@
       .setName('transition')
       .setChildren([
         {
-          name: 'realToElapsedTimeOffsetTimestamp',
+          name: 'realToBootTimeOffsetTimestamp',
           value: undefined,
           source: PropertySource.CALCULATED,
         },
       ])
       .build();
 
-    const operation = new AddRealToElapsedTimeOffsetTimestamp(undefined);
+    const operation = new AddRealToBootTimeOffsetTimestamp(undefined);
     operation.apply(propertyRoot);
     expect(propertyRoot).toEqual(expectedRoot);
   });
 
   it('adds offset timestamp', () => {
-    const realToElapsedTimeOffsetTimestamp =
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(12345n);
+    const realToBootTimeOffsetTimestamp =
+      TimestampConverterUtils.TIMESTAMP_CONVERTER.makeTimestampFromMonotonicNs(
+        12345n,
+      );
     const propertyRoot = new PropertyTreeBuilder()
       .setIsRoot(true)
       .setRootId('TransitionsTraceEntry')
@@ -60,15 +62,15 @@
       .setName('transition')
       .setChildren([
         {
-          name: 'realToElapsedTimeOffsetTimestamp',
-          value: realToElapsedTimeOffsetTimestamp,
+          name: 'realToBootTimeOffsetTimestamp',
+          value: realToBootTimeOffsetTimestamp,
           source: PropertySource.CALCULATED,
         },
       ])
       .build();
 
-    const operation = new AddRealToElapsedTimeOffsetTimestamp(
-      realToElapsedTimeOffsetTimestamp,
+    const operation = new AddRealToBootTimeOffsetTimestamp(
+      realToBootTimeOffsetTimestamp,
     );
     operation.apply(propertyRoot);
     expect(propertyRoot).toEqual(expectedRoot);
diff --git a/tools/winscope/src/parsers/transitions/operations/add_status_test.ts b/tools/winscope/src/parsers/transitions/operations/add_status_test.ts
index 470cbe2..38eef98 100644
--- a/tools/winscope/src/parsers/transitions/operations/add_status_test.ts
+++ b/tools/winscope/src/parsers/transitions/operations/add_status_test.ts
@@ -14,15 +14,15 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {PropertySource} from 'trace/tree_node/property_tree_node';
 import {AddStatus} from './add_status';
 
 describe('AddStatus', () => {
   let operation: AddStatus;
-  const time0 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n);
-  const time10 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n);
+  const time0 = TimestampConverterUtils.makeRealTimestamp(0n);
+  const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
 
   beforeEach(() => {
     operation = new AddStatus();
diff --git a/tools/winscope/src/parsers/transitions/parser_transitions_utils.ts b/tools/winscope/src/parsers/transitions/parser_transitions_utils.ts
index ed4e527..ed7570a 100644
--- a/tools/winscope/src/parsers/transitions/parser_transitions_utils.ts
+++ b/tools/winscope/src/parsers/transitions/parser_transitions_utils.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {AddDefaults} from 'parsers/operations/add_defaults';
 import {SetFormatters} from 'parsers/operations/set_formatters';
 import {
@@ -30,12 +30,12 @@
 import {
   EnumFormatter,
   PropertyFormatter,
-  TIMESTAMP_FORMATTER,
+  TIMESTAMP_NODE_FORMATTER,
 } from 'trace/tree_node/formatters';
 import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {AddDuration} from './operations/add_duration';
-import {AddRealToElapsedTimeOffsetTimestamp} from './operations/add_real_to_elapsed_time_offset_timestamp';
+import {AddRealToBootTimeOffsetTimestamp} from './operations/add_real_to_elapsed_time_offset_timestamp';
 import {AddRootProperties} from './operations/add_root_properties';
 import {AddStatus} from './operations/add_status';
 import {UpdateAbortTimeNodes} from './operations/update_abort_time_nodes';
@@ -46,9 +46,8 @@
     | com.android.server.wm.shell.ITransition
     | com.android.wm.shell.ITransition
     | perfetto.protos.IShellTransition;
-  realToElapsedTimeOffsetNs: bigint | undefined;
-  timestampType: TimestampType;
-  timestampFactory: TimestampFactory;
+  realToBootTimeOffsetNs: bigint | undefined;
+  timestampConverter: ParserTimestampConverter;
   handlerMapping?: {[key: number]: string};
 }
 
@@ -122,19 +121,19 @@
       );
     }
 
-    const realToElapsedTimeOffsetTimestamp =
-      info.realToElapsedTimeOffsetNs !== undefined
-        ? info.timestampFactory.makeTimestampFromType(
-            info.timestampType,
-            info.realToElapsedTimeOffsetNs,
-            0n,
-          )
-        : undefined;
+    let realToBootTimeOffsetTimestamp: Timestamp | undefined;
+
+    if (info.realToBootTimeOffsetNs !== undefined) {
+      realToBootTimeOffsetTimestamp =
+        info.timestampConverter.makeTimestampFromRealNs(
+          info.realToBootTimeOffsetNs,
+        );
+    }
 
     const wmDataNode = assertDefined(tree.getChildByName('wmData'));
-    new AddRealToElapsedTimeOffsetTimestamp(
-      realToElapsedTimeOffsetTimestamp,
-    ).apply(wmDataNode);
+    new AddRealToBootTimeOffsetTimestamp(realToBootTimeOffsetTimestamp).apply(
+      wmDataNode,
+    );
     ParserTransitionsUtils.WM_ADD_DEFAULTS_OPERATION.apply(wmDataNode);
     new TransformToTimestamp(
       [
@@ -144,38 +143,23 @@
         'finishTimeNs',
         'startingWindowRemoveTimeNs',
       ],
-      ParserTransitionsUtils.makeTimestampStrategy(info),
+      ParserTransitionsUtils.makeTimestampStrategy(info.timestampConverter),
     ).apply(wmDataNode);
 
     const customFormatters = new Map<string, PropertyFormatter>([
       ['type', ParserTransitionsUtils.TRANSITION_TYPE_FORMATTER],
       ['mode', ParserTransitionsUtils.TRANSITION_TYPE_FORMATTER],
-      ['abortTimeNs', TIMESTAMP_FORMATTER],
-      ['createTimeNs', TIMESTAMP_FORMATTER],
-      ['sendTimeNs', TIMESTAMP_FORMATTER],
-      ['finishTimeNs', TIMESTAMP_FORMATTER],
-      ['startingWindowRemoveTimeNs', TIMESTAMP_FORMATTER],
+      ['abortTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['createTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['sendTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['finishTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['startingWindowRemoveTimeNs', TIMESTAMP_NODE_FORMATTER],
     ]);
 
     new SetFormatters(undefined, customFormatters).apply(tree);
     return tree;
   }
 
-  private static makeTimestampStrategy(
-    info: TransitionInfo,
-  ): MakeTimestampStrategyType {
-    if (info.timestampType === TimestampType.REAL) {
-      return (valueNs: bigint) => {
-        return info.timestampFactory.makeRealTimestamp(
-          valueNs,
-          info.realToElapsedTimeOffsetNs,
-        );
-      };
-    } else {
-      return info.timestampFactory.makeElapsedTimestamp;
-    }
-  }
-
   static makeShellPropertiesTree(
     info?: TransitionInfo,
     denylistProperties: string[] = [],
@@ -199,31 +183,30 @@
       );
     }
 
-    const realToElapsedTimeOffsetTimestamp =
-      info.realToElapsedTimeOffsetNs !== undefined
-        ? info.timestampFactory.makeTimestampFromType(
-            info.timestampType,
-            info.realToElapsedTimeOffsetNs,
-            0n,
-          )
-        : undefined;
+    let realToBootTimeOffsetTimestamp: Timestamp | undefined;
+    if (info.realToBootTimeOffsetNs !== undefined) {
+      realToBootTimeOffsetTimestamp =
+        info.timestampConverter.makeTimestampFromRealNs(
+          info.realToBootTimeOffsetNs,
+        );
+    }
 
     const shellDataNode = assertDefined(tree.getChildByName('shellData'));
-    new AddRealToElapsedTimeOffsetTimestamp(
-      realToElapsedTimeOffsetTimestamp,
-    ).apply(shellDataNode);
+    new AddRealToBootTimeOffsetTimestamp(realToBootTimeOffsetTimestamp).apply(
+      shellDataNode,
+    );
     new TransformToTimestamp(
       ['dispatchTimeNs', 'mergeRequestTimeNs', 'mergeTimeNs', 'abortTimeNs'],
-      ParserTransitionsUtils.makeTimestampStrategy(info),
+      ParserTransitionsUtils.makeTimestampStrategy(info.timestampConverter),
     ).apply(shellDataNode);
 
     const customFormatters = new Map<string, PropertyFormatter>([
       ['type', ParserTransitionsUtils.TRANSITION_TYPE_FORMATTER],
       ['mode', ParserTransitionsUtils.TRANSITION_TYPE_FORMATTER],
-      ['dispatchTimeNs', TIMESTAMP_FORMATTER],
-      ['mergeRequestTimeNs', TIMESTAMP_FORMATTER],
-      ['mergeTimeNs', TIMESTAMP_FORMATTER],
-      ['abortTimeNs', TIMESTAMP_FORMATTER],
+      ['dispatchTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['mergeRequestTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['mergeTimeNs', TIMESTAMP_NODE_FORMATTER],
+      ['abortTimeNs', TIMESTAMP_NODE_FORMATTER],
     ]);
 
     if (info.handlerMapping) {
@@ -234,4 +217,12 @@
 
     return tree;
   }
+
+  private static makeTimestampStrategy(
+    timestampConverter: ParserTimestampConverter,
+  ): MakeTimestampStrategyType {
+    return (valueNs: bigint) => {
+      return timestampConverter.makeTimestampFromBootTimeNs(valueNs);
+    };
+  }
 }
diff --git a/tools/winscope/src/parsers/transitions/perfetto/parser_transitions.ts b/tools/winscope/src/parsers/transitions/perfetto/parser_transitions.ts
index bdaecde..8d6c26e 100644
--- a/tools/winscope/src/parsers/transitions/perfetto/parser_transitions.ts
+++ b/tools/winscope/src/parsers/transitions/perfetto/parser_transitions.ts
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
 import {AbstractParser} from 'parsers/perfetto/abstract_parser';
 import {FakeProtoBuilder} from 'parsers/perfetto/fake_proto_builder';
 import {ParserTransitionsUtils} from 'parsers/transitions/parser_transitions_utils';
@@ -29,12 +28,8 @@
     return TraceType.TRANSITION;
   }
 
-  override async getEntry(
-    index: number,
-    timestampType: TimestampType,
-  ): Promise<PropertyTreeNode> {
-    const transitionProto = await this.queryTransition(index);
-
+  override async getEntry(index: number): Promise<PropertyTreeNode> {
+    const transitionProto = await this.queryEntry(index);
     if (this.handlerIdToName === undefined) {
       const handlers = await this.queryHandlers();
       this.handlerIdToName = {};
@@ -42,26 +37,56 @@
         (it) => (assertDefined(this.handlerIdToName)[it.id] = it.name),
       );
     }
-
-    return this.makePropertiesTree(timestampType, transitionProto);
+    return this.makePropertiesTree(transitionProto);
   }
 
   protected override getTableName(): string {
     return 'window_manager_shell_transitions';
   }
 
+  protected override async queryEntry(
+    index: number,
+  ): Promise<perfetto.protos.ShellTransition> {
+    const protoBuilder = new FakeProtoBuilder();
+
+    const sql = `
+      SELECT
+        transitions.transition_id,
+        args.key,
+        args.value_type,
+        args.int_value,
+        args.string_value,
+        args.real_value
+      FROM
+        window_manager_shell_transitions as transitions
+        INNER JOIN args ON transitions.arg_set_id = args.arg_set_id
+      WHERE transitions.id = ${index};
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    for (const it = result.iter({}); it.valid(); it.next()) {
+      protoBuilder.addArg(
+        it.get('key') as string,
+        it.get('value_type') as string,
+        it.get('int_value') as bigint | undefined,
+        it.get('real_value') as number | undefined,
+        it.get('string_value') as string | undefined,
+      );
+    }
+
+    return protoBuilder.build();
+  }
+
   private makePropertiesTree(
-    timestampType: TimestampType,
     transitionProto: perfetto.protos.ShellTransition,
   ): PropertyTreeNode {
     this.validatePerfettoTransition(transitionProto);
 
     const perfettoTransitionInfo = {
       entry: transitionProto,
-      realToElapsedTimeOffsetNs: assertDefined(this.realToElapsedTimeOffsetNs),
-      timestampType,
+      realToBootTimeOffsetNs: undefined,
       handlerMapping: this.handlerIdToName,
-      timestampFactory: this.timestampFactory,
+      timestampConverter: this.timestampConverter,
     };
 
     const shellEntryTree = ParserTransitionsUtils.makeShellPropertiesTree(
@@ -97,39 +122,6 @@
     );
   }
 
-  private async queryTransition(
-    index: number,
-  ): Promise<perfetto.protos.ShellTransition> {
-    const protoBuilder = new FakeProtoBuilder();
-
-    const sql = `
-      SELECT
-        transitions.transition_id,
-        args.key,
-        args.value_type,
-        args.int_value,
-        args.string_value,
-        args.real_value
-      FROM
-        window_manager_shell_transitions as transitions
-        INNER JOIN args ON transitions.arg_set_id = args.arg_set_id
-      WHERE transitions.id = ${index};
-    `;
-    const result = await this.traceProcessor.query(sql).waitAllRows();
-
-    for (const it = result.iter({}); it.valid(); it.next()) {
-      protoBuilder.addArg(
-        it.get('key') as string,
-        it.get('value_type') as string,
-        it.get('int_value') as bigint | undefined,
-        it.get('real_value') as number | undefined,
-        it.get('string_value') as string | undefined,
-      );
-    }
-
-    return protoBuilder.build();
-  }
-
   private async queryHandlers(): Promise<TransitionHandler[]> {
     const sql =
       'SELECT handler_id, handler_name FROM window_manager_shell_transition_handlers;';
diff --git a/tools/winscope/src/parsers/transitions/perfetto/parser_transitions_test.ts b/tools/winscope/src/parsers/transitions/perfetto/parser_transitions_test.ts
index c773f12..f5d4e79 100644
--- a/tools/winscope/src/parsers/transitions/perfetto/parser_transitions_test.ts
+++ b/tools/winscope/src/parsers/transitions/perfetto/parser_transitions_test.ts
@@ -13,9 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
@@ -42,45 +42,31 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LATEST);
     });
 
-    it('provides elapsed timestamps', () => {
+    it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(479602824452n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(480676958445n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(487195167758n),
+        TimestampConverterUtils.makeRealTimestamp(1700573425448299306n),
+        TimestampConverterUtils.makeRealTimestamp(1700573426522433299n),
+        TimestampConverterUtils.makeRealTimestamp(1700573433040642612n),
       ];
-      const actual = assertDefined(
-        parser.getTimestamps(TimestampType.ELAPSED),
-      ).slice(0, 3);
-      expect(actual).toEqual(expected);
-    });
-
-    it('provides real timestamps', () => {
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1700573903102738218n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1700573904176872211n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1700573910695081524n),
-      ];
-      const actual = assertDefined(
-        parser.getTimestamps(TimestampType.REAL),
-      ).slice(0, 3);
+      const actual = assertDefined(parser.getTimestamps()).slice(0, 3);
       expect(actual).toEqual(expected);
     });
 
     it('decodes transition properties', async () => {
-      const entry = await parser.getEntry(0, TimestampType.REAL);
+      const entry = await parser.getEntry(0);
       const wmDataNode = assertDefined(entry.getChildByName('wmData'));
       const shellDataNode = assertDefined(entry.getChildByName('shellData'));
 
       expect(entry.getChildByName('id')?.getValue()).toEqual(32n);
       expect(
         wmDataNode.getChildByName('createTimeNs')?.formattedValue(),
-      ).toEqual('2023-11-21T13:38:23.083364560');
+      ).toEqual('2023-11-21T13:30:25.428925648');
       expect(wmDataNode.getChildByName('sendTimeNs')?.formattedValue()).toEqual(
-        '2023-11-21T13:38:23.096319557',
+        '2023-11-21T13:30:25.441880645',
       );
       expect(
         wmDataNode.getChildByName('finishTimeNs')?.formattedValue(),
-      ).toEqual('2023-11-21T13:38:23.624691628');
+      ).toEqual('2023-11-21T13:30:25.970252716');
       expect(entry.getChildByName('merged')?.getValue()).toBeFalse();
       expect(entry.getChildByName('played')?.getValue()).toBeTrue();
       expect(entry.getChildByName('aborted')?.getValue()).toBeFalse();
@@ -89,7 +75,7 @@
         assertDefined(
           wmDataNode.getChildByName('startingWindowRemoveTimeNs'),
         ).formattedValue(),
-      ).toEqual('2023-11-21T13:38:23.219566424');
+      ).toEqual('2023-11-21T13:30:25.565127512');
       expect(
         assertDefined(
           wmDataNode.getChildByName('startTransactionId'),
@@ -125,7 +111,7 @@
         assertDefined(
           shellDataNode.getChildByName('dispatchTimeNs'),
         ).formattedValue(),
-      ).toEqual('2023-11-21T13:38:23.102738218');
+      ).toEqual('2023-11-21T13:30:25.448299306');
       expect(shellDataNode.getChildByName('mergeRequestTime')).toBeUndefined();
       expect(shellDataNode.getChildByName('mergeTime')).toBeUndefined();
       expect(shellDataNode.getChildByName('abortTimeNs')).toBeUndefined();
@@ -134,58 +120,5 @@
         assertDefined(shellDataNode.getChildByName('handler')).formattedValue(),
       ).toEqual('com.android.wm.shell.transition.DefaultMixedHandler');
     });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = await UnitTestUtils.getPerfettoParser(
-        TraceType.TRANSITION,
-        'traces/perfetto/shell_transitions_trace.perfetto-trace',
-        true,
-      );
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.TRANSITION,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(479602824452n));
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        )[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1700593703102738218n),
-      );
-
-      const entry = await parserWithTimezoneInfo.getEntry(
-        0,
-        TimestampType.REAL,
-      );
-      const wmDataNode = assertDefined(entry.getChildByName('wmData'));
-      const shellDataNode = assertDefined(entry.getChildByName('shellData'));
-
-      expect(
-        wmDataNode.getChildByName('createTimeNs')?.formattedValue(),
-      ).toEqual('2023-11-21T19:08:23.083364560');
-      expect(wmDataNode.getChildByName('sendTimeNs')?.formattedValue()).toEqual(
-        '2023-11-21T19:08:23.096319557',
-      );
-      expect(
-        wmDataNode.getChildByName('finishTimeNs')?.formattedValue(),
-      ).toEqual('2023-11-21T19:08:23.624691628');
-
-      expect(
-        assertDefined(
-          wmDataNode.getChildByName('startingWindowRemoveTimeNs'),
-        ).formattedValue(),
-      ).toEqual('2023-11-21T19:08:23.219566424');
-
-      expect(
-        assertDefined(
-          shellDataNode.getChildByName('dispatchTimeNs'),
-        ).formattedValue(),
-      ).toEqual('2023-11-21T19:08:23.102738218');
-    });
   });
 });
diff --git a/tools/winscope/src/parsers/view_capture/parser_view_capture.ts b/tools/winscope/src/parsers/view_capture/parser_view_capture.ts
index b0f9828..6de9409 100644
--- a/tools/winscope/src/parsers/view_capture/parser_view_capture.ts
+++ b/tools/winscope/src/parsers/view_capture/parser_view_capture.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {ParsingUtils} from 'parsers/legacy/parsing_utils';
 import {com} from 'protos/viewcapture/latest/static';
 import {Parser} from 'trace/parser';
@@ -30,7 +30,7 @@
 
   constructor(
     private readonly traceFile: TraceFile,
-    private readonly timestampFactory: TimestampFactory,
+    private readonly timestampConverter: ParserTimestampConverter,
   ) {}
 
   async parse() {
@@ -44,7 +44,7 @@
       traceBuffer,
     ) as com.android.app.viewcapture.data.IExportedData;
 
-    const realToElapsedTimeOffsetNs = BigInt(
+    const realToBootTimeOffsetNs = BigInt(
       assertDefined(exportedData.realToElapsedTimeOffsetNanos).toString(),
     );
 
@@ -55,10 +55,10 @@
             [this.traceFile.getDescriptor()],
             windowData.frameData ?? [],
             ParserViewCapture.toTraceType(windowData),
-            realToElapsedTimeOffsetNs,
+            realToBootTimeOffsetNs,
             assertDefined(exportedData.package),
             assertDefined(exportedData.classname),
-            this.timestampFactory,
+            this.timestampConverter,
           ),
         ),
     );
diff --git a/tools/winscope/src/parsers/view_capture/parser_view_capture_test.ts b/tools/winscope/src/parsers/view_capture/parser_view_capture_test.ts
index 4dd3799..4cacfe2 100644
--- a/tools/winscope/src/parsers/view_capture/parser_view_capture_test.ts
+++ b/tools/winscope/src/parsers/view_capture/parser_view_capture_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -50,56 +49,17 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
   });
 
-  it('provides elapsed timestamps', () => {
+  it('provides timestamps', () => {
     const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(181114412436130n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(181114421012750n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(181114429047540n),
+      TimestampConverterUtils.makeRealTimestamp(1691692936292808460n),
+      TimestampConverterUtils.makeRealTimestamp(1691692936301385080n),
+      TimestampConverterUtils.makeRealTimestamp(1691692936309419870n),
     ];
-    expect(
-      assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3),
-    ).toEqual(expected);
-  });
-
-  it('provides real timestamps', () => {
-    const expected = [
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1691692936292808460n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1691692936301385080n),
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1691692936309419870n),
-    ];
-    expect(
-      assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3),
-    ).toEqual(expected);
-  });
-
-  it('applies timezone info to real timestamps only', async () => {
-    const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-      'traces/elapsed_and_real_timestamp/com.google.android.apps.nexuslauncher_0.vc',
-      true,
-    )) as Parser<HierarchyTreeNode>;
-    expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-      TraceType.VIEW_CAPTURE_TASKBAR_DRAG_LAYER,
-    );
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(181114412436130n),
-    );
-
-    expect(
-      assertDefined(
-        parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-      )[0],
-    ).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1691712736292808460n),
-    );
+    expect(assertDefined(parser.getTimestamps()).slice(0, 3)).toEqual(expected);
   });
 
   it('retrieves trace entry', async () => {
-    const entry = await parser.getEntry(1, TimestampType.REAL);
+    const entry = await parser.getEntry(1);
     expect(entry.id).toEqual(
       'ViewNode com.android.launcher3.taskbar.TaskbarDragLayer@265160962',
     );
diff --git a/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts b/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts
index 3d45330..49e4f13 100644
--- a/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts
+++ b/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts
@@ -15,8 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {TimestampFactory} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {ParserTimestampConverter} from 'common/timestamp_converter';
 import {AddDefaults} from 'parsers/operations/add_defaults';
 import {SetFormatters} from 'parsers/operations/set_formatters';
 import {TranslateIntDef} from 'parsers/operations/translate_intdef';
@@ -82,16 +82,16 @@
     SetRootTransformProperties: new SetRootTransformProperties(),
   };
 
-  private timestamps = new Map<TimestampType, Timestamp[]>();
+  private timestamps: Timestamp[] | undefined;
 
   constructor(
     private readonly descriptors: string[],
     private readonly frameData: com.android.app.viewcapture.data.IFrameData[],
     private readonly traceType: TraceType,
-    private readonly realToElapsedTimeOffsetNs: bigint,
+    private readonly realToBootTimeOffsetNs: bigint,
     private readonly packageName: string,
     private readonly classNames: string[],
-    private readonly timestampFactory: TimestampFactory,
+    private readonly timestampConverter: ParserTimestampConverter,
   ) {
     /*
       TODO: Enable this once multiple ViewCapture Tabs becomes generic. Right now it doesn't matter since
@@ -101,11 +101,10 @@
         windowData.title
       )}`;
       */
-    this.parse();
   }
 
   parse() {
-    this.timestamps = this.decodeTimestamps();
+    throw new Error('Not implemented');
   }
 
   getTraceType(): TraceType {
@@ -120,11 +119,23 @@
     return this.frameData.length;
   }
 
-  getTimestamps(type: TimestampType): Timestamp[] | undefined {
-    return this.timestamps.get(type);
+  getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
   }
 
-  getEntry(index: number, _: TimestampType): Promise<HierarchyTreeNode> {
+  getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  createTimestamps() {
+    this.timestamps = this.decodeTimestamps();
+  }
+
+  getTimestamps(): Timestamp[] | undefined {
+    return this.timestamps;
+  }
+
+  getEntry(index: number): Promise<HierarchyTreeNode> {
     const tree = this.makeHierarchyTree(this.frameData[index]);
     return Promise.resolve(tree);
   }
@@ -144,35 +155,12 @@
     return this.descriptors;
   }
 
-  private decodeTimestamps(): Map<TimestampType, Timestamp[]> {
-    const timestampMap = new Map<TimestampType, Timestamp[]>();
-    for (const type of [TimestampType.ELAPSED, TimestampType.REAL]) {
-      const timestamps: Timestamp[] = [];
-      let areTimestampsValid = true;
-
-      for (const entry of this.frameData) {
-        const timestampNs = BigInt(assertDefined(entry.timestamp).toString());
-
-        let timestamp: Timestamp | undefined;
-        if (this.timestampFactory.canMakeTimestampFromType(type, 0n)) {
-          timestamp = this.timestampFactory.makeTimestampFromType(
-            type,
-            timestampNs,
-            this.realToElapsedTimeOffsetNs,
-          );
-        }
-        if (timestamp === undefined) {
-          areTimestampsValid = false;
-          break;
-        }
-        timestamps.push(timestamp);
-      }
-
-      if (areTimestampsValid) {
-        timestampMap.set(type, timestamps);
-      }
-    }
-    return timestampMap;
+  private decodeTimestamps(): Timestamp[] {
+    return this.frameData.map((entry) =>
+      this.timestampConverter.makeTimestampFromBootTimeNs(
+        BigInt(assertDefined(entry.timestamp).toString()),
+      ),
+    );
   }
 
   private makeHierarchyTree(
diff --git a/tools/winscope/src/parsers/window_manager/parser_window_manager.ts b/tools/winscope/src/parsers/window_manager/parser_window_manager.ts
index 317f1f7..44f646d 100644
--- a/tools/winscope/src/parsers/window_manager/parser_window_manager.ts
+++ b/tools/winscope/src/parsers/window_manager/parser_window_manager.ts
@@ -15,7 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {com} from 'protos/windowmanager/latest/static';
 import {
@@ -38,7 +38,7 @@
     0x09, 0x57, 0x49, 0x4e, 0x54, 0x52, 0x41, 0x43, 0x45,
   ]; // .WINTRACE
 
-  private realToElapsedTimeOffsetNs: undefined | bigint;
+  private realToBootTimeOffsetNs: bigint | undefined;
 
   override getTraceType(): TraceType {
     return TraceType.WINDOW_MANAGER;
@@ -48,6 +48,14 @@
     return ParserWindowManager.MAGIC_NUMBER;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.realToBootTimeOffsetNs;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): com.android.server.wm.IWindowManagerTraceProto[] {
@@ -57,35 +65,20 @@
     const timeOffset = BigInt(
       decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0',
     );
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    this.realToBootTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
     return decoded.entry ?? [];
   }
 
-  override getTimestamp(
-    type: TimestampType,
+  protected override getTimestamp(
     entry: com.android.server.wm.IWindowManagerTraceProto,
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(
-      assertDefined(entry.elapsedRealtimeNanos).toString(),
+  ): Timestamp {
+    return this.timestampConverter.makeTimestampFromBootTimeNs(
+      BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
-    if (
-      this.timestampFactory.canMakeTimestampFromType(
-        type,
-        this.realToElapsedTimeOffsetNs,
-      )
-    ) {
-      return this.timestampFactory.makeTimestampFromType(
-        type,
-        elapsedRealtimeNanos,
-        this.realToElapsedTimeOffsetNs,
-      );
-    }
-    return undefined;
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entry: com.android.server.wm.IWindowManagerTraceProto,
   ): HierarchyTreeNode {
     return this.makeHierarchyTree(entry);
diff --git a/tools/winscope/src/parsers/window_manager/parser_window_manager_dump.ts b/tools/winscope/src/parsers/window_manager/parser_window_manager_dump.ts
index deacbed..f6ec063 100644
--- a/tools/winscope/src/parsers/window_manager/parser_window_manager_dump.ts
+++ b/tools/winscope/src/parsers/window_manager/parser_window_manager_dump.ts
@@ -15,8 +15,7 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
 import {com} from 'protos/windowmanager/latest/static';
 import {
@@ -43,6 +42,14 @@
     return undefined;
   }
 
+  override getRealToBootTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
+  override getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return undefined;
+  }
+
   override decodeTrace(
     buffer: Uint8Array,
   ): com.android.server.wm.IWindowManagerServiceDumpProto[] {
@@ -55,28 +62,19 @@
     // sure that a trace entry can actually be created from the decoded proto.
     // If the trace entry creation fails, an exception is thrown and the parser
     // will be considered unsuited for this input data.
-    this.processDecodedEntry(
-      0,
-      TimestampType.ELAPSED /*irrelevant for dump*/,
-      entryProto,
-    );
+    this.processDecodedEntry(0, entryProto);
 
     return [entryProto];
   }
 
-  override getTimestamp(
-    type: TimestampType,
-    entryProto: any,
-  ): undefined | Timestamp {
-    if (NO_TIMEZONE_OFFSET_FACTORY.canMakeTimestampFromType(type, 0n)) {
-      return NO_TIMEZONE_OFFSET_FACTORY.makeTimestampFromType(type, 0n, 0n);
-    }
-    return undefined;
+  protected override getTimestamp(
+    entryProto: com.android.server.wm.IWindowManagerServiceDumpProto,
+  ): Timestamp {
+    return this.timestampConverter.makeZeroTimestamp();
   }
 
   override processDecodedEntry(
     index: number,
-    timestampType: TimestampType,
     entryProto: com.android.server.wm.IWindowManagerServiceDumpProto,
   ): HierarchyTreeNode {
     return this.makeHierarchyTree(entryProto);
diff --git a/tools/winscope/src/parsers/window_manager/parser_window_manager_dump_test.ts b/tools/winscope/src/parsers/window_manager/parser_window_manager_dump_test.ts
index 76fc8eb..4c585f4 100644
--- a/tools/winscope/src/parsers/window_manager/parser_window_manager_dump_test.ts
+++ b/tools/winscope/src/parsers/window_manager/parser_window_manager_dump_test.ts
@@ -13,8 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -29,6 +29,7 @@
   let trace: Trace<HierarchyTreeNode>;
 
   beforeAll(async () => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
     parser = (await UnitTestUtils.getParser(
       'traces/dump_WindowManager.pb',
     )) as Parser<HierarchyTreeNode>;
@@ -46,35 +47,27 @@
     expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
   });
 
-  it('provides elapsed timestamp (always zero)', () => {
-    const expected = [NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n)];
-    expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(expected);
-  });
-
-  it('provides real timestamp (always zero)', () => {
-    const expected = [NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n)];
-    expect(parser.getTimestamps(TimestampType.REAL)).toEqual(expected);
+  it('provides timestamp (always zero)', () => {
+    const expected = [TimestampConverterUtils.makeElapsedTimestamp(0n)];
+    expect(parser.getTimestamps()).toEqual(expected);
   });
 
   it('does not apply timezone info', async () => {
     const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
       'traces/dump_WindowManager.pb',
-      true,
+      UnitTestUtils.getTimestampConverter(true),
     )) as Parser<HierarchyTreeNode>;
     expect(parserWithTimezoneInfo.getTraceType()).toEqual(
       TraceType.WINDOW_MANAGER,
     );
 
-    expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual([
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(0n),
-    ]);
-    expect(parser.getTimestamps(TimestampType.REAL)).toEqual([
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n),
+    expect(parser.getTimestamps()).toEqual([
+      TimestampConverterUtils.makeElapsedTimestamp(0n),
     ]);
   });
 
   it('retrieves trace entry', async () => {
-    const entry = await parser.getEntry(0, TimestampType.ELAPSED);
+    const entry = await parser.getEntry(0);
     expect(entry).toBeInstanceOf(HierarchyTreeNode);
     expect(entry.getEagerPropertyByName('focusedApp')?.getValue()).toEqual(
       'com.google.android.apps.nexuslauncher/.NexusLauncherActivity',
diff --git a/tools/winscope/src/parsers/window_manager/parser_window_manager_test.ts b/tools/winscope/src/parsers/window_manager/parser_window_manager_test.ts
index b5db515..c2e2bea 100644
--- a/tools/winscope/src/parsers/window_manager/parser_window_manager_test.ts
+++ b/tools/winscope/src/parsers/window_manager/parser_window_manager_test.ts
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
 import {CoarseVersion} from 'trace/coarse_version';
@@ -26,7 +25,7 @@
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 
 describe('ParserWindowManager', () => {
-  describe('trace with elapsed + real timestamp', () => {
+  describe('trace with real timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
     let trace: Trace<HierarchyTreeNode>;
 
@@ -49,53 +48,19 @@
       expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
     });
 
-    it('provides elapsed timestamps', () => {
+    it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14474594000n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15398076788n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(15409222011n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089075566202n),
+        TimestampConverterUtils.makeRealTimestamp(1659107089999048990n),
+        TimestampConverterUtils.makeRealTimestamp(1659107090010194213n),
       ];
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.ELAPSED)).slice(0, 3),
-      ).toEqual(expected);
-    });
-
-    it('provides real timestamps', () => {
-      const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089075566202n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107089999048990n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659107090010194213n),
-      ];
-      expect(
-        assertDefined(parser.getTimestamps(TimestampType.REAL)).slice(0, 3),
-      ).toEqual(expected);
-    });
-
-    it('applies timezone info to real timestamps only', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_and_real_timestamp/WindowManager.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.WINDOW_MANAGER,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(14474594000n));
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.REAL),
-        )[0],
-      ).toEqual(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126889075566202n),
+      expect(assertDefined(parser.getTimestamps()).slice(0, 3)).toEqual(
+        expected,
       );
     });
 
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(1, TimestampType.REAL);
+      const entry = await parser.getEntry(1);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('WindowManagerState root');
     });
@@ -109,7 +74,7 @@
     });
   });
 
-  describe('trace elapsed (only) timestamp', () => {
+  describe('trace with only elapsed timestamps', () => {
     let parser: Parser<HierarchyTreeNode>;
 
     beforeAll(async () => {
@@ -124,31 +89,15 @@
 
     it('provides timestamps', () => {
       const expected = [
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850254319343n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850763506110n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850782750048n),
+        TimestampConverterUtils.makeElapsedTimestamp(850254319343n),
+        TimestampConverterUtils.makeElapsedTimestamp(850763506110n),
+        TimestampConverterUtils.makeElapsedTimestamp(850782750048n),
       ];
-      expect(parser.getTimestamps(TimestampType.ELAPSED)).toEqual(expected);
-    });
-
-    it('does not apply timezone info', async () => {
-      const parserWithTimezoneInfo = (await UnitTestUtils.getParser(
-        'traces/elapsed_timestamp/WindowManager.pb',
-        true,
-      )) as Parser<HierarchyTreeNode>;
-      expect(parserWithTimezoneInfo.getTraceType()).toEqual(
-        TraceType.WINDOW_MANAGER,
-      );
-
-      expect(
-        assertDefined(
-          parserWithTimezoneInfo.getTimestamps(TimestampType.ELAPSED),
-        )[0],
-      ).toEqual(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(850254319343n));
+      expect(parser.getTimestamps()).toEqual(expected);
     });
 
     it('retrieves trace entry', async () => {
-      const entry = await parser.getEntry(1, TimestampType.ELAPSED);
+      const entry = await parser.getEntry(1);
       expect(entry).toBeInstanceOf(HierarchyTreeNode);
       expect(entry.id).toEqual('WindowManagerState root');
     });
diff --git a/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt b/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt
index c02add5..95285e2 100644
--- a/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt
+++ b/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-UPB2.230407.019-2023-05-30-14-33-48.txt
@@ -3,4 +3,6 @@
 ========================================================
 
 # ORIGINAL CONTENTS REMOVED TO AVOID INFORMATION LEAKS
-
+[persist.sys.locale]: [en-US]
+[persist.sys.timezone]: [Asia/Kolkata]
+[persist.sys.time.offset]: [19800000]
diff --git a/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-no-time-offset-UPB2.230407.019-2023-05-30-14-33-48.txt b/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-no-time-offset-UPB2.230407.019-2023-05-30-14-33-48.txt
new file mode 100644
index 0000000..aaa239b
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/bugreports/bugreport-codename_beta-no-time-offset-UPB2.230407.019-2023-05-30-14-33-48.txt
@@ -0,0 +1,7 @@
+========================================================
+== dumpstate: 2023-05-30 14:33:48
+========================================================
+
+# ORIGINAL CONTENTS REMOVED TO AVOID INFORMATION LEAKS
+[persist.sys.locale]: [en-US]
+[persist.sys.timezone]: [Asia/Kolkata]
diff --git a/tools/winscope/src/test/fixtures/bugreports/dumpstate_board.txt b/tools/winscope/src/test/fixtures/bugreports/dumpstate_board.txt
deleted file mode 100644
index 7d78918..0000000
--- a/tools/winscope/src/test/fixtures/bugreports/dumpstate_board.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-TEST DUMPSTATE BOARD
-[persist.sys.locale]: [en-US]
-[persist.sys.timezone]: [Asia/Kolkata]
-EOF
\ No newline at end of file
diff --git a/tools/winscope/src/test/unit/parser_builder.ts b/tools/winscope/src/test/unit/parser_builder.ts
index 4d19e4f..74dd008 100644
--- a/tools/winscope/src/test/unit/parser_builder.ts
+++ b/tools/winscope/src/test/unit/parser_builder.ts
@@ -15,7 +15,7 @@
  */
 
 import {Timestamp} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {
   CustomQueryParserResultTypeMap,
   CustomQueryType,
@@ -30,6 +30,7 @@
   private timestamps?: Timestamp[];
   private customQueryResult = new Map<CustomQueryType, {}>();
   private descriptors = ['file descriptor'];
+  private noOffsets = false;
 
   setType(type: TraceType): this {
     this.type = type;
@@ -46,6 +47,11 @@
     return this;
   }
 
+  setNoOffsets(value: boolean): this {
+    this.noOffsets = value;
+    return this;
+  }
+
   setCustomQueryResult<Q extends CustomQueryType>(
     type: Q,
     result: CustomQueryParserResultTypeMap[Q],
@@ -86,13 +92,14 @@
       this.entries,
       this.customQueryResult,
       this.descriptors,
+      this.noOffsets,
     );
   }
 
   private createTimestamps(entries: T[]): Timestamp[] {
     const timestamps = new Array<Timestamp>();
     for (let i = 0; i < entries.length; ++i) {
-      timestamps[i] = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(BigInt(i));
+      timestamps[i] = TimestampConverterUtils.makeRealTimestamp(BigInt(i));
     }
     return timestamps;
   }
diff --git a/tools/winscope/src/test/unit/timestamp_converter_utils.ts b/tools/winscope/src/test/unit/timestamp_converter_utils.ts
new file mode 100644
index 0000000..3b5c017
--- /dev/null
+++ b/tools/winscope/src/test/unit/timestamp_converter_utils.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {Timestamp} from 'common/time';
+import {TimestampConverter} from 'common/timestamp_converter';
+
+export class TimestampConverterUtils {
+  static readonly ASIA_TIMEZONE_INFO = {
+    timezone: 'Asia/Kolkata',
+    locale: 'en-US',
+    utcOffsetMs: 19800000,
+  };
+  static readonly UTC_TIMEZONE_INFO = {
+    timezone: 'UTC',
+    locale: 'en-US',
+    utcOffsetMs: 0,
+  };
+
+  static readonly TIMESTAMP_CONVERTER_WITH_UTC_OFFSET = new TimestampConverter(
+    TimestampConverterUtils.ASIA_TIMEZONE_INFO,
+    0n,
+    0n,
+  );
+
+  static readonly TIMESTAMP_CONVERTER = new TimestampConverter(
+    TimestampConverterUtils.UTC_TIMEZONE_INFO,
+    0n,
+    0n,
+  );
+
+  private static readonly TIMESTAMP_CONVERTER_NO_RTE_OFFSET =
+    new TimestampConverter({
+      timezone: 'UTC',
+      locale: 'en-US',
+      utcOffsetMs: 0,
+    });
+
+  static makeRealTimestamp(valueNs: bigint): Timestamp {
+    return TimestampConverterUtils.TIMESTAMP_CONVERTER.makeTimestampFromRealNs(
+      valueNs,
+    );
+  }
+
+  static makeRealTimestampWithUTCOffset(valueNs: bigint): Timestamp {
+    return TimestampConverterUtils.TIMESTAMP_CONVERTER_WITH_UTC_OFFSET.makeTimestampFromRealNs(
+      valueNs,
+    );
+  }
+
+  static makeElapsedTimestamp(valueNs: bigint): Timestamp {
+    return TimestampConverterUtils.TIMESTAMP_CONVERTER_NO_RTE_OFFSET.makeTimestampFromMonotonicNs(
+      valueNs,
+    );
+  }
+}
diff --git a/tools/winscope/src/test/unit/trace_builder.ts b/tools/winscope/src/test/unit/trace_builder.ts
index 6319cad..9be7037 100644
--- a/tools/winscope/src/test/unit/trace_builder.ts
+++ b/tools/winscope/src/test/unit/trace_builder.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {
   CustomQueryParserResultTypeMap,
   CustomQueryType,
@@ -37,7 +37,6 @@
   private parserCustomQueryResult = new Map<CustomQueryType, {}>();
   private entries?: T[];
   private timestamps?: Timestamp[];
-  private timestampType = TimestampType.REAL;
   private frameMap?: FrameMap;
   private frameMapBuilder?: FrameMapBuilder;
   private descriptors: string[] = [];
@@ -62,11 +61,6 @@
     return this;
   }
 
-  setTimestampType(type: TimestampType): TraceBuilder<T> {
-    this.timestampType = type;
-    return this;
-  }
-
   setFrameMap(frameMap?: FrameMap): TraceBuilder<T> {
     this.frameMap = frameMap;
     return this;
@@ -105,7 +99,6 @@
       this.parser,
       this.descriptors,
       undefined,
-      this.timestampType,
       entriesRange,
     );
 
diff --git a/tools/winscope/src/test/unit/utils.ts b/tools/winscope/src/test/unit/utils.ts
index 8a806b4..541c582 100644
--- a/tools/winscope/src/test/unit/utils.ts
+++ b/tools/winscope/src/test/unit/utils.ts
@@ -15,11 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {
-  NO_TIMEZONE_OFFSET_FACTORY,
-  TimestampFactory,
-} from 'common/timestamp_factory';
+import {Timestamp} from 'common/time';
+import {TimestampConverter} from 'common/timestamp_converter';
 import {UrlUtils} from 'common/url_utils';
 import {ParserFactory as LegacyParserFactory} from 'parsers/legacy/parser_factory';
 import {TracesParserFactory} from 'parsers/legacy/traces_parser_factory';
@@ -30,14 +27,10 @@
 import {TraceFile} from 'trace/trace_file';
 import {TraceEntryTypeMap, TraceType} from 'trace/trace_type';
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
+import {TimestampConverterUtils} from './timestamp_converter_utils';
 import {TraceBuilder} from './trace_builder';
 
 class UnitTestUtils {
-  static readonly TIMESTAMP_FACTORY_WITH_TIMEZONE = new TimestampFactory({
-    timezone: 'Asia/Kolkata',
-    locale: 'en-US',
-  });
-
   static async getFixtureFile(
     srcFilename: string,
     dstFilename: string = srcFilename,
@@ -54,7 +47,8 @@
     type: T,
     filename: string,
   ): Promise<Trace<T>> {
-    const legacyParsers = await UnitTestUtils.getParsers(filename);
+    const converter = UnitTestUtils.getTimestampConverter(false);
+    const legacyParsers = await UnitTestUtils.getParsers(filename, converter);
     expect(legacyParsers.length).toBeLessThanOrEqual(1);
     if (legacyParsers.length === 1) {
       expect(legacyParsers[0].getTraceType()).toEqual(type);
@@ -75,44 +69,28 @@
 
   static async getParser(
     filename: string,
-    withTimezoneInfo = false,
+    converter = UnitTestUtils.getTimestampConverter(),
+    initializeRealToElapsedTimeOffsetNs = true,
   ): Promise<Parser<object>> {
-    const parsers = await UnitTestUtils.getParsers(filename, withTimezoneInfo);
+    const parsers = await UnitTestUtils.getParsers(
+      filename,
+      converter,
+      initializeRealToElapsedTimeOffsetNs,
+    );
     expect(parsers.length)
       .withContext(`Should have been able to create a parser for ${filename}`)
       .toBeGreaterThanOrEqual(1);
     return parsers[0];
   }
 
-  static async getParsers(
-    filename: string,
-    withTimezoneInfo = false,
-  ): Promise<Array<Parser<object>>> {
-    const file = new TraceFile(
-      await UnitTestUtils.getFixtureFile(filename),
-      undefined,
-    );
-    const fileAndParsers = await new LegacyParserFactory().createParsers(
-      [file],
-      withTimezoneInfo
-        ? UnitTestUtils.TIMESTAMP_FACTORY_WITH_TIMEZONE
-        : NO_TIMEZONE_OFFSET_FACTORY,
-      undefined,
-      undefined,
-    );
-    return fileAndParsers.map((fileAndParser) => {
-      return fileAndParser.parser;
-    });
-  }
-
   static async getPerfettoParser<T extends TraceType>(
     traceType: T,
     fixturePath: string,
-    withTimezoneInfo = false,
+    withUTCOffset = false,
   ): Promise<Parser<TraceEntryTypeMap[T]>> {
     const parsers = await UnitTestUtils.getPerfettoParsers(
       fixturePath,
-      withTimezoneInfo,
+      withUTCOffset,
     );
     const parser = assertDefined(
       parsers.find((parser) => parser.getTraceType() === traceType),
@@ -122,36 +100,60 @@
 
   static async getPerfettoParsers(
     fixturePath: string,
-    withTimezoneInfo = false,
+    withUTCOffset = false,
   ): Promise<Array<Parser<object>>> {
     const file = await UnitTestUtils.getFixtureFile(fixturePath);
     const traceFile = new TraceFile(file);
-    return await new PerfettoParserFactory().createParsers(
+    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
+    const parsers = await new PerfettoParserFactory().createParsers(
       traceFile,
-      withTimezoneInfo
-        ? UnitTestUtils.TIMESTAMP_FACTORY_WITH_TIMEZONE
-        : NO_TIMEZONE_OFFSET_FACTORY,
+      converter,
       undefined,
     );
+    parsers.forEach((parser) => {
+      converter.setRealToBootTimeOffsetNs(
+        assertDefined(parser.getRealToBootTimeOffsetNs()),
+      );
+      parser.createTimestamps();
+    });
+    return parsers;
   }
 
   static async getTracesParser(
     filenames: string[],
-    withTimezoneInfo = false,
+    withUTCOffset = false,
   ): Promise<Parser<object>> {
+    const converter = UnitTestUtils.getTimestampConverter(withUTCOffset);
     const parsersArray = await Promise.all(
-      filenames.map((filename) =>
-        UnitTestUtils.getParser(filename, withTimezoneInfo),
+      filenames.map(async (filename) =>
+        UnitTestUtils.getParser(filename, converter, true),
       ),
     );
+    const offset = parsersArray
+      .filter((parser) => parser.getRealToBootTimeOffsetNs() !== undefined)
+      .sort((a, b) =>
+        Number(
+          (a.getRealToBootTimeOffsetNs() ?? 0n) -
+            (b.getRealToBootTimeOffsetNs() ?? 0n),
+        ),
+      )
+      .at(-1)
+      ?.getRealToBootTimeOffsetNs();
+
+    if (offset !== undefined) {
+      converter.setRealToBootTimeOffsetNs(offset);
+    }
 
     const traces = new Traces();
     parsersArray.forEach((parser) => {
-      const trace = Trace.fromParser(parser, TimestampType.REAL);
+      const trace = Trace.fromParser(parser);
       traces.setTrace(parser.getTraceType(), trace);
     });
 
-    const tracesParsers = await new TracesParserFactory().createParsers(traces);
+    const tracesParsers = await new TracesParserFactory().createParsers(
+      traces,
+      converter,
+    );
     expect(tracesParsers.length)
       .withContext(
         `Should have been able to create a traces parser for [${filenames.join()}]`,
@@ -160,6 +162,12 @@
     return tracesParsers[0];
   }
 
+  static getTimestampConverter(withUTCOffset = false): TimestampConverter {
+    return withUTCOffset
+      ? new TimestampConverter(TimestampConverterUtils.ASIA_TIMEZONE_INFO)
+      : new TimestampConverter(TimestampConverterUtils.UTC_TIMEZONE_INFO);
+  }
+
   static async getWindowManagerState(index = 0): Promise<HierarchyTreeNode> {
     return UnitTestUtils.getTraceEntry(
       'traces/elapsed_and_real_timestamp/WindowManager.pb',
@@ -194,7 +202,7 @@
       const parser = (await UnitTestUtils.getParser(
         'traces/ime/SurfaceFlinger_with_IME.pb',
       )) as Parser<HierarchyTreeNode>;
-      surfaceFlingerEntry = await parser.getEntry(5, TimestampType.ELAPSED);
+      surfaceFlingerEntry = await parser.getEntry(5);
     }
 
     let windowManagerEntry: HierarchyTreeNode | undefined;
@@ -202,7 +210,7 @@
       const parser = (await UnitTestUtils.getParser(
         'traces/ime/WindowManager_with_IME.pb',
       )) as Parser<HierarchyTreeNode>;
-      windowManagerEntry = await parser.getEntry(2, TimestampType.ELAPSED);
+      windowManagerEntry = await parser.getEntry(2);
     }
 
     const entries = new Map<TraceType, HierarchyTreeNode>();
@@ -237,14 +245,59 @@
     timestamp: Timestamp,
     expectedTimestamp: Timestamp,
   ): boolean {
-    if (timestamp.getType() !== expectedTimestamp.getType()) return false;
-    if (timestamp.getValueNs() !== expectedTimestamp.getValueNs()) return false;
+    if (timestamp.format() !== expectedTimestamp.format()) return false;
+    if (timestamp.getValueNs() !== expectedTimestamp.getValueNs()) {
+      return false;
+    }
     return true;
   }
 
   private static async getTraceEntry<T>(filename: string, index = 0) {
     const parser = (await UnitTestUtils.getParser(filename)) as Parser<T>;
-    return parser.getEntry(index, TimestampType.ELAPSED);
+    return parser.getEntry(index);
+  }
+
+  private static async getParsers(
+    filename: string,
+    converter: TimestampConverter,
+    initializeRealToElapsedTimeOffsetNs = true,
+  ): Promise<Array<Parser<object>>> {
+    const file = new TraceFile(
+      await UnitTestUtils.getFixtureFile(filename),
+      undefined,
+    );
+    const fileAndParsers = await new LegacyParserFactory().createParsers(
+      [file],
+      converter,
+      undefined,
+      undefined,
+    );
+
+    if (initializeRealToElapsedTimeOffsetNs) {
+      const monotonicOffset = fileAndParsers
+        .find(
+          (fileAndParser) =>
+            fileAndParser.parser.getRealToMonotonicTimeOffsetNs() !== undefined,
+        )
+        ?.parser.getRealToMonotonicTimeOffsetNs();
+      if (monotonicOffset !== undefined) {
+        converter.setRealToMonotonicTimeOffsetNs(monotonicOffset);
+      }
+      const bootTimeOffset = fileAndParsers
+        .find(
+          (fileAndParser) =>
+            fileAndParser.parser.getRealToBootTimeOffsetNs() !== undefined,
+        )
+        ?.parser.getRealToBootTimeOffsetNs();
+      if (bootTimeOffset !== undefined) {
+        converter.setRealToBootTimeOffsetNs(bootTimeOffset);
+      }
+    }
+
+    return fileAndParsers.map((fileAndParser) => {
+      fileAndParser.parser.createTimestamps();
+      return fileAndParser.parser;
+    });
   }
 }
 
diff --git a/tools/winscope/src/trace/frame_mapper_test.ts b/tools/winscope/src/trace/frame_mapper_test.ts
index 1737e41..45b44aa 100644
--- a/tools/winscope/src/trace/frame_mapper_test.ts
+++ b/tools/winscope/src/trace/frame_mapper_test.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesUtils} from 'test/unit/traces_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {CustomQueryType} from './custom_query';
@@ -28,16 +28,16 @@
 import {PropertyTreeNode} from './tree_node/property_tree_node';
 
 describe('FrameMapper', () => {
-  const time0 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n);
-  const time1 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1n);
-  const time2 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(2n);
-  const time3 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(3n);
-  const time4 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(4n);
-  const time5 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(5n);
-  const time6 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(6n);
-  const time7 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(7n);
-  const time8 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(8n);
-  const time10seconds = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(
+  const time0 = TimestampConverterUtils.makeRealTimestamp(0n);
+  const time1 = TimestampConverterUtils.makeRealTimestamp(1n);
+  const time2 = TimestampConverterUtils.makeRealTimestamp(2n);
+  const time3 = TimestampConverterUtils.makeRealTimestamp(3n);
+  const time4 = TimestampConverterUtils.makeRealTimestamp(4n);
+  const time5 = TimestampConverterUtils.makeRealTimestamp(5n);
+  const time6 = TimestampConverterUtils.makeRealTimestamp(6n);
+  const time7 = TimestampConverterUtils.makeRealTimestamp(7n);
+  const time8 = TimestampConverterUtils.makeRealTimestamp(8n);
+  const time10seconds = TimestampConverterUtils.makeRealTimestamp(
     10n * 1000000000n,
   );
 
diff --git a/tools/winscope/src/trace/parser.ts b/tools/winscope/src/trace/parser.ts
index 79f797c..e2ac2e5 100644
--- a/tools/winscope/src/trace/parser.ts
+++ b/tools/winscope/src/trace/parser.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {CoarseVersion} from './coarse_version';
 import {
   CustomQueryParamTypeMap,
@@ -28,12 +28,15 @@
   getCoarseVersion(): CoarseVersion;
   getTraceType(): TraceType;
   getLengthEntries(): number;
-  getTimestamps(type: TimestampType): Timestamp[] | undefined;
-  getEntry(index: AbsoluteEntryIndex, timestampType: TimestampType): Promise<T>;
+  getTimestamps(): Timestamp[] | undefined;
+  getEntry(index: AbsoluteEntryIndex): Promise<T>;
   customQuery<Q extends CustomQueryType>(
     type: Q,
     entriesRange: EntriesRange,
     param?: CustomQueryParamTypeMap[Q],
   ): Promise<CustomQueryParserResultTypeMap[Q]>;
   getDescriptors(): string[];
+  getRealToMonotonicTimeOffsetNs(): bigint | undefined;
+  getRealToBootTimeOffsetNs(): bigint | undefined;
+  createTimestamps(): void;
 }
diff --git a/tools/winscope/src/trace/parser_mock.ts b/tools/winscope/src/trace/parser_mock.ts
index afdde67..0ce8020 100644
--- a/tools/winscope/src/trace/parser_mock.ts
+++ b/tools/winscope/src/trace/parser_mock.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {CoarseVersion} from './coarse_version';
 import {CustomQueryParserResultTypeMap, CustomQueryType} from './custom_query';
 import {AbsoluteEntryIndex, EntriesRange} from './index_types';
@@ -28,6 +28,7 @@
     private readonly entries: T[],
     private readonly customQueryResult: Map<CustomQueryType, object>,
     private readonly descriptors: string[],
+    private readonly noOffsets: boolean,
   ) {
     if (timestamps.length !== entries.length) {
       throw new Error(`Timestamps and entries must have the same length`);
@@ -46,10 +47,19 @@
     return CoarseVersion.MOCK;
   }
 
-  getTimestamps(type: TimestampType): Timestamp[] | undefined {
-    if (type !== TimestampType.REAL) {
-      throw new Error('Parser mock contains only real timestamps');
-    }
+  createTimestamps() {
+    throw new Error('Not implemented');
+  }
+
+  getRealToMonotonicTimeOffsetNs(): bigint | undefined {
+    return this.noOffsets ? undefined : 0n;
+  }
+
+  getRealToBootTimeOffsetNs(): bigint | undefined {
+    return this.noOffsets ? undefined : 0n;
+  }
+
+  getTimestamps(): Timestamp[] {
     return this.timestamps;
   }
 
diff --git a/tools/winscope/src/trace/screen_recording_utils.ts b/tools/winscope/src/trace/screen_recording_utils.ts
index 3427e47..e11988a 100644
--- a/tools/winscope/src/trace/screen_recording_utils.ts
+++ b/tools/winscope/src/trace/screen_recording_utils.ts
@@ -14,26 +14,18 @@
  * limitations under the License.
  */
 
-import {Timestamp} from 'common/time';
-
-class ScreenRecordingUtils {
+export class ScreenRecordingUtils {
   // Video time correction epsilon. Without correction, we could display the previous frame.
   // This correction was already present in the legacy Winscope.
   private static readonly EPSILON_SECONDS = 0.00001;
 
   static timestampToVideoTimeSeconds(
-    firstTimestamp: Timestamp,
-    currentTimestamp: Timestamp,
+    firstTimestampNs: bigint,
+    currentTimestampNs: bigint,
   ) {
-    if (firstTimestamp.getType() !== currentTimestamp.getType()) {
-      throw new Error('Attempted to use timestamps with different type');
-    }
     const videoTimeSeconds =
-      Number(currentTimestamp.getValueNs() - firstTimestamp.getValueNs()) /
-        1000000000 +
+      Number(currentTimestampNs - firstTimestampNs) / 1000000000 +
       ScreenRecordingUtils.EPSILON_SECONDS;
     return videoTimeSeconds;
   }
 }
-
-export {ScreenRecordingUtils};
diff --git a/tools/winscope/src/trace/trace.ts b/tools/winscope/src/trace/trace.ts
index 22610bd..43c0daf 100644
--- a/tools/winscope/src/trace/trace.ts
+++ b/tools/winscope/src/trace/trace.ts
@@ -15,7 +15,7 @@
  */
 
 import {ArrayUtils} from 'common/array_utils';
-import {Timestamp, TimestampType} from 'common/time';
+import {Timestamp} from 'common/time';
 import {
   CustomQueryParamTypeMap,
   CustomQueryParserResultTypeMap,
@@ -87,7 +87,7 @@
   }
 
   override async getValue(): Promise<T> {
-    return await this.parser.getEntry(this.index, this.timestamp.getType());
+    return await this.parser.getEntry(this.index);
   }
 }
 
@@ -118,21 +118,16 @@
   private readonly parser: Parser<T>;
   private readonly descriptors: string[];
   private readonly fullTrace: Trace<T>;
-  private timestampType: TimestampType;
   private readonly entriesRange: EntriesRange;
   private frameMap?: FrameMap;
   private framesRange?: FramesRange;
 
-  static fromParser<T>(
-    parser: Parser<T>,
-    timestampType: TimestampType,
-  ): Trace<T> {
+  static fromParser<T>(parser: Parser<T>): Trace<T> {
     return new Trace(
       parser.getTraceType(),
       parser,
       parser.getDescriptors(),
       undefined,
-      timestampType,
       undefined,
     );
   }
@@ -142,7 +137,6 @@
     parser: Parser<T>,
     descriptors: string[],
     fullTrace: Trace<T> | undefined,
-    timestampType: TimestampType,
     entriesRange: EntriesRange | undefined,
   ) {
     this.type = type;
@@ -154,20 +148,12 @@
       end: parser.getLengthEntries(),
     };
     this.lengthEntries = this.entriesRange.end - this.entriesRange.start;
-    this.timestampType = timestampType;
   }
 
   getDescriptors(): string[] {
     return this.parser.getDescriptors();
   }
 
-  getTimestampType(): TimestampType {
-    if (this.timestampType === undefined) {
-      throw new Error('Trace no fully initialized yet!');
-    }
-    return this.timestampType;
-  }
-
   setFrameInfo(frameMap: FrameMap, framesRange: FramesRange | undefined) {
     if (frameMap.lengthEntries !== this.fullTrace.lengthEntries) {
       throw new Error(
@@ -238,7 +224,6 @@
   }
 
   findClosestEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
-    this.checkTimestampIsCompatible(time);
     if (this.lengthEntries === 0) {
       return undefined;
     }
@@ -272,7 +257,6 @@
   }
 
   findFirstGreaterOrEqualEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
-    this.checkTimestampIsCompatible(time);
     if (this.lengthEntries === 0) {
       return undefined;
     }
@@ -288,7 +272,7 @@
     }
 
     const entry = this.getEntry(pos - this.entriesRange.start);
-    if (entry.getTimestamp() < time) {
+    if (entry.getTimestamp().getValueNs() < time.getValueNs()) {
       return undefined;
     }
 
@@ -296,7 +280,6 @@
   }
 
   findFirstGreaterEntry(time: Timestamp): TraceEntryLazy<T> | undefined {
-    this.checkTimestampIsCompatible(time);
     if (this.lengthEntries === 0) {
       return undefined;
     }
@@ -309,7 +292,7 @@
     }
 
     const entry = this.getEntry(pos - this.entriesRange.start);
-    if (entry.getTimestamp() <= time) {
+    if (entry.getTimestamp().getValueNs() <= time.getValueNs()) {
       return undefined;
     }
 
@@ -364,8 +347,6 @@
   }
 
   sliceTime(start?: Timestamp, end?: Timestamp): Trace<T> {
-    this.checkTimestampIsCompatible(start);
-    this.checkTimestampIsCompatible(end);
     const startEntry =
       start === undefined
         ? this.entriesRange.start
@@ -493,15 +474,9 @@
   }
 
   private getFullTraceTimestamps(): Timestamp[] {
-    if (this.timestampType === undefined) {
-      throw new Error('Forgot to initialize trace?');
-    }
-
-    const timestamps = this.parser.getTimestamps(this.timestampType);
+    const timestamps = this.parser.getTimestamps();
     if (!timestamps) {
-      throw new Error(
-        `Timestamp type ${this.timestampType} is expected to be available`,
-      );
+      throw new Error('Timestamps expected to be available');
     }
     return timestamps;
   }
@@ -537,7 +512,6 @@
       this.parser,
       this.descriptors,
       this.fullTrace,
-      this.timestampType,
       entries,
     );
 
@@ -610,18 +584,4 @@
       );
     }
   }
-
-  private checkTimestampIsCompatible(timestamp?: Timestamp) {
-    if (!timestamp) {
-      return;
-    }
-    const timestamps = this.parser.getTimestamps(timestamp.getType());
-    if (timestamps === undefined) {
-      throw new Error(
-        `Trace ${
-          this.type
-        } can't be accessed using timestamp of type ${timestamp.getType()}`,
-      );
-    }
-  }
 }
diff --git a/tools/winscope/src/trace/trace_entry_test.ts b/tools/winscope/src/trace/trace_entry_test.ts
index 86d04e6..66c7fe7 100644
--- a/tools/winscope/src/trace/trace_entry_test.ts
+++ b/tools/winscope/src/trace/trace_entry_test.ts
@@ -14,22 +14,24 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
+import {UnitTestUtils} from 'test/unit/utils';
 import {Trace} from './trace';
 
 describe('TraceEntry', () => {
   let trace: Trace<string>;
 
   beforeAll(() => {
+    jasmine.addCustomEqualityTester(UnitTestUtils.timestampEqualityTester);
     trace = new TraceBuilder<string>()
       .setTimestamps([
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(13n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(14n),
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(15n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(11n),
+        TimestampConverterUtils.makeRealTimestamp(12n),
+        TimestampConverterUtils.makeRealTimestamp(13n),
+        TimestampConverterUtils.makeRealTimestamp(14n),
+        TimestampConverterUtils.makeRealTimestamp(15n),
       ])
       .setEntries([
         'entry-0',
@@ -61,10 +63,10 @@
 
   it('getTimestamp()', () => {
     expect(trace.getEntry(0).getTimestamp()).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
+      TimestampConverterUtils.makeRealTimestamp(10n),
     );
     expect(trace.getEntry(1).getTimestamp()).toEqual(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n),
+      TimestampConverterUtils.makeRealTimestamp(11n),
     );
   });
 
diff --git a/tools/winscope/src/trace/trace_test.ts b/tools/winscope/src/trace/trace_test.ts
index d078bcc..94f8a11 100644
--- a/tools/winscope/src/trace/trace_test.ts
+++ b/tools/winscope/src/trace/trace_test.ts
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TraceUtils} from 'test/unit/trace_utils';
 import {FrameMapBuilder} from './frame_map_builder';
@@ -24,13 +24,13 @@
 describe('Trace', () => {
   let trace: Trace<string>;
 
-  const time9 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(9n);
-  const time10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-  const time11 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n);
-  const time12 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n);
-  const time13 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(13n);
-  const time14 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(14n);
-  const time15 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(15n);
+  const time9 = TimestampConverterUtils.makeRealTimestamp(9n);
+  const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
+  const time11 = TimestampConverterUtils.makeRealTimestamp(11n);
+  const time12 = TimestampConverterUtils.makeRealTimestamp(12n);
+  const time13 = TimestampConverterUtils.makeRealTimestamp(13n);
+  const time14 = TimestampConverterUtils.makeRealTimestamp(14n);
+  const time15 = TimestampConverterUtils.makeRealTimestamp(15n);
 
   beforeAll(() => {
     // Time:       10    11                 12    13
@@ -936,8 +936,8 @@
     ]);
 
     // time
-    const time12 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n);
-    const time13 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(13n);
+    const time12 = TimestampConverterUtils.makeRealTimestamp(12n);
+    const time13 = TimestampConverterUtils.makeRealTimestamp(13n);
     expect(
       await TraceUtils.extractEntries(trace.sliceTime(time12, time12)),
     ).toEqual([]);
@@ -993,8 +993,8 @@
     );
 
     // time
-    const time12 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n);
-    const time13 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(13n);
+    const time12 = TimestampConverterUtils.makeRealTimestamp(12n);
+    const time13 = TimestampConverterUtils.makeRealTimestamp(13n);
     expect(await TraceUtils.extractEntries(empty.sliceTime())).toEqual([]);
     expect(await TraceUtils.extractEntries(empty.sliceTime(time12))).toEqual(
       [],
diff --git a/tools/winscope/src/trace/traces_test.ts b/tools/winscope/src/trace/traces_test.ts
index e79824b..0bae84d 100644
--- a/tools/winscope/src/trace/traces_test.ts
+++ b/tools/winscope/src/trace/traces_test.ts
@@ -16,7 +16,7 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {FunctionUtils} from 'common/function_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TracesUtils} from 'test/unit/traces_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
@@ -29,16 +29,16 @@
 describe('Traces', () => {
   let traces: Traces;
 
-  const time1 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1n);
-  const time2 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(2n);
-  const time3 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(3n);
-  const time4 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(4n);
-  const time5 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(5n);
-  const time6 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(6n);
-  const time7 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(7n);
-  const time8 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(8n);
-  const time9 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(9n);
-  const time10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
+  const time1 = TimestampConverterUtils.makeRealTimestamp(1n);
+  const time2 = TimestampConverterUtils.makeRealTimestamp(2n);
+  const time3 = TimestampConverterUtils.makeRealTimestamp(3n);
+  const time4 = TimestampConverterUtils.makeRealTimestamp(4n);
+  const time5 = TimestampConverterUtils.makeRealTimestamp(5n);
+  const time6 = TimestampConverterUtils.makeRealTimestamp(6n);
+  const time7 = TimestampConverterUtils.makeRealTimestamp(7n);
+  const time8 = TimestampConverterUtils.makeRealTimestamp(8n);
+  const time9 = TimestampConverterUtils.makeRealTimestamp(9n);
+  const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
 
   let extractedEntriesEmpty: Map<TraceType, Array<{}>>;
   let extractedEntriesFull: Map<TraceType, Array<{}>>;
diff --git a/tools/winscope/src/trace/tree_node/formatters.ts b/tools/winscope/src/trace/tree_node/formatters.ts
index aa4ffd8..2e323f1 100644
--- a/tools/winscope/src/trace/tree_node/formatters.ts
+++ b/tools/winscope/src/trace/tree_node/formatters.ts
@@ -15,7 +15,7 @@
  */
 
 import {Timestamp} from 'common/time';
-import {TimestampUtils} from 'common/timestamp_utils';
+import {TimeDuration} from 'common/time_duration';
 import {RawDataUtils} from 'parsers/raw_data_utils';
 import {TransformUtils} from 'parsers/surface_flinger/transform_utils';
 import {PropertyTreeNode} from './property_tree_node';
@@ -210,16 +210,16 @@
   }
 }
 
-class TimestampFormatter implements PropertyFormatter {
+class TimestampNodeFormatter implements PropertyFormatter {
   format(node: PropertyTreeNode): string {
     const timestamp = node.getValue();
-    if (timestamp instanceof Timestamp) {
-      return TimestampUtils.format(timestamp);
+    if (timestamp instanceof Timestamp || timestamp instanceof TimeDuration) {
+      return timestamp.format();
     }
     return 'null';
   }
 }
-const TIMESTAMP_FORMATTER = new TimestampFormatter();
+const TIMESTAMP_NODE_FORMATTER = new TimestampNodeFormatter();
 
 export {
   EMPTY_OBJ_STRING,
@@ -236,6 +236,6 @@
   REGION_FORMATTER,
   EnumFormatter,
   FixedStringFormatter,
-  TIMESTAMP_FORMATTER,
+  TIMESTAMP_NODE_FORMATTER,
   MATRIX_FORMATTER,
 };
diff --git a/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts b/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts
index 0997af3..9ab6fdb 100644
--- a/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts
+++ b/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts
@@ -15,6 +15,7 @@
  */
 
 import {Timestamp} from 'common/time';
+import {TimeDuration} from 'common/time_duration';
 import {
   PropertySource,
   PropertyTreeNode,
@@ -115,6 +116,7 @@
     if (Array.isArray(value)) return value.length > 0;
     if (this.isLongType(value)) return false;
     if (value instanceof Timestamp) return false;
+    if (value instanceof TimeDuration) return false;
     return typeof value === 'object' && Object.keys(value).length > 0;
   }
 
diff --git a/tools/winscope/src/trace_processor/engine.ts b/tools/winscope/src/trace_processor/engine.ts
index c6454a2..aae959f 100644
--- a/tools/winscope/src/trace_processor/engine.ts
+++ b/tools/winscope/src/trace_processor/engine.ts
@@ -15,7 +15,6 @@
 import {defer, Deferred} from './deferred';
 import {assertExists, assertTrue} from './logging';
 import {perfetto} from '../../deps_build/trace_processor/ui/tsc/gen/protos';
-
 import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
   ComputeMetricArgs,
@@ -31,7 +30,6 @@
   QueryResult,
   WritableQueryResult,
 } from './query_result';
-
 import TraceProcessorRpc = perfetto.protos.TraceProcessorRpc;
 import TraceProcessorRpcStream = perfetto.protos.TraceProcessorRpcStream;
 import TPM = perfetto.protos.TraceProcessorRpc.TraceProcessorMethod;
diff --git a/tools/winscope/src/trace_processor/query_result.ts b/tools/winscope/src/trace_processor/query_result.ts
index 4daa72c..24dd2e8 100644
--- a/tools/winscope/src/trace_processor/query_result.ts
+++ b/tools/winscope/src/trace_processor/query_result.ts
@@ -52,9 +52,7 @@
 // The Winscope parsers need the 64-bit proto fields to be retrieved as Long instead of number,
 // otherwise data (e.g. state flags) would be lost because of the 53-bit integer limitation.
 // import './static_initializers';
-
 import protobuf from 'protobufjs/minimal';
-
 import {defer, Deferred} from './deferred';
 import {assertExists, assertFalse, assertTrue} from './logging';
 import {utf8Decode} from './string_utils';
diff --git a/tools/winscope/src/trace_processor/string_utils.ts b/tools/winscope/src/trace_processor/string_utils.ts
index bc33b70..7f7a859 100644
--- a/tools/winscope/src/trace_processor/string_utils.ts
+++ b/tools/winscope/src/trace_processor/string_utils.ts
@@ -22,7 +22,6 @@
   read as utf8Read,
   write as utf8Write,
 } from '@protobufjs/utf8';
-
 import {assertTrue} from './logging';
 
 // TextDecoder/Decoder requires the full DOM and isn't available in all types
diff --git a/tools/winscope/src/trace_processor/wasm_engine_proxy.ts b/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
index 2cf01f6..9ea76d5 100644
--- a/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
+++ b/tools/winscope/src/trace_processor/wasm_engine_proxy.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {assertExists, assertTrue} from './logging';
-
 import {Engine, LoadingTracker} from './engine';
 
 let bundlePath: string;
diff --git a/tools/winscope/src/viewers/common/ime_utils.ts b/tools/winscope/src/viewers/common/ime_utils.ts
index 91d2dd1..242b43e 100644
--- a/tools/winscope/src/viewers/common/ime_utils.ts
+++ b/tools/winscope/src/viewers/common/ime_utils.ts
@@ -15,7 +15,6 @@
  */
 import {assertDefined} from 'common/assert_utils';
 import {Timestamp} from 'common/time';
-import {TimestampUtils} from 'common/timestamp_utils';
 import {Item} from 'trace/item';
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
@@ -86,9 +85,7 @@
     const displayContent = entry.getAllChildren()[0];
 
     const props: WmStateProperties = {
-      timestamp: wmEntryTimestamp
-        ? TimestampUtils.format(wmEntryTimestamp)
-        : undefined,
+      timestamp: wmEntryTimestamp ? wmEntryTimestamp.format() : undefined,
       focusedApp: entry.getEagerPropertyByName('focusedApp')?.getValue(),
       focusedWindow: this.getFocusedWindowString(entry),
       focusedActivity: this.getFocusedActivityString(entry),
@@ -171,7 +168,7 @@
     );
 
     const rootProperties = sfEntryTimestamp
-      ? {timestamp: TimestampUtils.format(sfEntryTimestamp)}
+      ? {timestamp: sfEntryTimestamp.format()}
       : undefined;
 
     return new ImeLayers(
diff --git a/tools/winscope/src/viewers/common/presenter_input_method.ts b/tools/winscope/src/viewers/common/presenter_input_method.ts
index 57192d7..e561766 100644
--- a/tools/winscope/src/viewers/common/presenter_input_method.ts
+++ b/tools/winscope/src/viewers/common/presenter_input_method.ts
@@ -17,7 +17,6 @@
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
 import {Timestamp} from 'common/time';
-import {TimestampUtils} from 'common/timestamp_utils';
 import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
 import {Trace, TraceEntry} from 'trace/trace';
 import {Traces} from 'trace/traces';
@@ -404,9 +403,7 @@
       return [undefined, undefined, undefined];
     }
 
-    this.currentImeEntryTimestamp = TimestampUtils.format(
-      imeEntry.getTimestamp(),
-    );
+    this.currentImeEntryTimestamp = imeEntry.getTimestamp().format();
 
     if (!this.imeTrace.hasFrameInfo()) {
       return [imeEntry, undefined, undefined];
diff --git a/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts b/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts
index 82e3580..151115f 100644
--- a/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts
+++ b/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts
@@ -21,9 +21,9 @@
 import {MatButtonModule} from '@angular/material/button';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
 import {ViewerEvents} from 'viewers/common/viewer_events';
@@ -59,9 +59,9 @@
         .setRootId('test node')
         .setName('timestamp')
         .setValue(
-          NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(1659126889102158832n),
+          TimestampConverterUtils.makeRealTimestamp(1659126889102158832n),
         )
-        .setFormatter(TIMESTAMP_FORMATTER)
+        .setFormatter(TIMESTAMP_NODE_FORMATTER)
         .build(),
     );
     component.node = node;
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
index bb88c3f..f0b293e 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {Trace} from 'trace/trace';
@@ -25,7 +25,7 @@
 import {TraceType} from 'trace/trace_type';
 import {
   DEFAULT_PROPERTY_FORMATTER,
-  TIMESTAMP_FORMATTER,
+  TIMESTAMP_NODE_FORMATTER,
 } from 'trace/tree_node/formatters';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {Presenter} from './presenter';
@@ -41,12 +41,12 @@
   let outputUiData: undefined | UiData;
 
   beforeEach(async () => {
-    const time10 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n);
-    const time11 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(11n);
-    const time12 = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(12n);
-    const elapsedTime10 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n);
-    const elapsedTime20 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(20n);
-    const elapsedTime30 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(30n);
+    const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
+    const time11 = TimestampConverterUtils.makeRealTimestamp(11n);
+    const time12 = TimestampConverterUtils.makeRealTimestamp(12n);
+    const elapsedTime10 = TimestampConverterUtils.makeElapsedTimestamp(10n);
+    const elapsedTime20 = TimestampConverterUtils.makeElapsedTimestamp(20n);
+    const elapsedTime30 = TimestampConverterUtils.makeElapsedTimestamp(30n);
 
     const entries = [
       new PropertyTreeBuilder()
@@ -57,7 +57,7 @@
           {
             name: 'timestamp',
             value: elapsedTime10,
-            formatter: TIMESTAMP_FORMATTER,
+            formatter: TIMESTAMP_NODE_FORMATTER,
           },
           {name: 'tag', value: 'tag0', formatter: DEFAULT_PROPERTY_FORMATTER},
           {
@@ -81,7 +81,7 @@
           {
             name: 'timestamp',
             value: elapsedTime20,
-            formatter: TIMESTAMP_FORMATTER,
+            formatter: TIMESTAMP_NODE_FORMATTER,
           },
           {name: 'tag', value: 'tag1', formatter: DEFAULT_PROPERTY_FORMATTER},
           {
@@ -105,7 +105,7 @@
           {
             name: 'timestamp',
             value: elapsedTime30,
-            formatter: TIMESTAMP_FORMATTER,
+            formatter: TIMESTAMP_NODE_FORMATTER,
           },
           {name: 'tag', value: 'tag2', formatter: DEFAULT_PROPERTY_FORMATTER},
           {
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
index 2636ec7..55f65cc 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
@@ -29,9 +29,9 @@
 import {MatSelectModule} from '@angular/material/select';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils';
 import {ViewerEvents} from 'viewers/common/viewer_events';
 import {SelectWithFilterComponent} from 'viewers/components/select_with_filter_component';
@@ -271,8 +271,8 @@
     const time = new PropertyTreeBuilder()
       .setRootId('ProtologMessage')
       .setName('timestamp')
-      .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n))
-      .setFormatter(TIMESTAMP_FORMATTER)
+      .setValue(TimestampConverterUtils.makeElapsedTimestamp(10n))
+      .setFormatter(TIMESTAMP_NODE_FORMATTER)
       .build();
 
     const messages = [];
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
index 83aefe3..418f7d7 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter.ts
@@ -16,7 +16,6 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
-import {TimestampUtils} from 'common/timestamp_utils';
 import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
 import {LayerFlag} from 'parsers/surface_flinger/layer_flag';
 import {Trace, TraceEntry} from 'trace/trace';
@@ -144,9 +143,7 @@
         );
         this.currentHierarchyTree = await entry?.getValue();
         if (entry) {
-          this.currentHierarchyTreeName = TimestampUtils.format(
-            entry.getTimestamp(),
-          );
+          this.currentHierarchyTreeName = entry.getTimestamp().format();
         }
 
         this.previousEntry =
diff --git a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
index 7382ea4..39c4e04 100644
--- a/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_surface_flinger/presenter_test.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {MockStorage} from 'test/unit/mock_storage';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TreeNodeUtils} from 'test/unit/tree_node_utils';
 import {UnitTestUtils} from 'test/unit/utils';
@@ -85,7 +85,7 @@
     const presenter = createPresenter(emptyTrace);
 
     const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n),
+      TimestampConverterUtils.makeRealTimestamp(0n),
     );
     await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
     expect(uiData.hierarchyUserOptions).toBeTruthy();
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter.ts b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
index 64badca..c9a538b 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
@@ -22,7 +22,7 @@
 import {Traces} from 'trace/traces';
 import {TraceEntryFinder} from 'trace/trace_entry_finder';
 import {TraceType} from 'trace/trace_type';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory';
 import {Filter} from 'viewers/common/operations/filter';
@@ -406,7 +406,7 @@
           'timestamp',
           entry.getTimestamp(),
         );
-      entryTimestamp.setFormatter(TIMESTAMP_FORMATTER);
+      entryTimestamp.setFormatter(TIMESTAMP_NODE_FORMATTER);
 
       for (const transactionState of assertDefined(
         entryNode.getChildByName('transactions'),
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
index 03b1c4f..279fd88 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
@@ -15,10 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {MockStorage} from 'test/unit/mock_storage';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TreeNodeUtils} from 'test/unit/tree_node_utils';
@@ -48,7 +47,7 @@
 
   beforeEach(async () => {
     outputUiData = undefined;
-    await setUpTestEnvironment(TimestampType.ELAPSED);
+    await setUpTestEnvironment();
   });
 
   it('is robust to empty trace', async () => {
@@ -75,7 +74,7 @@
     };
     await presenter.onAppEvent(
       TracePositionUpdate.fromTimestamp(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
       ),
     );
     expect(outputUiData).toEqual(UiData.EMPTY);
@@ -342,25 +341,15 @@
     expect(assertDefined(outputUiData).currentEntryIndex).toEqual(13);
   });
 
-  it('formats real time', async () => {
-    await setUpTestEnvironment(TimestampType.REAL);
+  it('formats entry time', async () => {
+    await setUpTestEnvironment();
     expect(
       assertDefined(outputUiData).entries[0].time.formattedValue(),
     ).toEqual('2022-08-03T06:19:01.051480997');
   });
 
-  it('formats elapsed time', async () => {
-    await setUpTestEnvironment(TimestampType.ELAPSED);
-    expect(
-      assertDefined(outputUiData).entries[0].time.formattedValue(),
-    ).toEqual('2s450ms981445ns');
-  });
-
-  async function setUpTestEnvironment(timestampType: TimestampType) {
-    trace = new TraceBuilder<PropertyTreeNode>()
-      .setParser(parser)
-      .setTimestampType(timestampType)
-      .build();
+  async function setUpTestEnvironment() {
+    trace = new TraceBuilder<PropertyTreeNode>().setParser(parser).build();
 
     traces = new Traces();
     traces.setTrace(TraceType.TRANSACTIONS, trace);
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
index c8d3143..b30a594 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
@@ -30,9 +30,9 @@
 import {MatSelectModule} from '@angular/material/select';
 import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils';
 import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
 import {ViewerEvents} from 'viewers/common/viewer_events';
@@ -267,8 +267,8 @@
       const time = new PropertyTreeBuilder()
         .setRootId(propertiesTree.id)
         .setName('timestamp')
-        .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1n))
-        .setFormatter(TIMESTAMP_FORMATTER)
+        .setValue(TimestampConverterUtils.makeElapsedTimestamp(1n))
+        .setFormatter(TIMESTAMP_NODE_FORMATTER)
         .build();
 
       const entry = new UiDataEntry(
@@ -352,8 +352,8 @@
       const time = new PropertyTreeBuilder()
         .setRootId(propertiesTree.id)
         .setName('timestamp')
-        .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1n))
-        .setFormatter(TIMESTAMP_FORMATTER)
+        .setValue(TimestampConverterUtils.makeElapsedTimestamp(1n))
+        .setFormatter(TIMESTAMP_NODE_FORMATTER)
         .build();
 
       const uiData = new UiData(
diff --git a/tools/winscope/src/viewers/viewer_transitions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transitions/presenter_test.ts
index 41a4461..257fd8e 100644
--- a/tools/winscope/src/viewers/viewer_transitions/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/presenter_test.ts
@@ -15,9 +15,8 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TracesBuilder} from 'test/unit/traces_builder';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {UnitTestUtils} from 'test/unit/utils';
@@ -39,7 +38,7 @@
 
     await presenter.onAppEvent(
       TracePositionUpdate.fromTimestamp(
-        NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(10n),
+        TimestampConverterUtils.makeRealTimestamp(10n),
       ),
     );
     expect(outputUiData).toEqual(UiData.EMPTY);
@@ -53,7 +52,6 @@
 
     const trace = new TraceBuilder<PropertyTreeNode>()
       .setParser(parser)
-      .setTimestampType(TimestampType.REAL)
       .build();
 
     const traces = new Traces();
@@ -74,7 +72,7 @@
     expect(wmData.getChildByName('id')?.formattedValue()).toEqual('32');
     expect(wmData.getChildByName('type')?.formattedValue()).toEqual('OPEN');
     expect(wmData.getChildByName('createTimeNs')?.formattedValue()).toEqual(
-      '2023-11-21T13:38:23.083364560',
+      '2023-11-21T13:30:25.428925648',
     );
   });
 });
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
index aa58118..cea2ff6 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
@@ -23,10 +23,9 @@
 import {MatDividerModule} from '@angular/material/divider';
 import {MatIconModule} from '@angular/material/icon';
 import {assertDefined} from 'common/assert_utils';
-import {TimestampType} from 'common/time';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {UnitTestUtils} from 'test/unit/utils';
 import {Parser} from 'trace/parser';
 import {Trace} from 'trace/trace';
@@ -34,7 +33,7 @@
 import {TracePosition} from 'trace/trace_position';
 import {TraceType} from 'trace/trace_type';
 import {Transition} from 'trace/transition';
-import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
+import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {ViewerEvents} from 'viewers/common/viewer_events';
 import {PropertiesComponent} from 'viewers/components/properties_component';
@@ -141,7 +140,7 @@
       'traces/elapsed_and_real_timestamp/wm_transition_trace.pb',
       'traces/elapsed_and_real_timestamp/shell_transition_trace.pb',
     ])) as Parser<PropertyTreeNode>;
-    const trace = Trace.fromParser(parser, TimestampType.REAL);
+    const trace = Trace.fromParser(parser);
     const traces = new Traces();
     traces.setTrace(TraceType.TRANSITION, trace);
 
@@ -233,9 +232,9 @@
     .setRootId(transitionTree.id)
     .setName('sendTimeNs')
     .setValue(
-      NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(BigInt(sendTimeNanos)),
+      TimestampConverterUtils.makeElapsedTimestamp(BigInt(sendTimeNanos)),
     )
-    .setFormatter(TIMESTAMP_FORMATTER)
+    .setFormatter(TIMESTAMP_NODE_FORMATTER)
     .build();
 
   return {
diff --git a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
index 1d757a6..1158bfe 100644
--- a/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_view_capture/presenter_test.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {MockStorage} from 'test/unit/mock_storage';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TreeNodeUtils} from 'test/unit/tree_node_utils';
 import {UnitTestUtils} from 'test/unit/utils';
@@ -88,7 +88,7 @@
     const presenter = createPresenter(emptyTrace);
 
     const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n),
+      TimestampConverterUtils.makeRealTimestamp(0n),
     );
     await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
     expect(uiData.hierarchyUserOptions).toBeTruthy();
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
index 8b2e747..ae1c155 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
@@ -16,7 +16,6 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {PersistentStoreProxy} from 'common/persistent_store_proxy';
-import {TimestampUtils} from 'common/timestamp_utils';
 import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
 import {Trace, TraceEntry} from 'trace/trace';
 import {Traces} from 'trace/traces';
@@ -131,9 +130,7 @@
         );
         this.currentHierarchyTree = await entry?.getValue();
         if (entry) {
-          this.currentHierarchyTreeName = TimestampUtils.format(
-            entry.getTimestamp(),
-          );
+          this.currentHierarchyTreeName = entry.getTimestamp().format();
         }
 
         this.previousEntry =
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
index 706c351..7ad5e3c 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
@@ -15,9 +15,9 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
-import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
 import {TracePositionUpdate} from 'messaging/winscope_event';
 import {MockStorage} from 'test/unit/mock_storage';
+import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
 import {TraceBuilder} from 'test/unit/trace_builder';
 import {TreeNodeUtils} from 'test/unit/tree_node_utils';
 import {UnitTestUtils} from 'test/unit/utils';
@@ -75,7 +75,7 @@
     expect(uiData.tree).toBeFalsy();
 
     const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
-      NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(0n),
+      TimestampConverterUtils.makeRealTimestamp(0n),
     );
     await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
     expect(uiData.hierarchyUserOptions).toBeTruthy();