ui: Add TimelineFetcher

First step of removing TrackHelperLEGACY.

Change-Id: I6ed1a7e3d26b68d007c7be2a3c57827536bf613a
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 8990a44..933ce45 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -14,14 +14,13 @@
 
 import m from 'mithril';
 
-import {duration, Time, time} from '../base/time';
+import {Disposable} from '../base/disposable';
+import {duration, Time, time, TimeSpan} from '../base/time';
 import {raf} from '../core/raf_scheduler';
 import {globals} from '../frontend/globals';
 import {PanelSize} from '../frontend/panel';
 import {SliceRect, Track, TrackContext} from '../public';
 
-import {TrackData} from './track_data';
-
 export {Store} from '../frontend/store';
 export {EngineProxy} from '../trace_processor/engine';
 export {
@@ -33,6 +32,97 @@
   STR_NULL,
 } from '../trace_processor/query_result';
 
+type FetchTimeline<Data> = (start: time, end: time, resolution: duration) =>
+    Promise<Data>;
+
+// This helper provides the logic to call |doFetch()| only when more
+// data is needed as the visible window is panned and zoomed about, and
+// includes an FSM to ensure doFetch is not re-entered.
+class TimelineFetcher<Data> implements Disposable {
+  private requestingData = false;
+  private queuedRequest = false;
+  private doFetch: FetchTimeline<Data>;
+
+  private data_?: Data;
+
+  // Timespan and resolution of the latest *request*. data_ may cover
+  // a different time window.
+  private latestTimespan: TimeSpan;
+  private latestResolution: duration;
+
+  constructor(doFetch: FetchTimeline<Data>) {
+    this.doFetch = doFetch;
+    this.latestTimespan = TimeSpan.ZERO;
+    this.latestResolution = 0n;
+  }
+
+  requestDataForCurrentTime(): void {
+    const currentTimeSpan = globals.frontendLocalState.visibleTimeSpan;
+    const currentResolution = globals.getCurResolution();
+    this.requestData(currentTimeSpan, currentResolution);
+  }
+
+  requestData(timespan: TimeSpan, resolution: duration): void {
+    if (this.shouldLoadNewData(timespan, resolution)) {
+      // Over request data, one page worth to the left and right.
+      const start = Time.sub(timespan.start, timespan.duration);
+      const end = Time.add(timespan.end, timespan.duration);
+      this.latestTimespan = new TimeSpan(start, end);
+      this.latestResolution = resolution;
+      this.loadData();
+    }
+  }
+
+  get data(): Data|undefined {
+    return this.data_;
+  }
+
+  dispose() {
+    this.queuedRequest = false;
+    this.data_ = undefined;
+  }
+
+  private shouldLoadNewData(timespan: TimeSpan, resolution: duration): boolean {
+    if (this.data_ === undefined) {
+      return true;
+    }
+
+    if (timespan.start < this.latestTimespan.start) {
+      return true;
+    }
+
+    if (timespan.end > this.latestTimespan.end) {
+      return true;
+    }
+
+    if (resolution !== this.latestResolution) {
+      return true;
+    }
+
+    return false;
+  }
+
+  private loadData(): void {
+    if (this.requestingData) {
+      this.queuedRequest = true;
+      return;
+    }
+    const {start, end} = this.latestTimespan;
+    const resolution = this.latestResolution;
+    this.doFetch(start, end, resolution).then((data) => {
+      this.requestingData = false;
+      this.data_ = data;
+      if (this.queuedRequest) {
+        this.queuedRequest = false;
+        this.loadData();
+      } else {
+        raf.scheduleRedraw();
+      }
+    });
+    this.requestingData = true;
+  }
+}
+
 // A helper class which provides a base track implementation for tracks which
 // load their content asynchronously from the trace.
 //
@@ -49,17 +139,21 @@
 // Note: This class is deprecated and should not be used for new tracks. Use
 // |BaseSliceTrack| instead.
 export abstract class TrackHelperLEGACY<Data> implements Track {
-  private requestingData = false;
-  private queuedRequest = false;
-  private currentState?: TrackData;
-  protected data?: Data;
+  private timelineFetcher: TimelineFetcher<Data>;
+
+  constructor() {
+    this.timelineFetcher =
+        new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+  }
 
   onCreate(_ctx: TrackContext): void {}
 
   onDestroy(): void {
-    this.queuedRequest = false;
-    this.currentState = undefined;
-    this.data = undefined;
+    this.timelineFetcher.dispose();
+  }
+
+  get data(): Data|undefined {
+    return this.timelineFetcher.data;
   }
 
   // Returns a place where a given slice should be drawn. Should be implemented
@@ -93,65 +187,7 @@
   abstract renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void;
 
   render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
-    if (this.shouldLoadNewData()) {
-      this.loadData();
-    }
-
+    this.timelineFetcher.requestDataForCurrentTime();
     this.renderCanvas(ctx, size);
   }
-
-  private loadData(): void {
-    if (this.requestingData) {
-      this.queuedRequest = true;
-      return;
-    }
-
-    const ts = globals.frontendLocalState.visibleTimeSpan;
-    const resolution = globals.getCurResolution();
-
-    const start = Time.sub(ts.start, ts.duration);
-    const end = Time.add(ts.end, ts.duration);
-
-    this.currentState = {
-      start,
-      end,
-      resolution,
-      length: 0,
-    };
-
-    this.onBoundsChange(start, end, resolution).then((data) => {
-      this.requestingData = false;
-      this.data = data;
-
-      if (this.queuedRequest) {
-        this.queuedRequest = false;
-        this.loadData();
-      } else {
-        raf.scheduleRedraw();
-      }
-    });
-
-    this.requestingData = true;
-  }
-
-  private shouldLoadNewData(): boolean {
-    if (!this.currentState) {
-      return true;
-    }
-
-    const ts = globals.frontendLocalState.visibleTimeSpan;
-    if (ts.start < this.currentState.start) {
-      return true;
-    }
-
-    if (ts.end > this.currentState.end) {
-      return true;
-    }
-
-    if (globals.getCurResolution() !== this.currentState.resolution) {
-      return true;
-    }
-
-    return false;
-  }
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 46fbb46..f73da71 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -21,7 +21,6 @@
 import {addDebugSliceTrack} from '../../tracks/debug/slice_track';
 
 class AndroidPerfTraceCounters implements Plugin {
-
   onActivate(_: PluginContext): void {}
 
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -76,7 +75,7 @@
       ssi.l3_cache_miss
     FROM sched_switch_ipc ssi
   )
-`
+`;
 
         await addDebugSliceTrack(
           ctx.engine,
@@ -86,7 +85,7 @@
           },
           'Rutime IPC:' + tid,
           {ts: 'ts', dur: 'dur', name: 'ipc'},
-          ['instruction', 'cycle', 'stall_backend_mem', 'l3_cache_miss' ],
+          ['instruction', 'cycle', 'stall_backend_mem', 'l3_cache_miss'],
         );
         ctx.tabs.openQuery(sql_prefix + `
 SELECT