[ui] Port cpu_freq tracks.

CpuFreqTrackController does some async stuff in it's onSetup function.
Previously this function was being called fire-n-forget, which meant
that onBoundsChange could be called before onSetup was complete. Thus,
this CL adds a more strict lifecycle to tracks where onCreate() is
guaranteed to complete before any of the other methods are called.

Change-Id: I5998fea769741c3218b28817f60cbfb279c3381c
diff --git a/ui/src/common/basic_async_track.ts b/ui/src/common/basic_async_track.ts
index 24e3f28..4e65e49 100644
--- a/ui/src/common/basic_async_track.ts
+++ b/ui/src/common/basic_async_track.ts
@@ -46,9 +46,9 @@
   private currentState?: TrackData;
   protected data?: Data;
 
-  onCreate(): void {}
+  async onCreate(): Promise<void> {}
 
-  onDestroy(): void {
+  async onDestroy(): Promise<void> {
     this.queuedRequest = false;
     this.currentState = undefined;
     this.data = undefined;
diff --git a/ui/src/common/track_adapter.ts b/ui/src/common/track_adapter.ts
index 0538e23..87d9a56 100644
--- a/ui/src/common/track_adapter.ts
+++ b/ui/src/common/track_adapter.ts
@@ -57,15 +57,15 @@
     this.controller = new Controller(config, engine);
   }
 
-  onCreate(): void {
-    this.controller.onSetup();
-    super.onCreate();
+  async onCreate(): Promise<void> {
+    await this.controller.onSetup();
+    await super.onCreate();
   }
 
-  onDestroy(): void {
-    this.track.onDestroy();
-    this.controller.onDestroy();
-    super.onDestroy();
+  async onDestroy(): Promise<void> {
+    await this.track.onDestroy();
+    await this.controller.onDestroy();
+    await super.onDestroy();
   }
 
   getSliceRect(
@@ -205,8 +205,8 @@
   abstract onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data>;
 
-  onSetup(): void {}
-  onDestroy(): void {}
+  async onSetup(): Promise<void> {}
+  async onDestroy(): Promise<void> {}
 
   // Returns a valid SQL table name with the given prefix that should be unique
   // for each track.
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index c29e7c5..7d4732e 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -50,7 +50,6 @@
 } from '../tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
 import {COUNTER_TRACK_KIND} from '../tracks/counter';
-import {CPU_FREQ_TRACK_KIND} from '../tracks/cpu_freq';
 import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile';
 import {
   EXPECTED_FRAMES_SLICE_TRACK_KIND,
@@ -202,54 +201,35 @@
   async addCpuFreqTracks(engine: EngineProxy): Promise<void> {
     const cpus = await this.engine.getCpus();
 
-    const maxCpuFreqResult = await engine.query(`
-    select ifnull(max(value), 0) as freq
-    from counter c
-    inner join cpu_counter_track t on c.track_id = t.id
-    where name = 'cpufreq';
-  `);
-    const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
-
     for (const cpu of cpus) {
       // Only add a cpu freq track if we have
       // cpu freq data.
       // TODO(hjd): Find a way to display cpu idle
       // events even if there are no cpu freq events.
       const cpuFreqIdleResult = await engine.query(`
-      select
-        id as cpuFreqId,
-        (
-          select id
-          from cpu_counter_track
-          where name = 'cpuidle'
-          and cpu = ${cpu}
-          limit 1
-        ) as cpuIdleId
-      from cpu_counter_track
-      where name = 'cpufreq' and cpu = ${cpu}
-      limit 1;
-    `);
+        select
+          id as cpuFreqId,
+          (
+            select id
+            from cpu_counter_track
+            where name = 'cpuidle'
+            and cpu = ${cpu}
+            limit 1
+          ) as cpuIdleId
+        from cpu_counter_track
+        where name = 'cpufreq' and cpu = ${cpu}
+        limit 1;
+      `);
 
       if (cpuFreqIdleResult.numRows() > 0) {
-        const row = cpuFreqIdleResult.firstRow({
-          cpuFreqId: NUM,
-          cpuIdleId: NUM_NULL,
-        });
-        const freqTrackId = row.cpuFreqId;
-        const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
-
         this.tracksToAdd.push({
           engineId: this.engineId,
-          kind: CPU_FREQ_TRACK_KIND,
+          kind: PLUGIN_TRACK_KIND,
           trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
           name: `Cpu ${cpu} Frequency`,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {
-            cpu,
-            maximumValue: maxCpuFreq,
-            freqTrackId,
-            idleTrackId,
-          },
+          config: {},
+          uri: `perfetto.CpuFreq#${cpu}`,
         });
       }
     }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index f16057b..130987b 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -505,10 +505,10 @@
     }  // if (hoveredSlice)
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy() {
+    await super.onDestroy();
     this.isDestroyed = true;
-    this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
+    await this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
   }
 
   // This method figures out if the visible window is outside the bounds of
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 31e9c79..46d16c6 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -75,11 +75,11 @@
     this.lastTrackState = assertExists(globals.state.tracks[this.trackId]);
   }
 
-  onCreate() {}
+  async onCreate(): Promise<void> {}
 
   // Last call the track will receive. Called just before the last reference to
   // this object is removed.
-  onDestroy() {}
+  async onDestroy(): Promise<void> {}
 
   protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 4459c98..a799112 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -30,7 +30,11 @@
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
-import {renderChips, TrackContent} from './track_panel';
+import {
+  renderChips,
+  TrackContent,
+  TrackLifecycleContainer,
+} from './track_panel';
 import {trackRegistry} from './track_registry';
 import {
   drawVerticalLineAtTime,
@@ -45,7 +49,7 @@
   private readonly trackGroupId: string;
   private shellWidth = 0;
   private backgroundColor = '#ffffff';  // Updated from CSS later.
-  private summaryTrack: TrackLike|undefined;
+  private summaryTrack?: TrackLifecycleContainer;
 
   constructor(vnode: m.CVnode<Attrs>) {
     super();
@@ -70,10 +74,12 @@
       },
     };
 
-    this.summaryTrack =
+    const track =
         uri ? pluginManager.createTrack(uri, ctx) : loadTrack(trackState, id);
 
-    this.summaryTrack?.onCreate();
+    if (track) {
+      this.summaryTrack = new TrackLifecycleContainer(track);
+    }
   }
 
   get trackGroupState(): TrackGroupState {
@@ -200,7 +206,7 @@
 
   onremove() {
     if (this.summaryTrack !== undefined) {
-      this.summaryTrack.onDestroy();
+      this.summaryTrack.dispose();
       this.summaryTrack = undefined;
     }
   }
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 2c2d5e7..64cfd63 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -15,6 +15,7 @@
 import {hex} from 'color-convert';
 import m from 'mithril';
 
+import {Disposable} from '../base/disposable';
 import {currentTargetOffset} from '../base/dom_utils';
 import {Icons} from '../base/semantic_icons';
 import {duration, Span, time} from '../base/time';
@@ -101,7 +102,7 @@
 }
 
 interface TrackShellAttrs {
-  track: TrackLike;
+  track: TrackLifecycleContainer;
   trackState: TrackState;
 }
 
@@ -233,7 +234,7 @@
 }
 
 export interface TrackContentAttrs {
-  track: TrackLike;
+  track: TrackLifecycleContainer;
 }
 export class TrackContent implements m.ClassComponent<TrackContentAttrs> {
   private mouseDownX?: number;
@@ -292,7 +293,7 @@
 
 interface TrackComponentAttrs {
   trackState: TrackState;
-  track: TrackLike;
+  track: TrackLifecycleContainer;
 }
 class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
   view({attrs}: m.CVnode<TrackComponentAttrs>) {
@@ -352,12 +353,121 @@
   selectable: boolean;
 }
 
+enum TrackLifecycleState {
+  Initializing,
+  Initialized,
+  DestroyPending,
+  Destroying,
+  Destroyed,
+}
+
+export class TrackLifecycleContainer implements Disposable {
+  private state = TrackLifecycleState.Initializing;
+
+  constructor(private track: TrackLike) {
+    track.onCreate().then(() => {
+      if (this.state === TrackLifecycleState.DestroyPending) {
+        track.onDestroy();
+        this.state = TrackLifecycleState.Destroying;
+      } else {
+        this.state = TrackLifecycleState.Initialized;
+      }
+    });
+  }
+
+  onFullRedraw(): void {
+    if (this.state === TrackLifecycleState.Initialized) {
+      this.track.onFullRedraw();
+    }
+  }
+
+  getSliceRect(
+      visibleTimeScale: TimeScale, visibleWindow: Span<time, bigint>,
+      windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect
+      |undefined {
+    if (this.state === TrackLifecycleState.Initialized) {
+      return this.track.getSliceRect(
+          visibleTimeScale, visibleWindow, windowSpan, tStart, tEnd, depth);
+    } else {
+      return undefined;
+    }
+  }
+
+  getHeight(): number {
+    if (this.state === TrackLifecycleState.Initialized) {
+      return this.track.getHeight();
+    } else {
+      return 18;
+    }
+  }
+
+  getTrackShellButtons(): m.Vnode<TrackButtonAttrs, {}>[] {
+    if (this.state === TrackLifecycleState.Initialized) {
+      return this.track.getTrackShellButtons();
+    } else {
+      return [];
+    }
+  }
+
+  getContextMenu(): m.Vnode<any, {}>|null {
+    if (this.state === TrackLifecycleState.Initialized) {
+      return this.track.getContextMenu();
+    } else {
+      return null;
+    }
+  }
+
+  onMouseMove(position: {x: number; y: number;}): void {
+    if (this.state === TrackLifecycleState.Initialized) {
+      this.track.onMouseMove(position);
+    }
+  }
+
+  onMouseClick(position: {x: number; y: number;}): boolean {
+    if (this.state === TrackLifecycleState.Initialized) {
+      return this.track.onMouseClick(position);
+    } else {
+      return false;
+    }
+  }
+
+  onMouseOut(): void {
+    if (this.state === TrackLifecycleState.Initialized) {
+      this.track.onMouseOut();
+    }
+  }
+
+  render(ctx: CanvasRenderingContext2D) {
+    if (this.state === TrackLifecycleState.Initialized) {
+      this.track.render(ctx);
+    }
+  }
+
+  dispose() {
+    switch (this.state) {
+      case TrackLifecycleState.Initializing:
+        this.state = TrackLifecycleState.DestroyPending;
+        break;
+      case TrackLifecycleState.Initialized:
+        this.state = TrackLifecycleState.Destroying;
+        this.track.onDestroy().then(() => {
+          this.state = TrackLifecycleState.Destroyed;
+        });
+        break;
+      case TrackLifecycleState.DestroyPending:
+      case TrackLifecycleState.Destroying:
+      case TrackLifecycleState.Destroyed:
+        break;
+      default:
+        const x: never = this.state;
+        throw new Error(`Invalid state "${x}"`);
+    }
+  }
+}
+
 export class TrackPanel extends Panel<TrackPanelAttrs> {
-  // TODO(hjd): It would be nicer if these could not be undefined here.
-  // We should implement a NullTrack which can be used if the trackState
-  // has disappeared.
-  private track: TrackLike|undefined;
-  private trackState: TrackState|undefined;
+  private track?: TrackLifecycleContainer;
+  private trackState?: TrackState;
 
   private tryLoadTrack(vnode: m.CVnode<TrackPanelAttrs>) {
     const trackId = vnode.attrs.id;
@@ -379,10 +489,13 @@
       },
     };
 
-    this.track = uri ? pluginManager.createTrack(uri, trackCtx) :
-                       loadTrack(trackState, id);
-    this.track?.onCreate();
-    this.trackState = trackState;
+    const track = uri ? pluginManager.createTrack(uri, trackCtx) :
+                        loadTrack(trackState, id);
+
+    if (track) {
+      this.track = new TrackLifecycleContainer(track);
+      this.trackState = trackState;
+    }
   }
 
   view(vnode: m.CVnode<TrackPanelAttrs>) {
@@ -410,7 +523,7 @@
 
   onremove() {
     if (this.track !== undefined) {
-      this.track.onDestroy();
+      this.track.dispose();
       this.track = undefined;
     }
   }
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 8c90493..1ae68b0 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -190,7 +190,7 @@
 // TODO(stevegolton): Rename `Track` to `BaseTrack` (or similar) and rename this
 // interface to `Track`.
 export interface TrackLike {
-  onCreate(): void;
+  onCreate(): Promise<void>;
   render(ctx: CanvasRenderingContext2D): void;
   onFullRedraw(): void;
   getSliceRect(
@@ -203,7 +203,7 @@
   onMouseMove(position: {x: number, y: number}): void;
   onMouseClick(position: {x: number, y: number}): boolean;
   onMouseOut(): void;
-  onDestroy(): void;
+  onDestroy(): Promise<void>;
 }
 
 export interface PluginTrackInfo {
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index 8632bf4..d4b24aa 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -65,8 +65,8 @@
     });
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy() {
+    await super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(EventLatencyTrack.kind);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index b4d47ff..87a6a3e 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -89,8 +89,8 @@
     };
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy() {
+    await super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(ScrollJankV3Track.kind);
   }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index 56a672b..e2d050f 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -69,8 +69,8 @@
     });
   }
 
-  onDestroy() {
-    super.onDestroy();
+  async onDestroy() {
+    await super.onDestroy();
     ScrollJankPluginState.getInstance().unregisterTrack(
         TopLevelScrollTrack.kind);
   }
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 478266d..00a89ca 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -126,7 +126,6 @@
 }
 
 export class CounterTrack extends BasicAsyncTrack<Data> {
-  private setup = false;
   private maximumValueSeen = 0;
   private minimumValueSeen = 0;
   private maximumDeltaSeen = 0;
@@ -166,61 +165,59 @@
     }
   }
 
-  async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
-    if (!this.setup) {
-      if (this.config.namespace === undefined) {
-        await this.engine.query(`
-          create view ${this.tableName('counter_view')} as
-          select
-            id,
-            ts,
-            dur,
-            value,
-            delta
-          from experimental_counter_dur
-          where track_id = ${this.config.trackId};
-        `);
-      } else {
-        await this.engine.query(`
-          create view ${this.tableName('counter_view')} as
-          select
-            id,
-            ts,
-            lead(ts, 1, ts) over (order by ts) - ts as dur,
-            lead(value, 1, value) over (order by ts) - value as delta,
-            value
-          from ${this.namespaceTable('counter')}
-          where track_id = ${this.config.trackId};
-        `);
-      }
-
-      const maxDurResult = await this.engine.query(`
-          select
-            max(
-              iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
-            ) as maxDur
-          from ${this.tableName('counter_view')}
-      `);
-      this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
-
-      const queryRes = await this.engine.query(`
+  async onCreate() {
+    if (this.config.namespace === undefined) {
+      await this.engine.query(`
+        create view ${this.tableName('counter_view')} as
         select
-          ifnull(max(value), 0) as maxValue,
-          ifnull(min(value), 0) as minValue,
-          ifnull(max(delta), 0) as maxDelta,
-          ifnull(min(delta), 0) as minDelta
-        from ${this.tableName('counter_view')}`);
-      const row = queryRes.firstRow(
-          {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
-      this.maximumValueSeen = row.maxValue;
-      this.minimumValueSeen = row.minValue;
-      this.maximumDeltaSeen = row.maxDelta;
-      this.minimumDeltaSeen = row.minDelta;
-
-      this.setup = true;
+          id,
+          ts,
+          dur,
+          value,
+          delta
+        from experimental_counter_dur
+        where track_id = ${this.config.trackId};
+      `);
+    } else {
+      await this.engine.query(`
+        create view ${this.tableName('counter_view')} as
+        select
+          id,
+          ts,
+          lead(ts, 1, ts) over (order by ts) - ts as dur,
+          lead(value, 1, value) over (order by ts) - value as delta,
+          value
+        from ${this.namespaceTable('counter')}
+        where track_id = ${this.config.trackId};
+      `);
     }
 
+    const maxDurResult = await this.engine.query(`
+        select
+          max(
+            iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
+          ) as maxDur
+        from ${this.tableName('counter_view')}
+    `);
+    this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
+
+    const queryRes = await this.engine.query(`
+      select
+        ifnull(max(value), 0) as maxValue,
+        ifnull(min(value), 0) as minValue,
+        ifnull(max(delta), 0) as maxDelta,
+        ifnull(min(delta), 0) as minDelta
+      from ${this.tableName('counter_view')}`);
+    const row = queryRes.firstRow(
+        {maxValue: NUM, minValue: NUM, maxDelta: NUM, minDelta: NUM});
+    this.maximumValueSeen = row.maxValue;
+    this.minimumValueSeen = row.minValue;
+    this.maximumDeltaSeen = row.maxDelta;
+    this.minimumDeltaSeen = row.minDelta;
+  }
+
+  async onBoundsChange(start: time, end: time, resolution: duration):
+      Promise<Data> {
     const queryRes = await this.engine.query(`
       select
         (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
@@ -601,8 +598,9 @@
     }
   }
 
-  onDestroy(): void {
-    this.engine.query(`DROP VIEW IF EXISTS ${this.tableName('counter_view')}`);
+  async onDestroy(): Promise<void> {
+    await this.engine.query(
+        `DROP VIEW IF EXISTS ${this.tableName('counter_view')}`);
   }
 }
 
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index baa24f9..5b182be 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -26,12 +26,21 @@
   NUM_NULL,
   QueryResult,
 } from '../../common/query_result';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+  TrackWithControllerAdapter,
+} from '../../common/track_adapter';
 import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginInfo,
+  TracePluginContext,
+} from '../../public';
 
 
 export const CPU_FREQ_TRACK_KIND = 'CpuFreqTrack';
@@ -55,9 +64,7 @@
   minimumValue?: number;
 }
 
-class CpuFreqTrackController extends TrackController<Config, Data> {
-  static readonly kind = CPU_FREQ_TRACK_KIND;
-
+class CpuFreqTrackController extends TrackControllerAdapter<Config, Data> {
   private maxDur: duration = 0n;
   private maxTsEnd: time = Time.ZERO;
   private maximumValueSeen = 0;
@@ -266,8 +273,7 @@
 const MARGIN_TOP = 4.5;
 const RECT_HEIGHT = 20;
 
-class CpuFreqTrack extends Track<Config, Data> {
-  static readonly kind = CPU_FREQ_TRACK_KIND;
+class CpuFreqTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): CpuFreqTrack {
     return new CpuFreqTrack(args);
   }
@@ -484,9 +490,70 @@
 }
 
 class CpuFreq implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(CpuFreqTrackController);
-    ctx.registerTrack(CpuFreqTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+    const {engine} = ctx;
+
+    const cpus = await engine.getCpus();
+
+    const maxCpuFreqResult = await engine.query(`
+      select ifnull(max(value), 0) as freq
+      from counter c
+      inner join cpu_counter_track t on c.track_id = t.id
+      where name = 'cpufreq';
+    `);
+    const maxCpuFreq = maxCpuFreqResult.firstRow({freq: NUM}).freq;
+
+    for (const cpu of cpus) {
+      // Only add a cpu freq track if we have
+      // cpu freq data.
+      // TODO(hjd): Find a way to display cpu idle
+      // events even if there are no cpu freq events.
+      const cpuFreqIdleResult = await engine.query(`
+        select
+          id as cpuFreqId,
+          (
+            select id
+            from cpu_counter_track
+            where name = 'cpuidle'
+            and cpu = ${cpu}
+            limit 1
+          ) as cpuIdleId
+        from cpu_counter_track
+        where name = 'cpufreq' and cpu = ${cpu}
+        limit 1;
+      `);
+
+      if (cpuFreqIdleResult.numRows() > 0) {
+        const row = cpuFreqIdleResult.firstRow({
+          cpuFreqId: NUM,
+          cpuIdleId: NUM_NULL,
+        });
+        const freqTrackId = row.cpuFreqId;
+        const idleTrackId = row.cpuIdleId === null ? undefined : row.cpuIdleId;
+
+        ctx.addTrack({
+          uri: `perfetto.CpuFreq#${cpu}`,
+          displayName: `Cpu ${cpu} Frequency`,
+          kind: CPU_FREQ_TRACK_KIND,
+          cpu,
+          trackFactory: ({trackInstanceId}) => {
+            return new TrackWithControllerAdapter<Config, Data>(
+                engine,
+                trackInstanceId,
+                {
+                  cpu,
+                  maximumValue: maxCpuFreq,
+                  freqTrackId,
+                  idleTrackId,
+                },
+                CpuFreqTrack,
+                CpuFreqTrackController);
+          },
+        });
+      }
+    }
   }
 }
 
diff --git a/ui/src/tracks/process_summary/process_summary_track.ts b/ui/src/tracks/process_summary/process_summary_track.ts
index eb62667..7085cb8 100644
--- a/ui/src/tracks/process_summary/process_summary_track.ts
+++ b/ui/src/tracks/process_summary/process_summary_track.ts
@@ -41,47 +41,44 @@
 // and no cpu scheduling.
 export class ProcessSummaryTrackController extends
     TrackControllerAdapter<Config, Data> {
-  private setup = false;
+  async onSetup(): Promise<void> {
+    await this.query(
+        `create virtual table ${this.tableName('window')} using window;`);
+
+    let utids = [this.config.utid];
+    if (this.config.upid) {
+      const threadQuery = await this.query(
+          `select utid from thread where upid=${this.config.upid}`);
+      utids = [];
+      for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
+        utids.push(it.utid);
+      }
+    }
+
+    const trackQuery = await this.query(
+        `select id from thread_track where utid in (${utids.join(',')})`);
+    const tracks = [];
+    for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
+      tracks.push(it.id);
+    }
+
+    const processSliceView = this.tableName('process_slice_view');
+    await this.query(
+        `create view ${processSliceView} as ` +
+        // 0 as cpu is a dummy column to perform span join on.
+        `select ts, dur/${utids.length} as dur ` +
+        `from slice s ` +
+        `where depth = 0 and track_id in ` +
+        `(${tracks.join(',')})`);
+    await this.query(`create virtual table ${this.tableName('span')}
+        using span_join(${processSliceView},
+                        ${this.tableName('window')});`);
+  }
 
   async onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data> {
     assertFalse(resolution === 0n, 'Resolution cannot be 0');
 
-    if (this.setup === false) {
-      await this.query(
-          `create virtual table ${this.tableName('window')} using window;`);
-
-      let utids = [this.config.utid];
-      if (this.config.upid) {
-        const threadQuery = await this.query(
-            `select utid from thread where upid=${this.config.upid}`);
-        utids = [];
-        for (const it = threadQuery.iter({utid: NUM}); it.valid(); it.next()) {
-          utids.push(it.utid);
-        }
-      }
-
-      const trackQuery = await this.query(
-          `select id from thread_track where utid in (${utids.join(',')})`);
-      const tracks = [];
-      for (const it = trackQuery.iter({id: NUM}); it.valid(); it.next()) {
-        tracks.push(it.id);
-      }
-
-      const processSliceView = this.tableName('process_slice_view');
-      await this.query(
-          `create view ${processSliceView} as ` +
-          // 0 as cpu is a dummy column to perform span join on.
-          `select ts, dur/${utids.length} as dur ` +
-          `from slice s ` +
-          `where depth = 0 and track_id in ` +
-          `(${tracks.join(',')})`);
-      await this.query(`create virtual table ${this.tableName('span')}
-          using span_join(${processSliceView},
-                          ${this.tableName('window')});`);
-      this.setup = true;
-    }
-
     // |resolution| is in ns/px we want # ns for 10px window:
     // Max value with 1 so we don't end up with resolution 0.
     const bucketSize = resolution * 10n;
@@ -132,12 +129,9 @@
     return summary;
   }
 
-  onDestroy(): void {
-    if (this.setup) {
-      this.query(`drop table ${this.tableName('window')}`);
-      this.query(`drop table ${this.tableName('span')}`);
-      this.setup = false;
-    }
+  async onDestroy(): Promise<void> {
+    await this.query(`drop table ${this.tableName('window')}; drop table ${
+        this.tableName('span')}`);
   }
 }