[ui] Port counter tracks to plugin tracks.

Summary:
- Each track instance has it's own state - accessed via mountState()
- Created 'CounterTrack' track impl for all counter tracks - port of
  the original controller based track.
- Added suggestTrack() to TracePluginContext which replaces
  findPotentialTracks().
- Added new "annotation" plugin for annotation counter tracks.
- Moved kind, cpu, trackIds, to PluginTrackInfo. kind is required,
  but the others are optional. Use this new interface everywhere they
  were previously extracted from config (e.g. aggregations, flows).
- Added new command "Find track by URI".

Change-Id: I6e75ee3dcf16ceb13b0cefd0a5743daa22d5e2c0
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index acb6a47..0999b31 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -16,6 +16,7 @@
 
 import {assertExists, assertTrue, assertUnreachable} from '../base/logging';
 import {duration, time} from '../base/time';
+import {exists} from '../base/utils';
 import {RecordConfig} from '../controller/record_config_types';
 import {
   GenericSliceDetailsTabConfig,
@@ -29,7 +30,7 @@
   tableColumnEquals,
   toggleEnabled,
 } from '../frontend/pivot_table_types';
-import {TrackTags} from '../public/index';
+import {PrimaryTrackSortKey, TrackTags} from '../public/index';
 import {DebugTrackV2Config} from '../tracks/debug/slice_track';
 
 import {randomColor} from './colorizer';
@@ -47,6 +48,7 @@
   traceEventEnd,
   TraceEventScope,
 } from './metatracing';
+import {pluginManager} from './plugins';
 import {
   AdbRecordingTarget,
   Area,
@@ -61,7 +63,6 @@
   Pagination,
   PendingDeeplinkState,
   PivotTableResult,
-  PrimaryTrackSortKey,
   ProfileType,
   RecordingTarget,
   SCROLLING_TRACK_GROUP,
@@ -225,16 +226,25 @@
       state.uiTrackIdByTraceTrackId[trackId] = uiTrackId;
     };
 
-    const config = trackState.config as {trackId: number};
-    if (config.trackId !== undefined) {
-      setUiTrackId(config.trackId, uiTrackId);
-      return;
-    }
-
-    const multiple = trackState.config as {trackIds: number[]};
-    if (multiple.trackIds !== undefined) {
-      for (const trackId of multiple.trackIds) {
+    const {uri, config} = trackState;
+    if (exists(uri)) {
+      // If track is a new "plugin" type track (i.e. it has a uri), resolve the
+      // track ids from through the pluginManager.
+      const trackInfo = pluginManager.resolveTrackInfo(uri);
+      if (trackInfo?.trackIds) {
+        for (const trackId of trackInfo.trackIds) {
+          setUiTrackId(trackId, uiTrackId);
+        }
+      }
+    } else {
+      // Traditional track - resolve track ids through the config.
+      const {trackId, trackIds} = config;
+      if (exists(trackId)) {
         setUiTrackId(trackId, uiTrackId);
+      } else if (exists(trackIds)) {
+        for (const trackId of trackIds) {
+          setUiTrackId(trackId, uiTrackId);
+        }
       }
     }
   },
@@ -413,11 +423,6 @@
     state.visibleTracks = args.tracks;
   },
 
-  updateTrackConfig(state: StateDraft, args: {id: string, config: {}}) {
-    if (state.tracks[args.id] === undefined) return;
-    state.tracks[args.id].config = args.config;
-  },
-
   moveTrack(
       state: StateDraft,
       args: {srcId: string; op: 'before' | 'after', dstId: string}): void {
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 261dd2e..de857bc 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -16,6 +16,7 @@
 
 import {assertExists} from '../base/logging';
 import {Time} from '../base/time';
+import {PrimaryTrackSortKey} from '../public';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
 import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile';
 import {
@@ -27,7 +28,6 @@
 import {createEmptyState} from './empty_state';
 import {
   InThreadTrackSortKey,
-  PrimaryTrackSortKey,
   ProfileType,
   SCROLLING_TRACK_GROUP,
   State,
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index 43c5f16..c442e73 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -499,6 +499,13 @@
     return this.engine.getCpus();
   }
 
+  async getNumberOfGpus(): Promise<number> {
+    if (!this.isAlive) {
+      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
+    }
+    return this.engine.getNumberOfGpus();
+  }
+
   get engineId(): string {
     return this.engine.id;
   }
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 9961921..7278e06 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -102,7 +102,8 @@
   constructor(
       private ctx: PluginContext, readonly store: Store<T>,
       readonly engine: EngineProxy,
-      private trackRegistry: Map<string, PluginTrackInfo>,
+      readonly trackRegistry: Map<string, PluginTrackInfo>,
+      private suggestedTracks: Set<TrackInfo>,
       private commandRegistry: Map<string, Command>) {
     this.trash.add(engine);
     this.trash.add(store);
@@ -147,11 +148,17 @@
     if (!this.alive) return;
     const {uri} = trackDetails;
     this.trackRegistry.set(uri, trackDetails);
-    this.trash.add({
-      dispose: () => {
-        this.trackRegistry.delete(uri);
-      },
-    });
+    this.trash.addCallback(() => this.trackRegistry.delete(uri));
+  }
+
+  // Ask Perfetto to add a track to the track list when a fresh trace is loaded.
+  // Ignored when a trace is loaded from a permalink.
+  // This is a direct replacement for findPotentialTracks().
+  // Note: This interface is likely to be deprecated soon, but is required while
+  // both plugin and original type tracks coexist.
+  suggestTrack(trackInfo: TrackInfo): void {
+    this.suggestedTracks.add(trackInfo);
+    this.trash.addCallback(() => this.suggestedTracks.delete(trackInfo));
   }
 
   dispose(): void {
@@ -170,7 +177,7 @@
 interface PluginDetails<T> {
   plugin: Plugin<T>;
   context: PluginContext&Disposable;
-  traceContext?: TracePluginContext<T>&Disposable;
+  traceContext?: TracePluginContextImpl<unknown>;
 }
 
 function isPluginClass<T>(v: unknown): v is PluginClass<T> {
@@ -200,6 +207,7 @@
   private engine?: Engine;
   readonly trackRegistry = new Map<string, PluginTrackInfo>();
   readonly commandRegistry = new Map<string, Command>();
+  readonly suggestedTracks = new Set<TrackInfo>();
 
   constructor(registry: PluginRegistry) {
     this.registry = registry;
@@ -257,15 +265,8 @@
     return this.plugins.get(pluginId);
   }
 
-  findPotentialTracks(): Promise<TrackInfo[]>[] {
-    const promises: Promise<TrackInfo[]>[] = [];
-    for (const {plugin, traceContext} of this.plugins.values()) {
-      if (plugin.findPotentialTracks && traceContext) {
-        const promise = plugin.findPotentialTracks(traceContext);
-        promises.push(promise);
-      }
-    }
-    return promises;
+  findPotentialTracks(): TrackInfo[] {
+    return Array.from(this.suggestedTracks);
   }
 
   onTraceLoad(engine: Engine): void {
@@ -305,10 +306,9 @@
 
   // Create a new plugin track object from its URI.
   // Returns undefined if no such track is registered.
-  createTrack(uri: string, trackInstanceId: string): TrackLike|undefined {
+  createTrack(uri: string, trackCtx: TrackContext): TrackLike|undefined {
     const trackInfo = pluginManager.trackRegistry.get(uri);
-    const trackContext: TrackContext = {trackInstanceId};
-    return trackInfo && trackInfo.trackFactory(trackContext);
+    return trackInfo && trackInfo.trackFactory(trackCtx);
   }
 
   private doPluginTraceLoad<T>(
@@ -331,6 +331,7 @@
           proxyStore,
           engineProxy,
           this.trackRegistry,
+          this.suggestedTracks,
           this.commandRegistry);
       pluginDetails.traceContext = traceCtx;
 
@@ -347,6 +348,7 @@
           proxyStore,
           engineProxy,
           this.trackRegistry,
+          this.suggestedTracks,
           this.commandRegistry);
       pluginDetails.traceContext = traceCtx;
 
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 0d31e01..6ba8547 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -23,7 +23,7 @@
   PivotTree,
   TableColumn,
 } from '../frontend/pivot_table_types';
-import {TrackTags} from '../public/index';
+import {PrimaryTrackSortKey, TrackTags} from '../public/index';
 
 import {Direction} from './event_set';
 
@@ -130,36 +130,6 @@
 
 export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE'|'FORCE_BUILTIN_WASM';
 
-// Tracks within track groups (usually corresponding to processes) are sorted.
-// As we want to group all tracks related to a given thread together, we use
-// two keys:
-// - Primary key corresponds to a priority of a track block (all tracks related
-//   to a given thread or a single track if it's not thread-associated).
-// - Secondary key corresponds to a priority of a given thread-associated track
-//   within its thread track block.
-// Each track will have a sort key, which either a primary sort key
-// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
-// primary sort key is done independently).
-export enum PrimaryTrackSortKey {
-  DEBUG_SLICE_TRACK,
-  NULL_TRACK,
-  PROCESS_SCHEDULING_TRACK,
-  PROCESS_SUMMARY_TRACK,
-  EXPECTED_FRAMES_SLICE_TRACK,
-  ACTUAL_FRAMES_SLICE_TRACK,
-  PERF_SAMPLES_PROFILE_TRACK,
-  HEAP_PROFILE_TRACK,
-  MAIN_THREAD,
-  RENDER_THREAD,
-  GPU_COMPLETION_THREAD,
-  CHROME_IO_THREAD,
-  CHROME_COMPOSITOR_THREAD,
-  ORDINARY_THREAD,
-  COUNTER_TRACK,
-  ASYNC_SLICE_TRACK,
-  ORDINARY_TRACK,
-}
-
 // Key that is used to sort tracks within a block of tracks associated with a
 // given thread.
 export enum InThreadTrackSortKey {
@@ -260,6 +230,7 @@
     trackIds?: number[];
   };
   uri?: string;
+  state?: unknown;
 }
 
 export interface TrackGroupState {
@@ -268,6 +239,7 @@
   name: string;
   collapsed: boolean;
   tracks: string[];  // Child track ids.
+  state?: unknown;
 }
 
 export interface EngineConfig {
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 3a34afa..34646ba 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -12,8 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {PrimaryTrackSortKey} from '../public';
+
 import {createEmptyState} from './empty_state';
-import {getContainingTrackId, PrimaryTrackSortKey, State} from './state';
+import {getContainingTrackId, State} from './state';
 import {deserializeStateObject, serializeStateObject} from './upload_utils';
 
 test('createEmptyState', () => {
diff --git a/ui/src/common/track_adapter.ts b/ui/src/common/track_adapter.ts
index 9bb57a4..54b5055 100644
--- a/ui/src/common/track_adapter.ts
+++ b/ui/src/common/track_adapter.ts
@@ -181,6 +181,12 @@
   new (args: NewTrackArgs): TrackAdapter<Config, Data>
 }
 
+function hasNamespace(config: unknown): config is {
+  namespace: string
+} {
+  return !!config && typeof config === 'object' && 'namespace' in config;
+}
+
 // Extend from this class instead of `TrackController` to use existing track
 // controller implementations with `TrackWithControllerAdapter`.
 export abstract class TrackControllerAdapter<Config, Data> {
@@ -210,6 +216,14 @@
     const idSuffix = this.uuid.split('-').join('_');
     return `${prefix}_${idSuffix}`;
   }
+
+  namespaceTable(tableName: string): string {
+    if (hasNamespace(this.config)) {
+      return this.config.namespace + '_' + tableName;
+    } else {
+      return tableName;
+    }
+  }
 }
 
 type TrackControllerAdapterClass<Config, Data> = {
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
index 3c3cf15..ae3e1d7 100644
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts
@@ -15,9 +15,10 @@
 import {Duration} from '../../base/time';
 import {ColumnDef} from '../../common/aggregation_data';
 import {Engine} from '../../common/engine';
+import {pluginManager} from '../../common/plugins';
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
-import {Config, COUNTER_TRACK_KIND} from '../../tracks/counter';
+import {COUNTER_TRACK_KIND} from '../../tracks/counter';
 
 import {AggregationController} from './aggregation_controller';
 
@@ -25,19 +26,17 @@
   async createAggregateView(engine: Engine, area: Area) {
     await engine.query(`drop view if exists ${this.kind};`);
 
-    const ids = [];
+    const trackIds: (string|number)[] = [];
     for (const trackId of area.tracks) {
       const track = globals.state.tracks[trackId];
-      // Track will be undefined for track groups.
-      if (track !== undefined && track.kind === COUNTER_TRACK_KIND) {
-        const config = track.config as Config;
-        // TODO(hjd): Also aggregate annotation (with namespace) counters.
-        if (config.namespace === undefined) {
-          ids.push(config.trackId);
+      if (track?.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        if (trackInfo?.kind === COUNTER_TRACK_KIND) {
+          trackInfo.trackIds && trackIds.push(...trackInfo.trackIds);
         }
       }
     }
-    if (ids.length === 0) return false;
+    if (trackIds.length === 0) return false;
     const duration = area.end - area.start;
     const durationSec = Duration.toSeconds(duration);
 
@@ -61,7 +60,7 @@
         (partition by track_id order by ts
             range between unbounded preceding and unbounded following) as last
         from experimental_counter_dur
-        where track_id in (${ids})
+        where track_id in (${trackIds})
         and ts + dur >= ${area.start} and
         ts <= ${area.end})
     join counter_track
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 46f0f47..db176d9 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -31,9 +31,8 @@
       const track = globals.state.tracks[trackId];
       if (track?.uri) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          const cpu = trackInfo?.tags?.cpu;
-          cpu && selectedCpus.push(cpu);
+        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+          trackInfo.cpu && selectedCpus.push(trackInfo.cpu);
         }
       }
     }
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
index aebaf94..dbdf07c 100644
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
@@ -30,9 +30,8 @@
       const track = globals.state.tracks[trackId];
       if (track?.uri) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          const cpu = trackInfo?.tags?.cpu;
-          cpu && selectedCpus.push(cpu);
+        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+          trackInfo.cpu && selectedCpus.push(trackInfo.cpu);
         }
       }
     }
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index 06f9da9..a1c4170 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -15,6 +15,7 @@
 import {Time} from '../base/time';
 import {Engine} from '../common/engine';
 import {featureFlags} from '../common/feature_flags';
+import {pluginManager} from '../common/plugins';
 import {LONG, NUM, STR_NULL} from '../common/query_result';
 import {Area} from '../common/state';
 import {Flow, globals} from '../frontend/globals';
@@ -243,6 +244,17 @@
         return null;
       }
 
+      // Perform the same check for "plugin" style tracks.
+      if (track.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        const trackIds = trackInfo?.trackIds;
+        if (trackIds === undefined || trackIds.length <= 1) {
+          uiTrackIdToInfo.set(uiTrackId, null);
+          trackIdToInfo.set(trackId, null);
+          return null;
+        }
+      }
+
       const newInfo = {
         uiTrackId,
         siblingTrackIds: trackIds,
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index b8ee0d3..6055221 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -204,8 +204,8 @@
     for (const track of Object.values(globals.state.tracks)) {
       if (exists(track?.uri)) {
         const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          const cpu = trackInfo?.tags?.cpu;
+        if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+          const cpu = trackInfo?.cpu;
           cpu && cpuToTrackId.set(cpu, track.id);
         }
       }
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 027398a..31000d6 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -470,8 +470,6 @@
       }
     }
 
-    pluginManager.onTraceLoad(engine);
-
     const emptyOmniboxState = {
       omnibox: '',
       mode: globals.state.omniboxState.mode || 'SEARCH',
@@ -501,6 +499,8 @@
     // Make sure the helper views are available before we start adding tracks.
     await this.initialiseHelperViews();
 
+    pluginManager.onTraceLoad(engine);
+
     {
       // When we reload from a permalink don't create extra tracks:
       const {pinnedTracks, tracks} = globals.state;
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index e6ae81d..a806363 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -25,7 +25,6 @@
 import {featureFlags, PERF_SAMPLE_FLAG} from '../common/feature_flags';
 import {pluginManager} from '../common/plugins';
 import {
-  LONG_NULL,
   NUM,
   NUM_NULL,
   STR,
@@ -33,11 +32,12 @@
 } from '../common/query_result';
 import {
   InThreadTrackSortKey,
-  PrimaryTrackSortKey,
   SCROLLING_TRACK_GROUP,
   TrackSortKey,
   UtidToTrackSortKey,
 } from '../common/state';
+import {PrimaryTrackSortKey} from '../public';
+import {getTrackName} from '../public/utils';
 import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames';
 import {ASYNC_SLICE_TRACK_KIND} from '../tracks/async_slices';
 import {
@@ -49,7 +49,7 @@
   decideTracks as scrollJankDecideTracks,
 } from '../tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
-import {COUNTER_TRACK_KIND, CounterScaleOptions} from '../tracks/counter';
+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 {
@@ -130,29 +130,6 @@
 const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
 const MISC_GROUP = 'Misc Global Tracks';
 
-// Sets the default 'scale' for counter tracks. If the regex matches
-// then the paired mode is used. Entries are in priority order so the
-// first match wins.
-const COUNTER_REGEX: [RegExp, CounterScaleOptions][] = [
-  // Power counters make more sense in rate mode since you're typically
-  // interested in the slope of the graph rather than the absolute
-  // value.
-  [new RegExp('^power\..*$'), 'RATE'],
-  // Same for network counters.
-  [NETWORK_TRACK_REGEX, 'RATE'],
-  // Entity residency
-  [ENTITY_RESIDENCY_REGEX, 'RATE'],
-];
-
-function getCounterScale(name: string): CounterScaleOptions|undefined {
-  for (const [re, scale] of COUNTER_REGEX) {
-    if (name.match(re)) {
-      return scale;
-    }
-  }
-  return undefined;
-}
-
 export async function decideTracks(
     engineId: string, engine: Engine): Promise<DeferredAction[]> {
   return (new TrackDecider(engineId, engine)).decideTracks();
@@ -171,66 +148,6 @@
     this.engine = engine;
   }
 
-  static getTrackName(args: Partial<{
-    name: string | null,
-    utid: number,
-    processName: string|null,
-    pid: number|null,
-    threadName: string|null,
-    tid: number|null,
-    upid: number|null,
-    kind: string,
-    threadTrack: boolean
-  }>) {
-    const {
-      name,
-      upid,
-      utid,
-      processName,
-      threadName,
-      pid,
-      tid,
-      kind,
-      threadTrack,
-    } = args;
-
-    const hasName = name !== undefined && name !== null && name !== '[NULL]';
-    const hasUpid = upid !== undefined && upid !== null;
-    const hasUtid = utid !== undefined && utid !== null;
-    const hasProcessName = processName !== undefined && processName !== null;
-    const hasThreadName = threadName !== undefined && threadName !== null;
-    const hasTid = tid !== undefined && tid !== null;
-    const hasPid = pid !== undefined && pid !== null;
-    const hasKind = kind !== undefined;
-    const isThreadTrack = threadTrack !== undefined && threadTrack;
-
-    // If we don't have any useful information (better than
-    // upid/utid) we show the track kind to help with tracking
-    // down where this is coming from.
-    const kindSuffix = hasKind ? ` (${kind})` : '';
-
-    if (isThreadTrack && hasName && hasTid) {
-      return `${name} (${tid})`;
-    } else if (hasName) {
-      return `${name}`;
-    } else if (hasUpid && hasPid && hasProcessName) {
-      return `${processName} ${pid}`;
-    } else if (hasUpid && hasPid) {
-      return `Process ${pid}`;
-    } else if (hasThreadName && hasTid) {
-      return `${threadName} ${tid}`;
-    } else if (hasTid) {
-      return `Thread ${tid}`;
-    } else if (hasUpid) {
-      return `upid: ${upid}${kindSuffix}`;
-    } else if (hasUtid) {
-      return `utid: ${utid}${kindSuffix}`;
-    } else if (hasKind) {
-      return `Unnamed ${kind}`;
-    }
-    return 'Unknown';
-  }
-
   async guessCpuSizes(): Promise<Map<number, string>> {
     const cpuToSize = new Map<number, string>();
     await this.engine.query(`
@@ -394,7 +311,7 @@
       const kind = ASYNC_SLICE_TRACK_KIND;
       const rawName = it.name === null ? undefined : it.name;
       const rawParentName = it.parentName === null ? undefined : it.parentName;
-      const name = TrackDecider.getTrackName({name: rawName, kind});
+      const name = getTrackName({name: rawName, kind});
       const rawTrackIds = it.trackIds;
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const parentTrackId = it.parentId;
@@ -412,8 +329,7 @@
           trackGroup = uuidv4();
           parentIdToGroupId.set(parentTrackId, trackGroup);
 
-          const parentName =
-              TrackDecider.getTrackName({name: rawParentName, kind});
+          const parentName = getTrackName({name: rawParentName, kind});
 
           const summaryTrackId = uuidv4();
           this.tracksToAdd.push({
@@ -463,36 +379,24 @@
 
   async addGpuFreqTracks(engine: EngineProxy): Promise<void> {
     const numGpus = await this.engine.getNumberOfGpus();
-    const maxGpuFreqResult = await engine.query(`
-    select ifnull(max(value), 0) as maximumValue
-    from counter c
-    inner join gpu_counter_track t on c.track_id = t.id
-    where name = 'gpufreq';
-  `);
-    const maximumValue =
-        maxGpuFreqResult.firstRow({maximumValue: NUM}).maximumValue;
-
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
       // gpu freq data.
       const freqExistsResult = await engine.query(`
-      select id
+      select *
       from gpu_counter_track
       where name = 'gpufreq' and gpu_id = ${gpu}
       limit 1;
     `);
       if (freqExistsResult.numRows() > 0) {
-        const trackId = freqExistsResult.firstRow({id: NUM}).id;
         this.tracksToAdd.push({
           engineId: this.engineId,
-          kind: COUNTER_TRACK_KIND,
+          kind: PLUGIN_TRACK_KIND,
           name: `Gpu ${gpu} Frequency`,
           trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {
-            trackId,
-            maximumValue,
-          },
+          config: {},
+          uri: `perfetto.Counter#gpu_freq${gpu}`,
         });
       }
     }
@@ -537,15 +441,12 @@
       const trackId = it.id;
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind: COUNTER_TRACK_KIND,
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
         trackGroup: SCROLLING_TRACK_GROUP,
-        config: {
-          name,
-          trackId,
-          scale: getCounterScale(name),
-        },
+        config: {},
+        uri: `perfetto.Counter#cpu${trackId}`,
       });
     }
   }
@@ -866,17 +767,6 @@
     }
   }
 
-  applyDefaultCounterScale(): void {
-    for (const track of this.tracksToAdd) {
-      if (track.kind === COUNTER_TRACK_KIND) {
-        const scaleConfig = {
-          scale: getCounterScale(track.name),
-        };
-        track.config = Object.assign({}, track.config, scaleConfig);
-      }
-    }
-  }
-
   async addLogsTrack(engine: EngineProxy): Promise<void> {
     const result =
         await engine.query(`select count(1) as cnt from android_logs`);
@@ -1017,44 +907,30 @@
     }
 
     const counterResult = await engine.query(`
-    SELECT
-      id,
-      name,
-      upid,
-      min_value as minValue,
-      max_value as maxValue
-    FROM annotation_counter_track`);
+      SELECT id, name, upid FROM annotation_counter_track
+    `);
 
     const counterIt = counterResult.iter({
       id: NUM,
       name: STR,
       upid: NUM,
-      minValue: NUM_NULL,
-      maxValue: NUM_NULL,
     });
 
     for (; counterIt.valid(); counterIt.next()) {
       const id = counterIt.id;
       const name = counterIt.name;
       const upid = counterIt.upid;
-      const minimumValue =
-          counterIt.minValue === null ? undefined : counterIt.minValue;
-      const maximumValue =
-          counterIt.maxValue === null ? undefined : counterIt.maxValue;
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind: 'CounterTrack',
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
         trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
                                  this.upidToUuid.get(upid),
         config: {
-          name,
           namespace: 'annotation',
-          trackId: id,
-          minimumValue,
-          maximumValue,
         },
+        uri: `perfetto.Annotation#counter${id}`,
       });
     }
   }
@@ -1101,7 +977,7 @@
         this.tracksToAdd.push({
           engineId: this.engineId,
           kind: THREAD_STATE_TRACK_KIND,
-          name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
+          name: getTrackName({utid, tid, threadName, kind}),
           trackGroup: uuid,
           trackSortKey: {
             utid,
@@ -1116,7 +992,7 @@
         this.tracksToAdd.push({
           engineId: this.engineId,
           kind,
-          name: TrackDecider.getTrackName({utid, tid, threadName, kind}),
+          name: getTrackName({utid, tid, threadName, kind}),
           trackGroup: uuid,
           trackSortKey: {
             utid,
@@ -1177,9 +1053,7 @@
       upid,
       tid,
       thread.name as threadName,
-      thread_counter_track.id as trackId,
-      thread.start_ts as startTs,
-      thread.end_ts as endTs
+      thread_counter_track.id as trackId
     from thread_counter_track
     join thread using(utid)
     left join process using(upid)
@@ -1192,9 +1066,7 @@
       upid: NUM_NULL,
       tid: NUM_NULL,
       threadName: STR_NULL,
-      startTs: LONG_NULL,
       trackId: NUM,
-      endTs: LONG_NULL,
     });
     for (; it.valid(); it.next()) {
       const utid = it.utid;
@@ -1204,27 +1076,25 @@
       const trackName = it.trackName;
       const threadName = it.threadName;
       const uuid = this.getUuid(utid, upid);
-      const startTs = it.startTs === null ? undefined : it.startTs;
-      const endTs = it.endTs === null ? undefined : it.endTs;
-      const kind = COUNTER_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, utid, tid, kind, threadName, threadTrack: true});
+      const name = getTrackName({
+        name: trackName,
+        utid,
+        tid,
+        kind: COUNTER_TRACK_KIND,
+        threadName,
+        threadTrack: true,
+      });
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind,
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: {
           utid,
           priority: InThreadTrackSortKey.ORDINARY,
         },
         trackGroup: uuid,
-        config: {
-          name,
-          trackId,
-          startTs,
-          endTs,
-          tid,
-        },
+        config: {},
+        uri: `perfetto.Counter#thread${trackId}`,
       });
     }
   }
@@ -1279,8 +1149,8 @@
       const uuid = this.getUuid(0, upid);
 
       const kind = ASYNC_SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, processName, kind});
+      const name =
+          getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind,
@@ -1343,8 +1213,8 @@
       const uuid = this.getUuid(0, upid);
 
       const kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, processName, kind});
+      const name =
+          getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind,
@@ -1408,8 +1278,8 @@
       const uuid = this.getUuid(0, upid);
 
       const kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, processName, kind});
+      const name =
+          getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
         engineId: this.engineId,
         kind,
@@ -1467,8 +1337,7 @@
       const uuid = this.getUuid(utid, upid);
 
       const kind = SLICE_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, utid, tid, threadName, kind});
+      const name = getTrackName({name: trackName, utid, tid, threadName, kind});
       if (showV1()) {
         this.tracksToAdd.push({
           engineId: this.engineId,
@@ -1514,9 +1383,7 @@
       process_counter_track.name as trackName,
       upid,
       process.pid,
-      process.name as processName,
-      process.start_ts as startTs,
-      process.end_ts as endTs
+      process.name as processName
     from process_counter_track
     join process using(upid);
   `);
@@ -1526,8 +1393,6 @@
       upid: NUM,
       pid: NUM_NULL,
       processName: STR_NULL,
-      startTs: LONG_NULL,
-      endTs: LONG_NULL,
     });
     for (let i = 0; it.valid(); ++i, it.next()) {
       const pid = it.pid;
@@ -1536,24 +1401,17 @@
       const trackName = it.trackName;
       const processName = it.processName;
       const uuid = this.getUuid(0, upid);
-      const startTs = it.startTs === null ? undefined : it.startTs;
-      const endTs = it.endTs === null ? undefined : it.endTs;
-      const kind = COUNTER_TRACK_KIND;
-      const name = TrackDecider.getTrackName(
-          {name: trackName, upid, pid, kind, processName});
+      const name = getTrackName(
+          {name: trackName, upid, pid, kind: COUNTER_TRACK_KIND, processName});
       this.tracksToAdd.push({
         engineId: this.engineId,
-        kind,
+        kind: PLUGIN_TRACK_KIND,
         name,
         trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack(
             upid, trackName || undefined),
         trackGroup: uuid,
-        config: {
-          name,
-          trackId,
-          startTs,
-          endTs,
-        },
+        config: {},
+        uri: `perfetto.Counter#process${trackId}`,
       });
     }
   }
@@ -1866,8 +1724,8 @@
           labels: it.chromeProcessLabels.split(','),
         });
 
-        const name = TrackDecider.getTrackName(
-            {utid, processName, pid, threadName, tid, upid});
+        const name =
+            getTrackName({utid, processName, pid, threadName, tid, upid});
         const addTrackGroup = Actions.addTrackGroup({
           engineId: this.engineId,
           summaryTrackId,
@@ -1934,22 +1792,20 @@
     `);
   }
 
-  async addPluginTracks(): Promise<void> {
-    const promises = pluginManager.findPotentialTracks();
-    const groups = await Promise.all(promises);
-    for (const infos of groups) {
-      for (const info of infos) {
-        this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: info.trackKind,
-          name: info.name,
-          // TODO(hjd): Fix how sorting works. Plugins should expose
-          // 'sort keys' which the user can use to choose a sort order.
-          trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-          trackGroup: SCROLLING_TRACK_GROUP,
-          config: info.config,
-        });
-      }
+  addPluginTracks(): void {
+    const tracks = pluginManager.findPotentialTracks();
+    for (const info of tracks) {
+      this.tracksToAdd.push({
+        engineId: this.engineId,
+        kind: PLUGIN_TRACK_KIND,
+        name: info.name,
+        uri: info.uri,
+        // TODO(hjd): Fix how sorting works. Plugins should expose
+        // 'sort keys' which the user can use to choose a sort order.
+        trackSortKey: info.sortKey,
+        trackGroup: SCROLLING_TRACK_GROUP,
+        config: {},
+      });
     }
   }
 
@@ -1978,7 +1834,7 @@
           this.engine.getProxy('TrackDecider::addCpuFreqLimitCounterTracks'));
     await this.addCpuPerfCounterTracks(
         this.engine.getProxy('TrackDecider::addCpuPerfCounterTracks'));
-    await this.addPluginTracks();
+    this.addPluginTracks();
     await this.addAnnotationTracks(
         this.engine.getProxy('TrackDecider::addAnnotationTracks'));
     await this.groupGlobalIonTracks();
@@ -2058,8 +1914,6 @@
     this.addTrackGroupActions.push(
         Actions.setUtidToTrackSortKey({threadOrderingMetadata}));
 
-    this.applyDefaultCounterScale();
-
     return this.addTrackGroupActions;
   }
 
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 23401a0..955df02 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -43,6 +43,7 @@
 import {fullscreenModalContainer} from './modal';
 import {Omnibox, OmniboxOption} from './omnibox';
 import {runQueryInNewTab} from './query_result_tab';
+import {verticalScrollToTrack} from './scroll_helper';
 import {executeSearch} from './search_handler';
 import {Sidebar} from './sidebar';
 import {SqlTableTab} from './sql_table/tab';
@@ -307,8 +308,9 @@
           },
     },
     {
-      id: 'perfetto.PrintTrackInfoToConsole',
-      name: 'Print track info to console',
+      // Selects & reveals the first track on the timeline with a given URI.
+      id: 'perfetto.FindTrack',
+      name: 'Find track by URI',
       callback:
           async () => {
             const tracks = Array.from(pluginManager.trackRegistry.values());
@@ -326,9 +328,28 @@
             });
 
             try {
-              const uri = await this.prompt('Choose a track...', sortedOptions);
-              const trackDetails = pluginManager.resolveTrackInfo(uri);
-              console.log(trackDetails);
+              const selectedUri =
+                  await this.prompt('Choose a track...', sortedOptions);
+
+              // Find the first track with this URI
+              const firstTrack = Object.values(globals.state.tracks)
+                                     .find(({uri}) => uri === selectedUri);
+              if (firstTrack) {
+                console.log(firstTrack);
+                verticalScrollToTrack(firstTrack.id, true);
+                const traceTime = globals.stateTraceTimeTP();
+                globals.makeSelection(
+                    Actions.selectArea({
+                      area: {
+                        start: traceTime.start,
+                        end: traceTime.end,
+                        tracks: [firstTrack.id],
+                      },
+                    }),
+                );
+              } else {
+                alert(`No tracks with uri ${selectedUri} on the timeline`);
+              }
             } catch {
               // Prompt was probably cancelled - do nothing.
             }
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4d49c1a..c414454 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -12,7 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {TrackState} from 'src/common/state';
+
 import {time} from '../base/time';
+import {pluginManager} from '../common/plugins';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
@@ -67,6 +70,22 @@
   return (obj as {trackGroupId?: string}).trackGroupId !== undefined;
 }
 
+function getTrackIds(track: TrackState): number[] {
+  if (track.uri) {
+    const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+    if (trackInfo?.trackIds) return trackInfo?.trackIds;
+  } else {
+    const config = track.config;
+    if (hasTrackId(config)) {
+      return [config.trackId];
+    }
+    if (hasManyTrackIds(config)) {
+      return config.trackIds;
+    }
+  }
+  return [];
+}
+
 export class FlowEventsRendererArgs {
   trackIdToTrackPanel: Map<number, TrackPanelInfo>;
   groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>;
@@ -78,15 +97,9 @@
 
   registerPanel(panel: PanelVNode, yStart: number, height: number) {
     if (panel.state instanceof TrackPanel && hasId(panel.attrs)) {
-      const config = globals.state.tracks[panel.attrs.id].config;
-      if (hasTrackId(config)) {
-        this.trackIdToTrackPanel.set(
-            config.trackId, {panel: panel.state, yStart});
-      }
-      if (hasManyTrackIds(config)) {
-        for (const trackId of config.trackIds) {
-          this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
-        }
+      const track = globals.state.tracks[panel.attrs.id];
+      for (const trackId of getTrackIds(track)) {
+        this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
       }
     } else if (
         panel.state instanceof TrackGroupPanel &&
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index bb1b2c7..80f690b 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -146,8 +146,8 @@
   for (const track of Object.values(globals.state.tracks)) {
     if (exists(track?.uri)) {
       const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        if (trackInfo?.tags?.cpu === cpu) {
+      if (trackInfo?.kind === CPU_SLICE_TRACK_KIND) {
+        if (trackInfo?.cpu === cpu) {
           trackId = track.id;
           break;
         }
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index e1f2c86..4459c98 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -18,17 +18,19 @@
 import {assertExists} from '../base/logging';
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
+import {pluginManager} from '../common/plugins';
+import {RegistryError} from '../common/registry';
 import {
   getContainingTrackId,
   TrackGroupState,
   TrackState,
 } from '../common/state';
+import {Migrate, TrackContext, TrackLike} from '../public';
 
 import {globals} from './globals';
 import {drawGridLines} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
-import {Track} from './track';
-import {TrackChips, TrackContent} from './track_panel';
+import {renderChips, TrackContent} from './track_panel';
 import {trackRegistry} from './track_registry';
 import {
   drawVerticalLineAtTime,
@@ -43,21 +45,35 @@
   private readonly trackGroupId: string;
   private shellWidth = 0;
   private backgroundColor = '#ffffff';  // Updated from CSS later.
-  private summaryTrack: Track|undefined;
+  private summaryTrack: TrackLike|undefined;
 
-  constructor({attrs}: m.CVnode<Attrs>) {
+  constructor(vnode: m.CVnode<Attrs>) {
     super();
-    this.trackGroupId = attrs.trackGroupId;
-    const trackCreator = trackRegistry.get(this.summaryTrackState.kind);
-    const engineId = this.summaryTrackState.engineId;
-    const engine = globals.engines.get(engineId);
-    if (engine !== undefined) {
-      this.summaryTrack = trackCreator.create({
-        trackId: this.summaryTrackState.id,
-        engine: engine.getProxy(`Track; kind: ${
-            this.summaryTrackState.kind}; id: ${this.summaryTrackState.id}`),
-      });
-    }
+    this.trackGroupId = vnode.attrs.trackGroupId;
+  }
+
+  private tryLoadTrack() {
+    const trackId = this.trackGroupId;
+    const trackState = this.summaryTrackState;
+
+    const {id, uri} = trackState;
+
+    const ctx: TrackContext = {
+      trackInstanceId: id,
+      mountStore: <T>(migrate: Migrate<T>) => {
+        const {store, state} = globals;
+        const migratedState = migrate(state.trackGroups[trackId].state);
+        store.edit((draft) => {
+          draft.trackGroups[trackId].state = migratedState;
+        });
+        return store.createProxy<T>(['trackGroups', trackId, 'state']);
+      },
+    };
+
+    this.summaryTrack =
+        uri ? pluginManager.createTrack(uri, ctx) : loadTrack(trackState, id);
+
+    this.summaryTrack?.onCreate();
   }
 
   get trackGroupState(): TrackGroupState {
@@ -69,6 +85,10 @@
   }
 
   view({attrs}: m.CVnode<Attrs>) {
+    if (!this.summaryTrack) {
+      this.tryLoadTrack();
+    }
+
     const collapsed = this.trackGroupState.collapsed;
     let name = this.trackGroupState.name;
     if (name[0] === '/') {
@@ -132,7 +152,7 @@
                 'h1.track-title',
                 {title: name},
                 name,
-                m(TrackChips, {config: this.summaryTrackState.config}),
+                renderChips(this.summaryTrackState),
                 ),
             (this.trackGroupState.collapsed && child !== null) ?
                 m('h2.track-subtitle', child) :
@@ -286,3 +306,26 @@
 function StripPathFromExecutable(path: string) {
   return path.split('/').slice(-1)[0];
 }
+
+function loadTrack(trackState: TrackState, trackId: string): TrackLike|
+    undefined {
+  const engine = globals.engines.get(trackState.engineId);
+  if (engine === undefined) {
+    return undefined;
+  }
+
+  try {
+    const trackCreator = trackRegistry.get(trackState.kind);
+    return trackCreator.create({
+      trackId,
+      engine:
+          engine.getProxy(`Track; kind: ${trackState.kind}; id: ${trackId}`),
+    });
+  } catch (e) {
+    if (e instanceof RegistryError) {
+      return undefined;
+    } else {
+      throw e;
+    }
+  }
+}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 8dffff1..2c2d5e7 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -18,12 +18,13 @@
 import {currentTargetOffset} from '../base/dom_utils';
 import {Icons} from '../base/semantic_icons';
 import {duration, Span, time} from '../base/time';
+import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {pluginManager} from '../common/plugins';
 import {RegistryError} from '../common/registry';
 import {TrackState} from '../common/state';
 import {raf} from '../core/raf_scheduler';
-import {TrackLike} from '../public';
+import {Migrate, TrackContext, TrackLike} from '../public';
 
 import {SELECTION_FILL_COLOR, TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
@@ -68,24 +69,37 @@
   return selectedArea.tracks.includes(id);
 }
 
-interface TrackChipsAttrs {
-  config: {[k: string]: any};
+interface TrackChipAttrs {
+  text: string;
 }
 
-export class TrackChips implements m.ClassComponent<TrackChipsAttrs> {
-  view({attrs}: m.CVnode<TrackChipsAttrs>) {
-    const {config} = attrs;
-
-    const isMetric = 'namespace' in config;
-    const isDebuggable = ('isDebuggable' in config) && config.isDebuggable;
-
-    return [
-      isMetric && m('span.chip', 'metric'),
-      isDebuggable && m('span.chip', 'debuggable'),
-    ];
+class TrackChip implements m.ClassComponent<TrackChipAttrs> {
+  view({attrs}: m.CVnode<TrackChipAttrs>) {
+    return m('span.chip', attrs.text);
   }
 }
 
+export function renderChips({uri, config}: TrackState) {
+  const tagElements: m.Children = [];
+  if (exists(uri)) {
+    const trackInfo = pluginManager.resolveTrackInfo(uri);
+    const tags = trackInfo?.tags;
+    tags?.metric && tagElements.push(m(TrackChip, {text: 'metric'}));
+    tags?.debuggable && tagElements.push(m(TrackChip, {text: 'debuggable'}));
+  } else {
+    if (config && typeof config === 'object') {
+      if ('namespace' in config) {
+        tagElements.push(m(TrackChip, {text: 'metric'}));
+      }
+      if ('isDebuggable' in config && config.isDebuggable) {
+        tagElements.push(m(TrackChip, {text: 'debuggable'}));
+      }
+    }
+  }
+
+  return tagElements;
+}
+
 interface TrackShellAttrs {
   track: TrackLike;
   trackState: TrackState;
@@ -134,7 +148,7 @@
               },
             },
             attrs.trackState.name,
-            m(TrackChips, {config: attrs.trackState.config}),
+            renderChips(attrs.trackState),
             ),
         m('.track-buttons',
           attrs.track.getTrackShellButtons(),
@@ -352,8 +366,21 @@
     if (!trackState) return;
 
     const {id, uri} = trackState;
-    this.track =
-        uri ? pluginManager.createTrack(uri, id) : loadTrack(trackState, id);
+
+    const trackCtx: TrackContext = {
+      trackInstanceId: id,
+      mountStore: <T>(migrate: Migrate<T>) => {
+        const {store, state} = globals;
+        const migratedState = migrate(state.tracks[trackId].state);
+        globals.store.edit((draft) => {
+          draft.tracks[trackId].state = migratedState;
+        });
+        return store.createProxy<T>(['tracks', trackId, 'state']);
+      },
+    };
+
+    this.track = uri ? pluginManager.createTrack(uri, trackCtx) :
+                       loadTrack(trackState, id);
     this.track?.onCreate();
     this.trackState = trackState;
   }
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index b939428..8c90493 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -170,14 +170,21 @@
   addCommand(command: Command): void;
 }
 
-export interface TrackContext {
-  // A unique ID for the instance of this track.
-  trackInstanceId: string;
-}
+export type Migrate<State> = (init: unknown) => State;
 
 export interface TrackContext {
-  // A unique ID for the instance of this track.
+  // The ID of this track instance.
   trackInstanceId: string;
+
+  // Creates a new store overlaying the track instance's state object.
+  // A migrate function must be passed to convert any existing state to a
+  // compatible format.
+  // When opening a fresh trace, the value of |init| will be undefined, and
+  // state should be updated to an appropriate default value.
+  // When loading a permalink, the value of |init| will be whatever was saved
+  // when the permalink was shared, which might be from an old version of this
+  // track.
+  mountStore<State>(migrate: Migrate<State>): Store<State>;
 }
 
 // TODO(stevegolton): Rename `Track` to `BaseTrack` (or similar) and rename this
@@ -210,10 +217,55 @@
   // A factory function returning the track object.
   trackFactory: (ctx: TrackContext) => TrackLike;
 
-  // A list of tags used for sorting and grouping.
+  // The track "kind" Uued by various subsystems e.g. aggregation controllers.
+  // This is where "XXX_TRACK_KIND" values should be placed.
+  // TODO(stevegolton): This will be deprecated once we handle group selections
+  // in a more generic way - i.e. EventSet.
+  kind: string;
+
+  // An optional list of track IDs represented by this trace.
+  // This list is used for participation in track indexing by track ID.
+  // This index is used by various subsystems to find links between tracks based
+  // on the track IDs used by trace processor.
+  trackIds?: number[];
+
+  // Optional: The CPU number associated with this track.
+  cpu?: number;
+
+  // Optional: A list of tags used for sorting, grouping and "chips".
   tags?: TrackTags;
 }
 
+// Tracks within track groups (usually corresponding to processes) are sorted.
+// As we want to group all tracks related to a given thread together, we use
+// two keys:
+// - Primary key corresponds to a priority of a track block (all tracks related
+//   to a given thread or a single track if it's not thread-associated).
+// - Secondary key corresponds to a priority of a given thread-associated track
+//   within its thread track block.
+// Each track will have a sort key, which either a primary sort key
+// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
+// primary sort key is done independently).
+export enum PrimaryTrackSortKey {
+  DEBUG_SLICE_TRACK,
+  NULL_TRACK,
+  PROCESS_SCHEDULING_TRACK,
+  PROCESS_SUMMARY_TRACK,
+  EXPECTED_FRAMES_SLICE_TRACK,
+  ACTUAL_FRAMES_SLICE_TRACK,
+  PERF_SAMPLES_PROFILE_TRACK,
+  HEAP_PROFILE_TRACK,
+  MAIN_THREAD,
+  RENDER_THREAD,
+  GPU_COMPLETION_THREAD,
+  CHROME_IO_THREAD,
+  CHROME_COMPOSITOR_THREAD,
+  ORDINARY_THREAD,
+  COUNTER_TRACK,
+  ASYNC_SLICE_TRACK,
+  ORDINARY_TRACK,
+}
+
 // Similar to PluginContext but with additional properties to operate on the
 // currently loaded trace. Passed to trace-relevant hooks instead of
 // PluginContext.
@@ -224,6 +276,11 @@
   // Add a new track from this plugin. The track is just made available here,
   // it's not automatically shown until it's added to a workspace.
   addTrack(trackDetails: PluginTrackInfo): void;
+
+  // Suggest a track be added to the workspace on a fresh trace load.
+  // Supersedes `findPotentialTracks()` which has been removed.
+  // Note: this API will be deprecated soon.
+  suggestTrack(trackInfo: TrackInfo): void;
 }
 
 export interface BasePlugin<State> {
@@ -235,7 +292,6 @@
 
   // Extension points.
   metricVisualisations?(ctx: PluginContext): MetricVisualisation[];
-  findPotentialTracks?(ctx: TracePluginContext<State>): Promise<TrackInfo[]>;
 }
 
 export interface StatefulPlugin<State> extends BasePlugin<State> {
@@ -265,16 +321,16 @@
 }
 
 export interface TrackInfo {
-  // The id of this 'type' of track. This id is used to select the
-  // correct |TrackCreator| to construct the track.
-  trackKind: string;
-
   // A human readable name for this specific track. It will normally be
   // displayed on the left-hand-side of the track.
   name: string;
 
-  // An opaque config for the track.
-  config: {};
+  // Used to define default sort order for new traces.
+  // Note: sortKey will be deprecated soon in favour of tags.
+  sortKey: PrimaryTrackSortKey;
+
+  // URI of the suggested track.
+  uri: string;
 }
 
 // A predicate for selecting a groups of tracks.
@@ -284,23 +340,21 @@
   // A human readable name for this specific track.
   name: string;
 
-  // This is where "XXX_TRACK_KIND" values should be placed.
-  kind: string;
+  // Controls whether to show the "metric" chip.
+  metric: boolean;
 
-  // The CPU number associated with this track.
-  cpu: number;
+  // Controls whether to show the "debuggable" chip.
+  debuggable: boolean;
 }
 
 // An set of key/value pairs describing a given track. These are used for
-// selecting tracks to pin/unpin and (in future) the sorting and grouping of
-// tracks.
-// These are also (ab)used for communicating information about tracks for the
-// purposes of locating tracks by their properties e.g. aggregation & search.
+// selecting tracks to pin/unpin, diplsaying "chips" in the track shell, and
+// (in future) the sorting and grouping of tracks.
 // We define a handful of well known fields, and the rest are arbitrary key-
 // value pairs.
 export type TrackTags = Partial<WellKnownTrackTags>&{
   // There may be arbitrary other key/value pairs.
-  [key: string]: string|number|undefined;
+  [key: string]: string|number|boolean|undefined;
 }
 
 // Plugins can be passed as class refs, factory functions, or concrete plugin
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
new file mode 100644
index 0000000..d8db8f0
--- /dev/null
+++ b/ui/src/public/utils.ts
@@ -0,0 +1,73 @@
+// Copyright (C) 2023 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 function getTrackName(args: Partial<{
+  name: string | null,
+  utid: number,
+  processName: string | null,
+  pid: number | null,
+  threadName: string | null,
+  tid: number | null,
+  upid: number | null,
+  kind: string,
+  threadTrack: boolean
+}>) {
+  const {
+    name,
+    upid,
+    utid,
+    processName,
+    threadName,
+    pid,
+    tid,
+    kind,
+    threadTrack,
+  } = args;
+
+  const hasName = name !== undefined && name !== null && name !== '[NULL]';
+  const hasUpid = upid !== undefined && upid !== null;
+  const hasUtid = utid !== undefined && utid !== null;
+  const hasProcessName = processName !== undefined && processName !== null;
+  const hasThreadName = threadName !== undefined && threadName !== null;
+  const hasTid = tid !== undefined && tid !== null;
+  const hasPid = pid !== undefined && pid !== null;
+  const hasKind = kind !== undefined;
+  const isThreadTrack = threadTrack !== undefined && threadTrack;
+
+  // If we don't have any useful information (better than
+  // upid/utid) we show the track kind to help with tracking
+  // down where this is coming from.
+  const kindSuffix = hasKind ? ` (${kind})` : '';
+
+  if (isThreadTrack && hasName && hasTid) {
+    return `${name} (${tid})`;
+  } else if (hasName) {
+    return `${name}`;
+  } else if (hasUpid && hasPid && hasProcessName) {
+    return `${processName} ${pid}`;
+  } else if (hasUpid && hasPid) {
+    return `Process ${pid}`;
+  } else if (hasThreadName && hasTid) {
+    return `${threadName} ${tid}`;
+  } else if (hasTid) {
+    return `Thread ${tid}`;
+  } else if (hasUpid) {
+    return `upid: ${upid}${kindSuffix}`;
+  } else if (hasUtid) {
+    return `utid: ${utid}${kindSuffix}`;
+  } else if (hasKind) {
+    return `Unnamed ${kind}`;
+  }
+  return 'Unknown';
+}
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index 948f82d..597d334 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -30,6 +30,8 @@
   TracePluginContext,
 } from '../../public';
 
+export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
+
 export interface Data extends TrackData {
   // Total number of log events within [start, end], before any quantization.
   numEvents: number;
@@ -155,6 +157,7 @@
       ctx.addTrack({
         uri: 'perfetto.AndroidLog',
         displayName: 'Android logs',
+        kind: ANDROID_LOGS_TRACK_KIND,
         trackFactory: ({trackInstanceId}) => {
           return new TrackWithControllerAdapter<Config, Data>(
               ctx.engine,
diff --git a/ui/src/tracks/annotation/index.ts b/ui/src/tracks/annotation/index.ts
new file mode 100644
index 0000000..e2baabb
--- /dev/null
+++ b/ui/src/tracks/annotation/index.ts
@@ -0,0 +1,90 @@
+// Copyright (C) 2021 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 {
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../../common/query_result';
+import {
+  Plugin,
+  PluginContext,
+  PluginInfo,
+  TracePluginContext,
+} from '../../public';
+import {
+  Config as CounterTrackConfig,
+  COUNTER_TRACK_KIND,
+  CounterTrack,
+} from '../counter';
+
+class AnnotationPlugin implements Plugin {
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+    await this.addAnnotationCounterTracks(ctx);
+  }
+
+  private async addAnnotationCounterTracks(ctx: TracePluginContext) {
+    const {engine} = ctx;
+    const counterResult = await engine.query(`
+      SELECT
+        id,
+        name,
+        min_value as minValue,
+        max_value as maxValue
+      FROM annotation_counter_track`);
+
+    const counterIt = counterResult.iter({
+      id: NUM,
+      name: STR,
+      minValue: NUM_NULL,
+      maxValue: NUM_NULL,
+    });
+
+    for (; counterIt.valid(); counterIt.next()) {
+      const id = counterIt.id;
+      const name = counterIt.name;
+      const minimumValue =
+          counterIt.minValue === null ? undefined : counterIt.minValue;
+      const maximumValue =
+          counterIt.maxValue === null ? undefined : counterIt.maxValue;
+
+      const config: CounterTrackConfig = {
+        name,
+        trackId: id,
+        namespace: 'annotation',
+        minimumValue,
+        maximumValue,
+      };
+
+      ctx.addTrack({
+        uri: `perfetto.Annotation#counter${id}`,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        tags: {
+          metric: true,
+        },
+        trackFactory: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
+}
+
+export const plugin: PluginInfo = {
+  pluginId: 'perfetto.Annotation',
+  plugin: AnnotationPlugin,
+};
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 370c754..8632bf4 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -21,12 +21,13 @@
 import {
   generateSqlWithInternalLayout,
 } from '../../common/internal_layout_utils';
-import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
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 a2f83f7..b4d47ff 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
@@ -19,12 +19,12 @@
 } from '../../common/colorizer';
 import {Engine} from '../../common/engine';
 import {
-  PrimaryTrackSortKey,
   SCROLLING_TRACK_GROUP,
 } from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index d6bd1d5..56a672b 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -15,20 +15,20 @@
 import {v4 as uuidv4} from 'uuid';
 
 import {Engine} from '../../common/engine';
-import {
-  PrimaryTrackSortKey,
-  SCROLLING_TRACK_GROUP,
-} from '../../common/state';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
-import {ScrollJankPluginState} from './index';
 
-import {ScrollJankTracks as DecideTracksResult} from './index';
+import {
+  ScrollJankPluginState,
+  ScrollJankTracks as DecideTracksResult,
+} from './index';
 import {ScrollDetailsPanel} from './scroll_details_panel';
 
 export {Data} from '../chrome_slices';
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index cb96f6f..478266d 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -13,28 +13,36 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
 
 import {searchSegment} from '../../base/binary_search';
 import {assertTrue} from '../../base/logging';
 import {duration, time, Time} from '../../base/time';
 import {Actions} from '../../common/actions';
+import {
+  BasicAsyncTrack,
+  NUM_NULL,
+  STR_NULL,
+} from '../../common/basic_async_track';
 import {drawTrackHoverTooltip} from '../../common/canvas_utils';
 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 {
+  EngineProxy,
   LONG,
   LONG_NULL,
   NUM,
   Plugin,
   PluginContext,
   PluginInfo,
+  PrimaryTrackSortKey,
+  Store,
   STR,
   TracePluginContext,
-  TrackInfo,
+  TrackContext,
 } from '../../public';
+import {getTrackName} from '../../public/utils';
 import {Button} from '../../widgets/button';
 import {MenuItem, PopupMenu2} from '../../widgets/menu';
 
@@ -66,25 +74,103 @@
   minimumValue?: number;
   startTs?: time;
   endTs?: time;
-  namespace: string;
+  namespace?: string;
   trackId: number;
-  scale?: CounterScaleOptions;
+  defaultScale?: CounterScaleOptions;
 }
 
-class CounterTrackController extends TrackController<Config, Data> {
-  static readonly kind = COUNTER_TRACK_KIND;
+const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
+const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
+
+// Sets the default 'scale' for counter tracks. If the regex matches
+// then the paired mode is used. Entries are in priority order so the
+// first match wins.
+const COUNTER_REGEX: [RegExp, CounterScaleOptions][] = [
+  // Power counters make more sense in rate mode since you're typically
+  // interested in the slope of the graph rather than the absolute
+  // value.
+  [new RegExp('^power\..*$'), 'RATE'],
+  // Same for network counters.
+  [NETWORK_TRACK_REGEX, 'RATE'],
+  // Entity residency
+  [ENTITY_RESIDENCY_REGEX, 'RATE'],
+];
+
+function getCounterScale(name: string): CounterScaleOptions|undefined {
+  for (const [re, scale] of COUNTER_REGEX) {
+    if (name.match(re)) {
+      return scale;
+    }
+  }
+  return undefined;
+}
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 3.5;
+const RECT_HEIGHT = 24.5;
+
+interface CounterTrackState {
+  scale: CounterScaleOptions;
+}
+
+function isCounterState(x: unknown): x is CounterTrackState {
+  if (x && typeof x === 'object' && 'scale' in x) {
+    if (typeof x.scale === 'string') {
+      return true;
+    } else {
+      return false;
+    }
+  } else {
+    return false;
+  }
+}
+
+export class CounterTrack extends BasicAsyncTrack<Data> {
   private setup = false;
   private maximumValueSeen = 0;
   private minimumValueSeen = 0;
   private maximumDeltaSeen = 0;
   private minimumDeltaSeen = 0;
   private maxDurNs: duration = 0n;
+  private store: Store<CounterTrackState>;
+  private id: string;
+  private uuid = uuidv4();
+
+  constructor(
+      ctx: TrackContext, private config: Config, private engine: EngineProxy) {
+    super();
+    this.id = ctx.trackInstanceId;
+    this.store = ctx.mountStore<CounterTrackState>((init: unknown) => {
+      if (isCounterState(init)) {
+        return init;
+      } else {
+        return {scale: this.config.defaultScale ?? 'ZERO_BASED'};
+      }
+    });
+  }
+
+  // Returns a valid SQL table name with the given prefix that should be unique
+  // for each track.
+  tableName(prefix: string) {
+    // Derive table name from, since that is unique for each track.
+    // Track ID can be UUID but '-' is not valid for sql table name.
+    const idSuffix = this.uuid.split('-').join('_');
+    return `${prefix}_${idSuffix}`;
+  }
+
+  private namespaceTable(tableName: string): string {
+    if (this.config.namespace) {
+      return this.config.namespace + '_' + tableName;
+    } else {
+      return tableName;
+    }
+  }
 
   async onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data> {
     if (!this.setup) {
       if (this.config.namespace === undefined) {
-        await this.query(`
+        await this.engine.query(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -96,7 +182,7 @@
           where track_id = ${this.config.trackId};
         `);
       } else {
-        await this.query(`
+        await this.engine.query(`
           create view ${this.tableName('counter_view')} as
           select
             id,
@@ -109,7 +195,7 @@
         `);
       }
 
-      const maxDurResult = await this.query(`
+      const maxDurResult = await this.engine.query(`
           select
             max(
               iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
@@ -118,7 +204,7 @@
       `);
       this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
 
-      const queryRes = await this.query(`
+      const queryRes = await this.engine.query(`
         select
           ifnull(max(value), 0) as maxValue,
           ifnull(min(value), 0) as minValue,
@@ -135,7 +221,7 @@
       this.setup = true;
     }
 
-    const queryRes = await this.query(`
+    const queryRes = await this.engine.query(`
       select
         (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
         min(value) as minValue,
@@ -219,34 +305,18 @@
       return this.config.minimumValue;
     }
   }
-}
-
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 3.5;
-const RECT_HEIGHT = 24.5;
-
-class CounterTrack extends Track<Config, Data> {
-  static readonly kind = COUNTER_TRACK_KIND;
-  static create(args: NewTrackArgs): CounterTrack {
-    return new CounterTrack(args);
-  }
 
   private mousePos = {x: 0, y: 0};
   private hoveredValue: number|undefined = undefined;
   private hoveredTs: time|undefined = undefined;
   private hoveredTsEnd: time|undefined = undefined;
 
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
   getHeight() {
     return MARGIN_TOP + RECT_HEIGHT;
   }
 
   getContextMenu(): m.Vnode<any> {
-    const currentScale = this.config.scale;
+    const currentScale = this.store.state.scale;
     const scales: {name: CounterScaleOptions, humanName: string}[] = [
       {name: 'ZERO_BASED', humanName: 'Zero based'},
       {name: 'MIN_MAX', humanName: 'Min/Max'},
@@ -258,10 +328,8 @@
         label: scale.humanName,
         active: currentScale === scale.name,
         onclick: () => {
-          this.config.scale = scale.name;
-          Actions.updateTrackConfig({
-            id: this.trackState.id,
-            config: this.config,
+          this.store.edit((draft) => {
+            draft.scale = scale.name;
           });
         },
       });
@@ -282,7 +350,7 @@
       visibleTimeScale: timeScale,
       windowSpan,
     } = globals.frontendLocalState;
-    const data = this.data();
+    const data = this.data;
 
     // Can't possibly draw anything.
     if (data === undefined || data.timestamps.length === 0) {
@@ -295,7 +363,7 @@
     assertTrue(data.timestamps.length === data.totalDeltas.length);
     assertTrue(data.timestamps.length === data.rate.length);
 
-    const scale: CounterScaleOptions = this.config.scale || 'ZERO_BASED';
+    const scale: CounterScaleOptions = this.store.state.scale;
 
     let minValues = data.minValues;
     let maxValues = data.maxValues;
@@ -485,17 +553,17 @@
   }
 
   onMouseMove(pos: {x: number, y: number}) {
-    const data = this.data();
+    const data = this.data;
     if (data === undefined) return;
     this.mousePos = pos;
     const {visibleTimeScale} = globals.frontendLocalState;
     const time = visibleTimeScale.pxToHpTime(pos.x);
 
     let values = data.lastValues;
-    if (this.config.scale === 'DELTA_FROM_PREVIOUS') {
+    if (this.store.state.scale === 'DELTA_FROM_PREVIOUS') {
       values = data.totalDeltas;
     }
-    if (this.config.scale === 'RATE') {
+    if (this.store.state.scale === 'RATE') {
       values = data.rate;
     }
 
@@ -513,7 +581,7 @@
   }
 
   onMouseClick({x}: {x: number}): boolean {
-    const data = this.data();
+    const data = this.data;
     if (data === undefined) return false;
     const {visibleTimeScale} = globals.frontendLocalState;
     const time = visibleTimeScale.pxToHpTime(x);
@@ -527,21 +595,58 @@
         leftTs: Time.fromRaw(data.timestamps[left]),
         rightTs: Time.fromRaw(right !== -1 ? data.timestamps[right] : -1n),
         id: counterId,
-        trackId: this.trackState.id,
+        trackId: this.id,
       }));
       return true;
     }
   }
+
+  onDestroy(): void {
+    this.engine.query(`DROP VIEW IF EXISTS ${this.tableName('counter_view')}`);
+  }
+}
+
+interface CounterInfo {
+  name: string;
+  trackId: number;
 }
 
 class CounterPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(CounterTrackController);
-    ctx.registerTrack(CounterTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+    await this.addCounterTracks(ctx);
+    await this.addGpuFrequencyTracks(ctx);
+    await this.addCpuFreqLimitCounterTracks(ctx);
+    await this.addCpuPerfCounterTracks(ctx);
+    await this.addThreadCounterTracks(ctx);
+    await this.addProcessCounterTracks(ctx);
   }
 
-  async findPotentialTracks({engine}: TracePluginContext):
-      Promise<TrackInfo[]> {
+  private async addCounterTracks(ctx: TracePluginContext) {
+    const counters = await this.getCounterNames(ctx.engine);
+    for (const {trackId, name} of counters) {
+      const config:
+          Config = {name, trackId, defaultScale: getCounterScale(name)};
+      const uri = `perfetto.Counter#${trackId}`;
+      ctx.addTrack({
+        uri,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        trackIds: [trackId],
+        trackFactory: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+      ctx.suggestTrack({
+        uri,
+        name,
+        sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
+      });
+    }
+  }
+
+  private async getCounterNames(engine: EngineProxy): Promise<CounterInfo[]> {
     const result = await engine.query(`
     select name, id
     from (
@@ -562,21 +667,234 @@
       id: NUM,
     });
 
-    const tracks: TrackInfo[] = [];
+    const tracks: CounterInfo[] = [];
     for (; it.valid(); it.next()) {
-      const name = it.name;
-      const trackId = it.id;
       tracks.push({
-        trackKind: COUNTER_TRACK_KIND,
-        name,
-        config: {
-          name,
-          trackId,
-        },
+        trackId: it.id,
+        name: it.name,
       });
     }
     return tracks;
   }
+
+  private async addGpuFrequencyTracks(ctx: TracePluginContext) {
+    const engine = ctx.engine;
+    const numGpus = await engine.getNumberOfGpus();
+    const maxGpuFreqResult = await engine.query(`
+      select ifnull(max(value), 0) as maximumValue
+      from counter c
+      inner join gpu_counter_track t on c.track_id = t.id
+      where name = 'gpufreq';
+    `);
+    const maximumValue =
+        maxGpuFreqResult.firstRow({maximumValue: NUM}).maximumValue;
+
+    for (let gpu = 0; gpu < numGpus; gpu++) {
+      // Only add a gpu freq track if we have
+      // gpu freq data.
+      const freqExistsResult = await engine.query(`
+      select id
+      from gpu_counter_track
+      where name = 'gpufreq' and gpu_id = ${gpu}
+      limit 1;
+    `);
+      if (freqExistsResult.numRows() > 0) {
+        const trackId = freqExistsResult.firstRow({id: NUM}).id;
+        const uri = `perfetto.Counter#gpu_freq${gpu}`;
+        const name = `Gpu ${gpu} Frequency`;
+        const config: Config = {
+          name,
+          trackId,
+          maximumValue,
+          defaultScale: getCounterScale(name),
+        };
+        ctx.addTrack({
+          uri,
+          displayName: name,
+          kind: COUNTER_TRACK_KIND,
+          trackIds: [trackId],
+          trackFactory: (trackCtx) => {
+            return new CounterTrack(trackCtx, config, ctx.engine);
+          },
+        });
+      }
+    }
+  }
+
+  async addCpuFreqLimitCounterTracks(ctx: TracePluginContext): Promise<void> {
+    const cpuFreqLimitCounterTracksSql = `
+      select name, id
+      from cpu_counter_track
+      where name glob "Cpu * Freq Limit"
+      order by name asc
+    `;
+
+    this.addCpuCounterTracks(ctx, cpuFreqLimitCounterTracksSql);
+  }
+
+  async addCpuPerfCounterTracks(ctx: TracePluginContext): Promise<void> {
+    // Perf counter tracks are bound to CPUs, follow the scheduling and
+    // frequency track naming convention ("Cpu N ...").
+    // Note: we might not have a track for a given cpu if no data was seen from
+    // it. This might look surprising in the UI, but placeholder tracks are
+    // wasteful as there's no way of collapsing global counter tracks at the
+    // moment.
+    const addCpuPerfCounterTracksSql = `
+      select printf("Cpu %u %s", cpu, name) as name, id
+      from perf_counter_track as pct
+      order by perf_session_id asc, pct.name asc, cpu asc
+    `;
+    this.addCpuCounterTracks(ctx, addCpuPerfCounterTracksSql);
+  }
+
+  async addCpuCounterTracks(ctx: TracePluginContext, sql: string):
+      Promise<void> {
+    const result = await ctx.engine.query(sql);
+
+    const it = result.iter({
+      name: STR,
+      id: NUM,
+    });
+
+    for (; it.valid(); it.next()) {
+      const name = it.name;
+      const trackId = it.id;
+      const config: Config = {
+        name,
+        trackId,
+        defaultScale: getCounterScale(name),
+      };
+      ctx.addTrack({
+        uri: `perfetto.Counter#cpu${trackId}`,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        trackIds: [trackId],
+        trackFactory: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
+
+  async addThreadCounterTracks(ctx: TracePluginContext): Promise<void> {
+    const result = await ctx.engine.query(`
+      select
+        thread_counter_track.name as trackName,
+        utid,
+        upid,
+        tid,
+        thread.name as threadName,
+        thread_counter_track.id as trackId,
+        thread.start_ts as startTs,
+        thread.end_ts as endTs
+      from thread_counter_track
+      join thread using(utid)
+      left join process using(upid)
+      where thread_counter_track.name != 'thread_time'
+    `);
+
+    const it = result.iter({
+      startTs: LONG_NULL,
+      trackId: NUM,
+      endTs: LONG_NULL,
+      trackName: STR_NULL,
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      threadName: STR_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const tid = it.tid;
+      const startTs = it.startTs === null ? undefined : it.startTs;
+      const endTs = it.endTs === null ? undefined : it.endTs;
+      const trackId = it.trackId;
+      const trackName = it.trackName;
+      const threadName = it.threadName;
+      const kind = COUNTER_TRACK_KIND;
+      const name = getTrackName({
+        name: trackName,
+        utid,
+        tid,
+        kind,
+        threadName,
+        threadTrack: true,
+      });
+      const config: Config = {
+        name,
+        trackId,
+        startTs: Time.fromRaw(startTs),
+        endTs: Time.fromRaw(endTs),
+        defaultScale: getCounterScale(name),
+      };
+      ctx.addTrack({
+        uri: `perfetto.Counter#thread${trackId}`,
+        displayName: name,
+        kind,
+        trackIds: [trackId],
+        trackFactory: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
+
+  async addProcessCounterTracks(ctx: TracePluginContext): Promise<void> {
+    const result = await ctx.engine.query(`
+    select
+      process_counter_track.id as trackId,
+      process_counter_track.name as trackName,
+      upid,
+      process.pid,
+      process.name as processName,
+      process.start_ts as startTs,
+      process.end_ts as endTs
+    from process_counter_track
+    join process using(upid);
+  `);
+    const it = result.iter({
+      trackId: NUM,
+      trackName: STR_NULL,
+      upid: NUM,
+      startTs: LONG_NULL,
+      endTs: LONG_NULL,
+      pid: NUM_NULL,
+      processName: STR_NULL,
+    });
+    for (let i = 0; it.valid(); ++i, it.next()) {
+      const trackId = it.trackId;
+      const startTs = it.startTs === null ? undefined : it.startTs;
+      const endTs = it.endTs === null ? undefined : it.endTs;
+      const pid = it.pid;
+      const trackName = it.trackName;
+      const upid = it.upid;
+      const processName = it.processName;
+      const kind = COUNTER_TRACK_KIND;
+      const name = getTrackName({
+        name: trackName,
+        upid,
+        pid,
+        kind,
+        processName,
+      });
+      const config: Config = {
+        name,
+        trackId,
+        startTs: Time.fromRaw(startTs),
+        endTs: Time.fromRaw(endTs),
+        defaultScale: getCounterScale(name),
+      };
+      ctx.addTrack({
+        uri: `perfetto.Counter#process${trackId}`,
+        displayName: name,
+        kind: COUNTER_TRACK_KIND,
+        trackIds: [trackId],
+        trackFactory: (trackCtx) => {
+          return new CounterTrack(trackCtx, config, ctx.engine);
+        },
+      });
+    }
+  }
 }
 
 export const plugin: PluginInfo = {
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 40526a4..7e992ee 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -484,10 +484,8 @@
       ctx.addTrack({
         uri,
         displayName: name,
-        tags: {
-          cpu,
-          kind: CPU_SLICE_TRACK_KIND,
-        },
+        kind: CPU_SLICE_TRACK_KIND,
+        cpu,
         trackFactory: ({trackInstanceId}) => {
           return new TrackWithControllerAdapter<Config, Data>(
               ctx.engine,
diff --git a/ui/src/tracks/ftrace/index.ts b/ui/src/tracks/ftrace/index.ts
index 124696e..54cc44f 100644
--- a/ui/src/tracks/ftrace/index.ts
+++ b/ui/src/tracks/ftrace/index.ts
@@ -27,6 +27,7 @@
   TracePluginContext,
 } from '../../public';
 
+export const FTRACE_RAW_TRACK_KIND = 'FtraceRawTrack';
 
 export interface Data extends TrackData {
   timestamps: BigInt64Array;
@@ -146,6 +147,8 @@
       ctx.addTrack({
         uri,
         displayName: `Ftrace Track for CPU ${cpuNum}`,
+        kind: FTRACE_RAW_TRACK_KIND,
+        cpu: cpuNum,
         trackFactory: () => {
           return new FtraceRawTrack(ctx.engine, cpuNum);
         },
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index 63ff916..c15dec6 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.ts
@@ -16,12 +16,16 @@
 
 import {AddTrackArgs} from '../../common/actions';
 import {Engine} from '../../common/engine';
-import {PrimaryTrackSortKey} from '../../common/state';
 import {
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
-import {Plugin, PluginContext, PluginInfo} from '../../public';
+import {
+  Plugin,
+  PluginContext,
+  PluginInfo,
+  PrimaryTrackSortKey,
+} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,