[go: nahoru, domu]

Remove TempBackingStorage

This storage mechanism was used for storing Perf Panel traces but we do
not need it any more and can move away from it.

Longer term there is more we can do here to tidy up the way loading a
file works in the Perf Panel, but as a start this CL removes the
TempBackingStorage that maintained the JSON of the trace. Now that the
file saving is done by encoding the JSON from the new engine
(crrev.com/c/4381791), we do not rely on the backing storage to generate
the JSON that we then write to disk.

Bug: 1428842
Change-Id: Ic9cfd12b8aa7425cfab4b4392520ea03571069e2
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4562037
Reviewed-by: Paul Irish <paulirish@chromium.org>
Commit-Queue: Jack Franklin <jacktfranklin@chromium.org>
diff --git a/front_end/core/sdk/TracingModel.ts b/front_end/core/sdk/TracingModel.ts
index 770ce8b..fba6622 100644
--- a/front_end/core/sdk/TracingModel.ts
+++ b/front_end/core/sdk/TracingModel.ts
@@ -12,10 +12,7 @@
 };
 
 export class TracingModel {
-  #backingStorageInternal: BackingStorage;
-  readonly #shouldSaveToFile: boolean;
   readonly #title: string|undefined;
-  #firstWritePending: boolean;
   readonly #processById: Map<string|number, Process>;
   readonly #processByName: Map<string, Process>;
   #minimumRecordTimeInternal: number;
@@ -29,12 +26,8 @@
   readonly #mainFrameNavStartTimes: Map<string, PayloadEvent>;
   readonly #allEventsPayload: EventPayload[] = [];
 
-  constructor(backingStorage: BackingStorage, shouldSaveToFile = true, title?: string) {
-    this.#backingStorageInternal = backingStorage;
-    this.#shouldSaveToFile = shouldSaveToFile;
+  constructor(title?: string) {
     this.#title = title;
-    // Avoid extra reset of the storage as it's expensive.
-    this.#firstWritePending = true;
     this.#processById = new Map();
     this.#processByName = new Map();
     this.#minimumRecordTimeInternal = Number(Infinity);
@@ -117,11 +110,6 @@
 
   tracingComplete(): void {
     this.processPendingAsyncEvents();
-    if (this.#shouldSaveToFile) {
-      this.#backingStorageInternal.appendString(this.#firstWritePending ? '[]' : ']');
-      this.#backingStorageInternal.finishWriting();
-      this.#firstWritePending = false;
-    }
     for (const process of this.#processById.values()) {
       for (const thread of process.threads.values()) {
         thread.tracingComplete();
@@ -129,12 +117,6 @@
     }
   }
 
-  dispose(): void {
-    if (!this.#firstWritePending && this.#shouldSaveToFile) {
-      this.#backingStorageInternal.reset();
-    }
-  }
-
   private addEvent(payload: EventPayload): void {
     this.#allEventsPayload.push(payload);
     let process = this.#processById.get(payload.pid);
@@ -143,21 +125,6 @@
       this.#processById.set(payload.pid, process);
     }
 
-    let backingStorage: (() => Promise<string|null>)|null = null;
-    if (this.#shouldSaveToFile) {
-      const eventsDelimiter = ',\n';
-      this.#backingStorageInternal.appendString(this.#firstWritePending ? '[' : eventsDelimiter);
-      this.#firstWritePending = false;
-      const stringPayload = JSON.stringify(payload);
-      const isAccessible = payload.ph === TraceEngine.Types.TraceEvents.Phase.OBJECT_SNAPSHOT;
-      const keepStringsLessThan = 10000;
-      if (isAccessible && stringPayload.length > keepStringsLessThan) {
-        backingStorage = this.#backingStorageInternal.appendAccessibleString(stringPayload);
-      } else {
-        this.#backingStorageInternal.appendString(stringPayload);
-      }
-    }
-
     const timestamp = payload.ts / 1000;
     // We do allow records for unrelated threads to arrive out-of-order,
     // so there's a chance we're getting records from the past.
@@ -213,7 +180,6 @@
     if (TraceEngine.Types.TraceEvents.isAsyncPhase(payload.ph)) {
       this.#asyncEvents.push((event as AsyncEvent));
     }
-    event.setBackingStorage(backingStorage);
     if (event.hasCategory(DevToolsMetadataEventCategory)) {
       this.#devToolsMetadataEventsInternal.push(event);
     }
@@ -406,10 +372,6 @@
     console.assert(false, 'Invalid async event phase');
   }
 
-  backingStorage(): BackingStorage {
-    return this.#backingStorageInternal;
-  }
-
   title(): string|undefined {
     return this.#title;
   }
diff --git a/front_end/legacy_test_runner/performance_test_runner/TimelineTestRunner.js b/front_end/legacy_test_runner/performance_test_runner/TimelineTestRunner.js
index 635bd06..594d26f 100644
--- a/front_end/legacy_test_runner/performance_test_runner/TimelineTestRunner.js
+++ b/front_end/legacy_test_runner/performance_test_runner/TimelineTestRunner.js
@@ -73,7 +73,7 @@
 };
 
 PerformanceTestRunner.createTracingModel = function(events) {
-  const model = new SDK.TracingModel(new Bindings.TempFileBackingStorage('tracing'));
+  const model = new SDK.TracingModel();
   model.addEvents(events);
   model.tracingComplete();
   return model;
@@ -119,7 +119,7 @@
 };
 
 PerformanceTestRunner.createPerformanceModelWithEvents = async function(events) {
-  const tracingModel = new SDK.TracingModel(new Bindings.TempFileBackingStorage('tracing'));
+  const tracingModel = new SDK.TracingModel();
   tracingModel.addEvents(events);
   tracingModel.tracingComplete();
   const performanceModel = new Timeline.PerformanceModel();
diff --git a/front_end/models/bindings/TempFile.ts b/front_end/models/bindings/TempFile.ts
index a4a0214..1a79157 100644
--- a/front_end/models/bindings/TempFile.ts
+++ b/front_end/models/bindings/TempFile.ts
@@ -30,8 +30,6 @@
 
 import * as Common from '../../core/common/common.js';
 
-import type * as SDK from '../../core/sdk/sdk.js';
-
 import {ChunkedFileReader, type ChunkedReader} from './FileUtils.js';
 
 export class TempFile {
@@ -93,62 +91,3 @@
     this.#lastBlob = null;
   }
 }
-
-export class TempFileBackingStorage implements SDK.TracingModel.BackingStorage {
-  #file: TempFile|null;
-  #strings!: string[];
-  #stringsLength!: number;
-
-  constructor() {
-    this.#file = null;
-    this.reset();
-  }
-
-  appendString(string: string): void {
-    this.#strings.push(string);
-    this.#stringsLength += string.length;
-    const flushStringLength = 10 * 1024 * 1024;
-    if (this.#stringsLength > flushStringLength) {
-      this.flush();
-    }
-  }
-
-  appendAccessibleString(string: string): () => Promise<string|null> {
-    this.flush();
-    if (!this.#file) {
-      return async(): Promise<null> => null;
-    }
-    const startOffset = this.#file.size();
-    this.#strings.push(string);
-    this.flush();
-    return this.#file.readRange.bind(this.#file, startOffset, this.#file.size());
-  }
-
-  private flush(): void {
-    if (!this.#strings.length) {
-      return;
-    }
-    if (!this.#file) {
-      this.#file = new TempFile();
-    }
-    this.#stringsLength = 0;
-    this.#file.write(this.#strings.splice(0));
-  }
-
-  finishWriting(): void {
-    this.flush();
-  }
-
-  reset(): void {
-    if (this.#file) {
-      this.#file.remove();
-    }
-    this.#file = null;
-    this.#strings = [];
-    this.#stringsLength = 0;
-  }
-
-  writeToStream(outputStream: Common.StringOutputStream.OutputStream): Promise<DOMError|null> {
-    return this.#file ? this.#file.copyToOutputStream(outputStream) : Promise.resolve(null);
-  }
-}
diff --git a/front_end/models/bindings/bindings-legacy.ts b/front_end/models/bindings/bindings-legacy.ts
index 06c03ba..adfb04a 100644
--- a/front_end/models/bindings/bindings-legacy.ts
+++ b/front_end/models/bindings/bindings-legacy.ts
@@ -86,6 +86,3 @@
 
 /** @constructor */
 Bindings.TempFile = BindingsModule.TempFile.TempFile;
-
-/** @constructor */
-Bindings.TempFileBackingStorage = BindingsModule.TempFile.TempFileBackingStorage;
diff --git a/front_end/panels/network/NetworkPanel.ts b/front_end/panels/network/NetworkPanel.ts
index b327ebf..08b0e85 100644
--- a/front_end/panels/network/NetworkPanel.ts
+++ b/front_end/panels/network/NetworkPanel.ts
@@ -929,10 +929,7 @@
 
     this.tracingManager = tracingManager;
     this.resourceTreeModel = this.tracingManager.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
-    if (this.tracingModel) {
-      this.tracingModel.dispose();
-    }
-    this.tracingModel = new SDK.TracingModel.TracingModel(new Bindings.TempFile.TempFileBackingStorage());
+    this.tracingModel = new SDK.TracingModel.TracingModel();
     void this.tracingManager.start(this, '-*,disabled-by-default-devtools.screenshot', '');
 
     Host.userMetrics.actionTaken(Host.UserMetrics.Action.FilmStripStartedRecording);
diff --git a/front_end/panels/timeline/PerformanceModel.ts b/front_end/panels/timeline/PerformanceModel.ts
index d90bcfb..5c14222 100644
--- a/front_end/panels/timeline/PerformanceModel.ts
+++ b/front_end/panels/timeline/PerformanceModel.ts
@@ -182,12 +182,6 @@
     return this.frameModelInternal;
   }
 
-  dispose(): void {
-    if (this.tracingModelInternal) {
-      this.tracingModelInternal.dispose();
-    }
-  }
-
   filmStripModelFrame(frame: TimelineModel.TimelineFrameModel.TimelineFrame): SDK.FilmStripModel.Frame|null {
     // For idle frames, look at the state at the beginning of the frame.
     const screenshotTime = frame.idle ? frame.startTime : frame.endTime;
diff --git a/front_end/panels/timeline/TimelineController.ts b/front_end/panels/timeline/TimelineController.ts
index dc7c029..ad66f52 100644
--- a/front_end/panels/timeline/TimelineController.ts
+++ b/front_end/panels/timeline/TimelineController.ts
@@ -7,7 +7,6 @@
 import * as Platform from '../../core/platform/platform.js';
 import * as Root from '../../core/root/root.js';
 import * as SDK from '../../core/sdk/sdk.js';
-import * as Bindings from '../../models/bindings/bindings.js';
 import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
 import * as TraceEngine from '../../models/trace/trace.js';
 import type * as Protocol from '../../generated/protocol.js';
@@ -53,9 +52,7 @@
     this.performanceModel = new PerformanceModel();
     this.performanceModel.setMainTarget(target);
     this.client = client;
-
-    const backingStorage = new Bindings.TempFile.TempFileBackingStorage();
-    this.tracingModel = new SDK.TracingModel.TracingModel(backingStorage);
+    this.tracingModel = new SDK.TracingModel.TracingModel();
 
     SDK.TargetManager.TargetManager.instance().observeModels(SDK.CPUProfilerModel.CPUProfilerModel, this);
   }
@@ -360,7 +357,7 @@
   loadingProgress(progress?: number): void;
   loadingComplete(
       tracingModel: SDK.TracingModel.TracingModel|null,
-      exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null): void;
+      exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null): Promise<void>;
   loadingCompleteForTest(): void;
 }
 export interface RecordingOptions {
diff --git a/front_end/panels/timeline/TimelineHistoryManager.ts b/front_end/panels/timeline/TimelineHistoryManager.ts
index 01887af..50220d4 100644
--- a/front_end/panels/timeline/TimelineHistoryManager.ts
+++ b/front_end/panels/timeline/TimelineHistoryManager.ts
@@ -118,7 +118,6 @@
     const modelUsedMoreTimeAgo =
         this.recordings.reduce((a, b) => lastUsedTime(a.legacyModel) < lastUsedTime(b.legacyModel) ? a : b);
     this.recordings.splice(this.recordings.indexOf(modelUsedMoreTimeAgo), 1);
-    modelUsedMoreTimeAgo.legacyModel.dispose();
 
     function lastUsedTime(model: PerformanceModel): number {
       const data = TimelineHistoryManager.dataForModel(model);
@@ -139,7 +138,6 @@
   }
 
   clear(): void {
-    this.recordings.forEach(model => model.legacyModel.dispose());
     this.recordings = [];
     this.lastActiveModel = null;
     this.updateState();
diff --git a/front_end/panels/timeline/TimelineLoader.ts b/front_end/panels/timeline/TimelineLoader.ts
index 45820ab..23acc57 100644
--- a/front_end/panels/timeline/TimelineLoader.ts
+++ b/front_end/panels/timeline/TimelineLoader.ts
@@ -47,7 +47,6 @@
  */
 export class TimelineLoader implements Common.StringOutputStream.OutputStream {
   private client: Client|null;
-  private readonly backingStorage: Bindings.TempFile.TempFileBackingStorage;
   private tracingModel: SDK.TracingModel.TracingModel|null;
   private canceledCallback: (() => void)|null;
   private state: State;
@@ -58,12 +57,9 @@
   private totalSize!: number;
   private readonly jsonTokenizer: TextUtils.TextUtils.BalancedJSONTokenizer;
   private filter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null;
-  constructor(client: Client, shouldSaveTraceEventsToFile: boolean, title?: string) {
+  constructor(client: Client, title?: string) {
     this.client = client;
-
-    this.backingStorage = new Bindings.TempFile.TempFileBackingStorage();
-    this.tracingModel = new SDK.TracingModel.TracingModel(this.backingStorage, shouldSaveTraceEventsToFile, title);
-
+    this.tracingModel = new SDK.TracingModel.TracingModel(title);
     this.canceledCallback = null;
     this.state = State.Initial;
     this.buffer = '';
@@ -75,7 +71,7 @@
   }
 
   static async loadFromFile(file: File, client: Client): Promise<TimelineLoader> {
-    const loader = new TimelineLoader(client, /* shouldSaveTraceEventsToFile= */ true);
+    const loader = new TimelineLoader(client);
     const fileReader = new Bindings.FileUtils.ChunkedFileReader(file, TransferChunkLengthBytes);
     loader.canceledCallback = fileReader.cancel.bind(fileReader);
     loader.totalSize = file.size;
@@ -89,7 +85,7 @@
   }
 
   static loadFromEvents(events: SDK.TracingManager.EventPayload[], client: Client): TimelineLoader {
-    const loader = new TimelineLoader(client, /* shouldSaveTraceEventsToFile= */ true);
+    const loader = new TimelineLoader(client);
     window.setTimeout(async () => {
       void loader.addEvents(events);
     });
@@ -105,15 +101,12 @@
   }
 
   static loadFromCpuProfile(profile: Protocol.Profiler.Profile|null, client: Client, title?: string): TimelineLoader {
-    const loader = new TimelineLoader(client, /* shouldSaveTraceEventsToFile= */ false, title);
+    const loader = new TimelineLoader(client, title);
 
     try {
       const events = TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile(
           profile, /* tid */ 1, /* injectPageEvent */ true);
 
-      loader.backingStorage.appendString(JSON.stringify(profile));
-      loader.backingStorage.finishWriting();
-
       loader.filter = TimelineLoader.getCpuProfileFilter();
 
       window.setTimeout(async () => {
@@ -125,10 +118,10 @@
     return loader;
   }
 
-  static loadFromURL(url: Platform.DevToolsPath.UrlString, client: Client): TimelineLoader {
-    const loader = new TimelineLoader(client, /* shouldSaveTraceEventsToFile= */ true);
+  static async loadFromURL(url: Platform.DevToolsPath.UrlString, client: Client): Promise<TimelineLoader> {
+    const loader = new TimelineLoader(client);
     const stream = new Common.StringOutputStream.StringOutputStream();
-    client.loadingStarted();
+    await client.loadingStarted();
 
     const allowRemoteFilePaths =
         Common.Settings.Settings.instance().moduleSetting('network.enable-remote-file-loading').get();
@@ -156,22 +149,21 @@
   }
 
   async addEvents(events: SDK.TracingManager.EventPayload[]): Promise<void> {
-    this.client?.loadingStarted();
+    await this.client?.loadingStarted();
     const eventsPerChunk = 15_000;
     for (let i = 0; i < events.length; i += eventsPerChunk) {
       const chunk = events.slice(i, i + eventsPerChunk);
       (this.tracingModel as SDK.TracingModel.TracingModel).addEvents(chunk);
-      this.client?.loadingProgress((i + chunk.length) / events.length);
+      await this.client?.loadingProgress((i + chunk.length) / events.length);
       await new Promise(r => window.setTimeout(r));  // Yield event loop to paint.
     }
     void this.close();
   }
 
-  cancel(): void {
+  async cancel(): Promise<void> {
     this.tracingModel = null;
-    this.backingStorage.reset();
     if (this.client) {
-      this.client.loadingComplete(null, null);
+      await this.client.loadingComplete(null, null);
       this.client = null;
     }
     if (this.canceledCallback) {
@@ -232,6 +224,11 @@
     if (this.state !== State.ReadingEvents) {
       return Promise.resolve();
     }
+    // This is where we actually do the loading of events from JSON: the JSON
+    // Tokenizer writes the JSON to a buffer, and then as a callback the
+    // writeBalancedJSON method below is invoked. It then parses this chunk
+    // of JSON as a set of events, and adds them to the TracingModel via
+    // addEvents()
     if (this.jsonTokenizer.write(chunk)) {
       return Promise.resolve();
     }
@@ -280,7 +277,7 @@
     if (message) {
       Common.Console.Console.instance().error(message);
     }
-    this.cancel();
+    void this.cancel();
   }
 
   // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
@@ -293,7 +290,7 @@
     if (!this.client) {
       return;
     }
-    this.client.processingStarted();
+    await this.client.processingStarted();
     await this.finalizeTrace();
   }
 
@@ -324,15 +321,15 @@
 export const TransferChunkLengthBytes = 5000000;
 
 export interface Client {
-  loadingStarted(): void;
+  loadingStarted(): Promise<void>;
 
-  loadingProgress(progress?: number): void;
+  loadingProgress(progress?: number): Promise<void>;
 
-  processingStarted(): void;
+  processingStarted(): Promise<void>;
 
   loadingComplete(
       tracingModel: SDK.TracingModel.TracingModel|null,
-      exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null): void;
+      exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null): Promise<void>;
 }
 
 // TODO(crbug.com/1167717): Make this a const enum again
diff --git a/front_end/panels/timeline/TimelinePanel.ts b/front_end/panels/timeline/TimelinePanel.ts
index 3f7426c..26354d0 100644
--- a/front_end/panels/timeline/TimelinePanel.ts
+++ b/front_end/panels/timeline/TimelinePanel.ts
@@ -629,7 +629,6 @@
     console.assert(this.state === State.Idle);
     this.setState(State.Loading);
     if (this.performanceModel) {
-      this.performanceModel.dispose();
       this.performanceModel = null;
     }
   }
@@ -723,12 +722,12 @@
     this.createFileSelector();
   }
 
-  loadFromURL(url: Platform.DevToolsPath.UrlString): void {
+  async loadFromURL(url: Platform.DevToolsPath.UrlString): Promise<void> {
     if (this.state !== State.Idle) {
       return;
     }
     this.prepareToLoadTimeline();
-    this.loader = TimelineLoader.loadFromURL(url, this);
+    this.loader = await TimelineLoader.loadFromURL(url, this);
   }
 
   private updateOverviewControls(): void {
@@ -1181,7 +1180,7 @@
     this.landingPage.detach();
   }
 
-  loadingStarted(): void {
+  async loadingStarted(): Promise<void> {
     this.hideLandingPage();
 
     if (this.statusPane) {
@@ -1202,16 +1201,16 @@
     if (!this.loader) {
       this.statusPane.finish();
     }
-    this.loadingProgress(0);
+    await this.loadingProgress(0);
   }
 
-  loadingProgress(progress?: number): void {
+  async loadingProgress(progress?: number): Promise<void> {
     if (typeof progress === 'number' && this.statusPane) {
       this.statusPane.updateProgressBar(i18nString(UIStrings.received), progress * 100);
     }
   }
 
-  processingStarted(): void {
+  async processingStarted(): Promise<void> {
     if (this.statusPane) {
       this.statusPane.updateStatus(i18nString(UIStrings.processingProfile));
     }
@@ -1327,7 +1326,7 @@
 
   private cancelLoading(): void {
     if (this.loader) {
-      this.loader.cancel();
+      void this.loader.cancel();
     }
   }
 
@@ -1453,7 +1452,7 @@
     if (item.kind === 'string') {
       const url = dataTransfer.getData('text/uri-list') as Platform.DevToolsPath.UrlString;
       if (new Common.ParsedURL.ParsedURL(url).isValid) {
-        this.loadFromURL(url);
+        void this.loadFromURL(url);
       }
     } else if (item.kind === 'file') {
       const file = items[0].getAsFile();
@@ -1625,8 +1624,8 @@
   }
 
   handleQueryParam(value: string): void {
-    void UI.ViewManager.ViewManager.instance().showView('timeline').then(() => {
-      TimelinePanel.instance().loadFromURL(window.decodeURIComponent(value) as Platform.DevToolsPath.UrlString);
+    void UI.ViewManager.ViewManager.instance().showView('timeline').then(async () => {
+      await TimelinePanel.instance().loadFromURL(window.decodeURIComponent(value) as Platform.DevToolsPath.UrlString);
     });
   }
 }
diff --git a/test/unittests/front_end/core/sdk/TracingModel_test.ts b/test/unittests/front_end/core/sdk/TracingModel_test.ts
index 2489f0f..30746c2 100644
--- a/test/unittests/front_end/core/sdk/TracingModel_test.ts
+++ b/test/unittests/front_end/core/sdk/TracingModel_test.ts
@@ -5,14 +5,14 @@
 const {assert} = chai;
 
 import * as SDK from '../../../../../front_end/core/sdk/sdk.js';
-import {loadTraceEventsLegacyEventPayload, allModelsFromFile} from '../../helpers/TraceHelpers.js';
-import {FakeStorage, StubbedThread} from '../../helpers/TimelineHelpers.js';
 import {describeWithEnvironment} from '../../helpers/EnvironmentHelpers.js';
+import {loadTraceEventsLegacyEventPayload, allModelsFromFile} from '../../helpers/TraceHelpers.js';
+import {StubbedThread} from '../../helpers/TimelineHelpers.js';
 
 describeWithEnvironment('TracingModel', () => {
   it('can create events from an EventPayload[] and finds the correct number of processes', async () => {
     const events = await loadTraceEventsLegacyEventPayload('basic.json.gz');
-    const model = new SDK.TracingModel.TracingModel(new FakeStorage());
+    const model = new SDK.TracingModel.TracingModel();
     model.addEvents(events);
     assert.strictEqual(model.sortedProcesses().length, 4);
   });
diff --git a/test/unittests/front_end/helpers/TimelineHelpers.ts b/test/unittests/front_end/helpers/TimelineHelpers.ts
index d5b9eb1..ff99b58 100644
--- a/test/unittests/front_end/helpers/TimelineHelpers.ts
+++ b/test/unittests/front_end/helpers/TimelineHelpers.ts
@@ -9,21 +9,6 @@
 import * as Timeline from '../../../../front_end/panels/timeline/timeline.js';
 import {loadTraceEventsLegacyEventPayload} from './TraceHelpers.js';
 
-export class FakeStorage extends SDK.TracingModel.BackingStorage {
-  override appendString() {
-  }
-
-  appendAccessibleString(x: string): () => Promise<string|null> {
-    return () => Promise.resolve(x);
-  }
-
-  override finishWriting() {
-  }
-
-  override reset() {
-  }
-}
-
 /**
  * Provides a stubbed SDK.TracingModel.Thread instance.
  * IMPORTANT: this is not designed to be a fully stubbed Thread, but one that is
@@ -116,7 +101,7 @@
   performanceModel: Timeline.PerformanceModel.PerformanceModel,
 }> {
   const events = await loadTraceEventsLegacyEventPayload(file);
-  const tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+  const tracingModel = new SDK.TracingModel.TracingModel();
   const performanceModel = new Timeline.PerformanceModel.PerformanceModel();
   tracingModel.addEvents(events);
   tracingModel.tracingComplete();
diff --git a/test/unittests/front_end/helpers/TraceHelpers.ts b/test/unittests/front_end/helpers/TraceHelpers.ts
index 1d96d32..31532ac 100644
--- a/test/unittests/front_end/helpers/TraceHelpers.ts
+++ b/test/unittests/front_end/helpers/TraceHelpers.ts
@@ -8,7 +8,6 @@
 import * as PerfUI from '../../../../front_end/ui/legacy/components/perf_ui/perf_ui.js';
 import {initializeGlobalVars} from './EnvironmentHelpers.js';
 
-import {FakeStorage} from './TimelineHelpers.js';
 interface CompressionStream extends ReadableWritablePair<Uint8Array, Uint8Array> {}
 interface DecompressionStream extends ReadableWritablePair<Uint8Array, Uint8Array> {}
 declare const CompressionStream: {
@@ -250,7 +249,7 @@
 }> {
   const traceParsedData = await loadModelDataFromTraceFile(file);
   const events = await loadTraceEventsLegacyEventPayload(file);
-  const tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+  const tracingModel = new SDK.TracingModel.TracingModel();
   const performanceModel = new Timeline.PerformanceModel.PerformanceModel();
   tracingModel.addEvents(events);
   tracingModel.tracingComplete();
diff --git a/test/unittests/front_end/models/timeline_model/TimelineJSProfile_test.ts b/test/unittests/front_end/models/timeline_model/TimelineJSProfile_test.ts
index e7a52aa..d7e73dd 100644
--- a/test/unittests/front_end/models/timeline_model/TimelineJSProfile_test.ts
+++ b/test/unittests/front_end/models/timeline_model/TimelineJSProfile_test.ts
@@ -8,7 +8,6 @@
 
 import * as TimelineModel from '../../../../../front_end/models/timeline_model/timeline_model.js';
 import * as TraceEngine from '../../../../../front_end/models/trace/trace.js';
-import {FakeStorage} from '../../helpers/TimelineHelpers.js';
 
 describe('TimelineJSProfile', () => {
   let tracingModel: SDK.TracingModel.TracingModel;
@@ -22,7 +21,7 @@
   };
 
   before(() => {
-    tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+    tracingModel = new SDK.TracingModel.TracingModel();
     process = new SDK.TracingModel.Process(tracingModel, 1);
     thread = new SDK.TracingModel.Thread(process, 1);
   });
diff --git a/test/unittests/front_end/models/timeline_model/TimelineModel_test.ts b/test/unittests/front_end/models/timeline_model/TimelineModel_test.ts
index f411ca3..6b196c5 100644
--- a/test/unittests/front_end/models/timeline_model/TimelineModel_test.ts
+++ b/test/unittests/front_end/models/timeline_model/TimelineModel_test.ts
@@ -13,7 +13,6 @@
 import {describeWithEnvironment} from '../../helpers/EnvironmentHelpers.js';
 import {
   DevToolsTimelineCategory,
-  FakeStorage,
   makeFakeSDKEventFromPayload,
   traceModelFromTraceFile,
 } from '../../helpers/TimelineHelpers.js';
@@ -192,7 +191,7 @@
     tracingModel: SDK.TracingModel.TracingModel,
     timelineModel: TimelineModel.TimelineModel.TimelineModelImpl,
   } {
-    const tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+    const tracingModel = new SDK.TracingModel.TracingModel();
     const timelineModel = new TimelineModel.TimelineModel.TimelineModelImpl();
     tracingModel.addEvents((preamble as unknown as SDK.TracingManager.EventPayload[]).concat(events));
     tracingModel.tracingComplete();
@@ -403,7 +402,7 @@
     let nestableAsyncEvents: SDK.TracingModel.AsyncEvent[];
     let nonNestableAsyncEvents: SDK.TracingModel.AsyncEvent[];
     let syncEvents: SDK.TracingModel.PayloadEvent[];
-    const tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+    const tracingModel = new SDK.TracingModel.TracingModel();
     const process = new SDK.TracingModel.Process(tracingModel, 1);
     const thread = new SDK.TracingModel.Thread(process, 1);
     const nestableAsyncEventPayloads = [
diff --git a/test/unittests/front_end/panels/timeline/BUILD.gn b/test/unittests/front_end/panels/timeline/BUILD.gn
index e3901d5..63f671a 100644
--- a/test/unittests/front_end/panels/timeline/BUILD.gn
+++ b/test/unittests/front_end/panels/timeline/BUILD.gn
@@ -13,6 +13,7 @@
     "TimelineFlameChartDataProvider_test.ts",
     "TimelineFlameChartView_test.ts",
     "TimelineHistoryManager_test.ts",
+    "TimelineLoader_test.ts",
     "TimelineSelection_test.ts",
     "TimelineTreeView_test.ts",
     "TimelineUIUtils_test.ts",
diff --git a/test/unittests/front_end/panels/timeline/EventTypeHelpers_test.ts b/test/unittests/front_end/panels/timeline/EventTypeHelpers_test.ts
index 0c470e0..2ad295e 100644
--- a/test/unittests/front_end/panels/timeline/EventTypeHelpers_test.ts
+++ b/test/unittests/front_end/panels/timeline/EventTypeHelpers_test.ts
@@ -9,7 +9,6 @@
   makeFakeSDKEventFromPayload,
   type FakeEventPayload,
   makeFakeEventPayload,
-  FakeStorage,
 } from '../../helpers/TimelineHelpers.js';
 import {defaultTraceEvent} from '../../helpers/TraceHelpers.js';
 
@@ -146,7 +145,7 @@
         dur: 5_000,
       };
       const payload = makeFakeEventPayload(fakePayload);
-      const tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+      const tracingModel = new SDK.TracingModel.TracingModel();
       const process = new SDK.TracingModel.Process(tracingModel, 1);
       const thread = new SDK.TracingModel.Thread(process, 1);
       const event = SDK.TracingModel.PayloadEvent.fromPayload(payload, thread);
diff --git a/test/unittests/front_end/panels/timeline/SourceMaps_test.ts b/test/unittests/front_end/panels/timeline/SourceMaps_test.ts
index b82cdd8..318f943 100644
--- a/test/unittests/front_end/panels/timeline/SourceMaps_test.ts
+++ b/test/unittests/front_end/panels/timeline/SourceMaps_test.ts
@@ -20,7 +20,6 @@
   dispatchEvent,
   setMockConnectionResponseHandler,
 } from '../../helpers/MockConnection.js';
-import {FakeStorage} from '../../helpers/TimelineHelpers.js';
 
 const {assert} = chai;
 
@@ -94,7 +93,7 @@
     performanceModel = new Timeline.PerformanceModel.PerformanceModel();
     const traceEvents = TimelineModel.TimelineJSProfile.TimelineJSProfileProcessor.createFakeTraceFromCpuProfile(
         profile, 1, false, 'mock-name');
-    tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+    tracingModel = new SDK.TracingModel.TracingModel();
     tracingModel.addEvents(traceEvents);
     await performanceModel.setTracingModel(tracingModel);
     const workspace = Workspace.Workspace.WorkspaceImpl.instance();
diff --git a/test/unittests/front_end/panels/timeline/TimelineLoader_test.ts b/test/unittests/front_end/panels/timeline/TimelineLoader_test.ts
new file mode 100644
index 0000000..322d3da
--- /dev/null
+++ b/test/unittests/front_end/panels/timeline/TimelineLoader_test.ts
@@ -0,0 +1,68 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import * as Timeline from '../../../../../front_end/panels/timeline/timeline.js';
+import type * as TimelineModel from '../../../../../front_end/models/timeline_model/timeline_model.js';
+import type * as SDK from '../../../../../front_end/core/sdk/sdk.js';
+
+async function loadWebDevTraceAsFile(): Promise<File> {
+  const file = new URL('../../../fixtures/traces/web-dev.json.gz', import.meta.url);
+  const response = await fetch(file);
+  const asBlob = await response.blob();
+  const asFile = new File([asBlob], 'web-dev.json.gz', {
+    type: 'application/gzip',
+  });
+  return asFile;
+}
+
+describe('TimelineLoader', () => {
+  it('can load a saved file', async () => {
+    const file = await loadWebDevTraceAsFile();
+
+    const loadingStartedSpy = sinon.spy();
+    const loadingProgressSpy = sinon.spy();
+    const processingStartedSpy = sinon.spy();
+    const loadingCompleteSpy = sinon.spy();
+
+    const client: Timeline.TimelineLoader.Client = {
+      async loadingStarted() {
+        loadingStartedSpy();
+      },
+      async loadingProgress(progress?: number) {
+        loadingProgressSpy(progress);
+      },
+      async processingStarted() {
+        processingStartedSpy();
+      },
+      async loadingComplete(
+          tracingModel: SDK.TracingModel.TracingModel|null,
+          exclusiveFilter: TimelineModel.TimelineModelFilter.TimelineModelFilter|null,
+      ) {
+        loadingCompleteSpy(tracingModel, exclusiveFilter);
+      },
+    };
+    await Timeline.TimelineLoader.TimelineLoader.loadFromFile(file, client);
+
+    assert.isTrue(loadingStartedSpy.calledOnce);
+    // Exact number is deterministic so we can assert, but the fact it was 28
+    // calls doesn't really matter. We just want to check it got called "a
+    // bunch of times".
+    assert.strictEqual(loadingProgressSpy.callCount, 28);
+    assert.isTrue(processingStartedSpy.calledOnce);
+    assert.isTrue(loadingCompleteSpy.calledOnce);
+
+    // Get the arguments of the first (and only) call to the loadingComplete
+    // function. TS doesn't know what the types are (they are [any, any] by
+    // default), so we tell it that they align with the types of the
+    // loadingComplete parameters.
+    const [tracingModel, exclusiveFilter] =
+        loadingCompleteSpy.args[0] as Parameters<Timeline.TimelineLoader.Client['loadingComplete']>;
+    assert.isNull(exclusiveFilter);  // We are not filtering out any events for this trace.
+    if (!tracingModel) {
+      throw new Error('No tracing model found from results of loadTraceFromFile');
+    }
+    // Ensure that we loaded something that looks about right!
+    assert.lengthOf(tracingModel.allRawEvents(), 8252);
+  });
+});
diff --git a/test/unittests/front_end/panels/timeline/TimelineUIUtils_test.ts b/test/unittests/front_end/panels/timeline/TimelineUIUtils_test.ts
index 01fa038..77966c7 100644
--- a/test/unittests/front_end/panels/timeline/TimelineUIUtils_test.ts
+++ b/test/unittests/front_end/panels/timeline/TimelineUIUtils_test.ts
@@ -10,7 +10,6 @@
 import * as Timeline from '../../../../../front_end/panels/timeline/timeline.js';
 import {createTarget} from '../../helpers/EnvironmentHelpers.js';
 import {describeWithMockConnection} from '../../helpers/MockConnection.js';
-import {FakeStorage} from '../../helpers/TimelineHelpers.js';
 import * as Workspace from '../../../../../front_end/models/workspace/workspace.js';
 import * as Bindings from '../../../../../front_end/models/bindings/bindings.js';
 import {setupPageResourceLoaderForSourceMap} from '../../helpers/SourceMapHelpers.js';
@@ -29,7 +28,7 @@
 
   beforeEach(() => {
     target = createTarget();
-    tracingModel = new SDK.TracingModel.TracingModel(new FakeStorage());
+    tracingModel = new SDK.TracingModel.TracingModel();
     process = new SDK.TracingModel.Process(tracingModel, 1);
     thread = new SDK.TracingModel.Thread(process, 1);