[ui] Port process scheduling and summary tracks to plugin tracks.

Change-Id: I2cbfc5fe6d5a4b975af220a39ea92fdd8ec1a9c2
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index de857bc..8713c17 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -21,7 +21,7 @@
 import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile';
 import {
   PROCESS_SCHEDULING_TRACK_KIND,
-} from '../tracks/process_scheduling';
+} from '../tracks/process_summary/process_scheduling_track';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
 
 import {StateActions} from './actions';
diff --git a/ui/src/common/track_adapter.ts b/ui/src/common/track_adapter.ts
index 54b5055..0538e23 100644
--- a/ui/src/common/track_adapter.ts
+++ b/ui/src/common/track_adapter.ts
@@ -195,7 +195,7 @@
   // don't have access to it.
   private uuid = uuidv4();
 
-  constructor(protected config: Config, private engine: EngineProxy) {}
+  constructor(protected config: Config, protected engine: EngineProxy) {}
 
   protected async query(query: string) {
     const result = await this.engine.query(query);
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index a806363..c29e7c5 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -61,10 +61,6 @@
   PERF_SAMPLES_PROFILE_TRACK_KIND,
 } from '../tracks/perf_samples_profile';
 import {
-  PROCESS_SCHEDULING_TRACK_KIND,
-} from '../tracks/process_scheduling';
-import {PROCESS_SUMMARY_TRACK} from '../tracks/process_summary';
-import {
   decideTracks as screenshotDecideTracks,
 } from '../tracks/screenshots';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
@@ -1529,10 +1525,11 @@
     this.tracksToAdd.push({
       id: summaryTrackId,
       engineId: this.engineId,
-      kind: PROCESS_SUMMARY_TRACK,
+      kind: PLUGIN_TRACK_KIND,
       trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
       name: `Kernel thread summary`,
-      config: {pidForColor: 2, upid: it.upid, utid: it.utid},
+      config: {},
+      uri: 'perfetto.ProcessSummary#kernel',
     });
     const addTrackGroup = Actions.addTrackGroup({
       engineId: this.engineId,
@@ -1680,7 +1677,6 @@
       processName: STR_NULL,
       hasSched: NUM_NULL,
       hasHeapProfiles: NUM_NULL,
-      isDebuggable: NUM_NULL,
       chromeProcessLabels: STR,
     });
     for (; it.valid(); it.next()) {
@@ -1692,7 +1688,6 @@
       const processName = it.processName;
       const hasSched = !!it.hasSched;
       const hasHeapProfiles = !!it.hasHeapProfiles;
-      const isDebuggable = !!it.isDebuggable;
 
       // Group by upid if present else by utid.
       let pUuid =
@@ -1701,27 +1696,20 @@
       if (pUuid === undefined) {
         pUuid = this.getOrCreateUuid(utid, upid);
         const summaryTrackId = uuidv4();
-
-        const pidForColor = pid || tid || upid || utid || 0;
-        const kind =
-            hasSched ? PROCESS_SCHEDULING_TRACK_KIND : PROCESS_SUMMARY_TRACK;
+        const type = hasSched ? 'schedule' : 'summary';
+        const uri = `perfetto.ProcessScheduling#${utid}.${type}`;
 
         this.tracksToAdd.push({
           id: summaryTrackId,
           engineId: this.engineId,
-          kind,
+          kind: PLUGIN_TRACK_KIND,
           trackSortKey: hasSched ?
               PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK :
               PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
           name: `${upid === null ? tid : pid} summary`,
-          config: {
-            pidForColor,
-            upid,
-            utid,
-            tid,
-            isDebuggable: isDebuggable ?? undefined,
-          },
+          config: {},
           labels: it.chromeProcessLabels.split(','),
+          uri,
         });
 
         const name =
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index ea795a4..4e46456 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -12,212 +12,337 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath} from '../../base/bigint_math';
-import {assertFalse} from '../../base/logging';
-import {duration, Time, time} from '../../base/time';
-import {colorForTid} from '../../common/colorizer';
-import {NUM} from '../../common/query_result';
-import {LIMIT, 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 {v4 as uuidv4} from 'uuid';
 
-export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
+import {
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+} from '../../common/query_result';
+import {TrackWithControllerAdapter} from '../../common/track_adapter';
+import {
+  Plugin,
+  PluginContext,
+  PluginInfo,
+  TracePluginContext,
+} from '../../public';
 
-// TODO(dproy): Consider deduping with CPU summary data.
-export interface Data extends TrackData {
-  bucketSize: duration;
-  utilizations: Float64Array;
-}
+import {
+  Config as ProcessSchedulingTrackConfig,
+  Data as ProcessSchedulingTrackData,
+  PROCESS_SCHEDULING_TRACK_KIND,
+  ProcessSchedulingTrack,
+  ProcessSchedulingTrackController,
+} from './process_scheduling_track';
+import {
+  Config as ProcessSummaryTrackConfig,
+  Data as ProcessSummaryTrackData,
+  PROCESS_SUMMARY_TRACK,
+  ProcessSummaryTrack,
+  ProcessSummaryTrackController,
+} from './process_summary_track';
 
-export interface Config {
-  pidForColor: number;
-  upid: number|null;
-  utid: number;
-}
+// This plugin now manages both process "scheduling" and "summary" tracks.
+class ProcessSummaryPlugin implements Plugin {
+  private upidToUuid = new Map<number, string>();
+  private utidToUuid = new Map<number, string>();
 
-// This is the summary displayed when a process only contains chrome slices
-// and no cpu scheduling.
-class ProcessSummaryTrackController extends TrackController<Config, Data> {
-  static readonly kind = PROCESS_SUMMARY_TRACK;
-  private setup = false;
+  onActivate(_ctx: PluginContext): void {}
 
-  async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
-    assertFalse(resolution === 0n, 'Resolution cannot be 0');
+  async onTraceLoad(ctx: TracePluginContext): Promise<void> {
+    await this.addProcessTrackGroups(ctx);
+    await this.addKernelThreadSummary(ctx);
+  }
 
-    if (this.setup === false) {
-      await this.query(
-          `create virtual table ${this.tableName('window')} using window;`);
+  private async addProcessTrackGroups(ctx: TracePluginContext): Promise<void> {
+    this.upidToUuid.clear();
+    this.utidToUuid.clear();
 
-      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);
+    // We want to create groups of tracks in a specific order.
+    // The tracks should be grouped:
+    //    by upid
+    //    or (if upid is null) by utid
+    // the groups should be sorted by:
+    //  Chrome-based process rank based on process names (e.g. Browser)
+    //  has a heap profile or not
+    //  total cpu time *for the whole parent process*
+    //  process name
+    //  upid
+    //  thread name
+    //  utid
+    const result = await ctx.engine.query(`
+    select
+      the_tracks.upid,
+      the_tracks.utid,
+      total_dur as hasSched,
+      hasHeapProfiles,
+      process.pid as pid,
+      thread.tid as tid,
+      process.name as processName,
+      thread.name as threadName,
+      package_list.debuggable as isDebuggable,
+      ifnull((
+        select group_concat(string_value)
+        from args
+        where
+          process.arg_set_id is not null and
+          arg_set_id = process.arg_set_id and
+          flat_key = 'chrome.process_label'
+      ), '') AS chromeProcessLabels,
+      (case process.name
+         when 'Browser' then 3
+         when 'Gpu' then 2
+         when 'Renderer' then 1
+         else 0
+      end) as chromeProcessRank
+    from (
+      select upid, 0 as utid from process_track
+      union
+      select upid, 0 as utid from process_counter_track
+      union
+      select upid, utid from thread_counter_track join thread using(utid)
+      union
+      select upid, utid from thread_track join thread using(utid)
+      union
+      select upid, utid from sched join thread using(utid) group by utid
+      union
+      select upid, 0 as utid from (
+        select distinct upid
+        from perf_sample join thread using (utid) join process using (upid)
+        where callsite_id is not null)
+      union
+      select upid, utid from (
+        select distinct(utid) from cpu_profile_stack_sample
+      ) join thread using(utid)
+      union
+      select distinct(upid) as upid, 0 as utid from heap_profile_allocation
+      union
+      select distinct(upid) as upid, 0 as utid from heap_graph_object
+    ) the_tracks
+    left join (
+      select upid, sum(thread_total_dur) as total_dur
+      from (
+        select utid, sum(dur) as thread_total_dur
+        from sched where dur != -1 and utid != 0
+        group by utid
+      )
+      join thread using (utid)
+      group by upid
+    ) using(upid)
+    left join (
+      select
+        distinct(upid) as upid,
+        true as hasHeapProfiles
+      from heap_profile_allocation
+      union
+      select
+        distinct(upid) as upid,
+        true as hasHeapProfiles
+      from heap_graph_object
+    ) using (upid)
+    left join (
+      select
+        thread.upid as upid,
+        sum(cnt) as perfSampleCount
+      from (
+          select utid, count(*) as cnt
+          from perf_sample where callsite_id is not null
+          group by utid
+      ) join thread using (utid)
+      group by thread.upid
+    ) using (upid)
+    left join (
+      select
+        process.upid as upid,
+        sum(cnt) as sliceCount
+      from (select track_id, count(*) as cnt from slice group by track_id)
+        left join thread_track on track_id = thread_track.id
+        left join thread on thread_track.utid = thread.utid
+        left join process_track on track_id = process_track.id
+        join process on process.upid = thread.upid
+          or process_track.upid = process.upid
+      where process.upid is not null
+      group by process.upid
+    ) using (upid)
+    left join thread using(utid)
+    left join process using(upid)
+    left join package_list using(uid)
+    order by
+      chromeProcessRank desc,
+      hasHeapProfiles desc,
+      perfSampleCount desc,
+      total_dur desc,
+      sliceCount desc,
+      processName asc nulls last,
+      the_tracks.upid asc nulls last,
+      threadName asc nulls last,
+      the_tracks.utid asc nulls last;
+  `);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      pid: NUM_NULL,
+      threadName: STR_NULL,
+      processName: STR_NULL,
+      hasSched: NUM_NULL,
+      hasHeapProfiles: NUM_NULL,
+      isDebuggable: NUM_NULL,
+      chromeProcessLabels: STR,
+    });
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const tid = it.tid;
+      const upid = it.upid;
+      const pid = it.pid;
+      const hasSched = !!it.hasSched;
+      const isDebuggable = !!it.isDebuggable;
+
+      // Group by upid if present else by utid.
+      let pUuid =
+          upid === null ? this.utidToUuid.get(utid) : this.upidToUuid.get(upid);
+      // These should only happen once for each track group.
+      if (pUuid === undefined) {
+        pUuid = this.getOrCreateUuid(utid, upid);
+        const pidForColor = pid || tid || upid || utid || 0;
+        const type = hasSched ? 'schedule' : 'summary';
+        const uri = `perfetto.ProcessScheduling#${utid}.${type}`;
+
+        if (hasSched) {
+          const config: ProcessSchedulingTrackConfig = {
+            pidForColor,
+            upid,
+            utid,
+          };
+
+          ctx.addTrack({
+            uri,
+            displayName: `${upid === null ? tid : pid} schedule`,
+            kind: PROCESS_SCHEDULING_TRACK_KIND,
+            tags: {
+              isDebuggable,
+            },
+            trackFactory: ({trackInstanceId}) => {
+              return new TrackWithControllerAdapter<
+                  ProcessSchedulingTrackConfig,
+                  ProcessSchedulingTrackData>(
+                  ctx.engine,
+                  trackInstanceId,
+                  config,
+                  ProcessSchedulingTrack,
+                  ProcessSchedulingTrackController);
+            },
+          });
+        } else {
+          const config: ProcessSummaryTrackConfig = {
+            pidForColor,
+            upid,
+            utid,
+          };
+
+          ctx.addTrack({
+            uri,
+            displayName: `${upid === null ? tid : pid} summary`,
+            kind: PROCESS_SUMMARY_TRACK,
+            tags: {
+              isDebuggable,
+            },
+            trackFactory: ({trackInstanceId}) => {
+              return new TrackWithControllerAdapter<
+                  ProcessSummaryTrackConfig,
+                  ProcessSummaryTrackData>(
+                  ctx.engine,
+                  trackInstanceId,
+                  config,
+                  ProcessSummaryTrack,
+                  ProcessSummaryTrackController);
+            },
+          });
         }
       }
-
-      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;
-    const windowStart = Time.quant(start, bucketSize);
-    const windowDur = BigintMath.max(1n, end - windowStart);
-
-    await this.query(`update ${this.tableName('window')} set
-      window_start=${windowStart},
-      window_dur=${windowDur},
-      quantum=${bucketSize}
-      where rowid = 0;`);
-
-    return this.computeSummary(windowStart, end, resolution, bucketSize);
   }
 
-  private async computeSummary(
-      start: time, end: time, resolution: duration,
-      bucketSize: duration): Promise<Data> {
-    const duration = end - start;
-    const numBuckets = Math.min(Number(duration / bucketSize), LIMIT);
+  private async addKernelThreadSummary(ctx: TracePluginContext): Promise<void> {
+    const {engine} = ctx;
 
-    const query = `select
-      quantum_ts as bucket,
-      sum(dur)/cast(${bucketSize} as float) as utilization
-      from ${this.tableName('span')}
-      group by quantum_ts
-      limit ${LIMIT}`;
+    // Identify kernel threads if this is a linux system trace, and sufficient
+    // process information is available. Kernel threads are identified by being
+    // children of kthreadd (always pid 2).
+    // The query will return the kthreadd process row first, which must exist
+    // for any other kthreads to be returned by the query.
+    // TODO(rsavitski): figure out how to handle the idle process (swapper),
+    // which has pid 0 but appears as a distinct process (with its own comm) on
+    // each cpu. It'd make sense to exclude its thread state track, but still
+    // put process-scoped tracks in this group.
+    const result = await engine.query(`
+      select
+        t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
+      from
+        thread t
+        join process p using (upid)
+        left join process parent on (p.parent_upid = parent.upid)
+        join
+          (select true from metadata m
+             where (m.name = 'system_name' and m.str_value = 'Linux')
+           union
+           select 1 from (select true from sched limit 1))
+      where
+        p.pid = 2 or parent.pid = 2
+      order by isKthreadd desc
+    `);
 
-    const summary: Data = {
-      start,
-      end,
-      resolution,
-      length: numBuckets,
-      bucketSize,
-      utilizations: new Float64Array(numBuckets),
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM,
+    });
+
+    // Not applying kernel thread grouping.
+    if (!it.valid()) {
+      return;
+    }
+
+    const config: ProcessSummaryTrackConfig = {
+      pidForColor: 2,
+      upid: it.upid,
+      utid: it.utid,
     };
 
-    const queryRes = await this.query(query);
-    const it = queryRes.iter({bucket: NUM, utilization: NUM});
-    for (; it.valid(); it.next()) {
-      const bucket = it.bucket;
-      if (bucket > numBuckets) {
-        continue;
+    ctx.addTrack({
+      uri: 'perfetto.ProcessSummary#kernel',
+      displayName: `Kernel thread summary`,
+      kind: PROCESS_SUMMARY_TRACK,
+      trackFactory: ({trackInstanceId}) => {
+        return new TrackWithControllerAdapter<
+            ProcessSummaryTrackConfig,
+            ProcessSummaryTrackData>(
+            ctx.engine,
+            trackInstanceId,
+            config,
+            ProcessSummaryTrack,
+            ProcessSummaryTrackController);
+      },
+    });
+  }
+
+  private getOrCreateUuid(utid: number, upid: number|null) {
+    let uuid = this.getUuidUnchecked(utid, upid);
+    if (uuid === undefined) {
+      uuid = uuidv4();
+      if (upid === null) {
+        this.utidToUuid.set(utid, uuid);
+      } else {
+        this.upidToUuid.set(upid, uuid);
       }
-      summary.utilizations[bucket] = it.utilization;
     }
-
-    return summary;
+    return uuid;
   }
 
-  onDestroy(): void {
-    if (this.setup) {
-      this.query(`drop table ${this.tableName('window')}`);
-      this.query(`drop table ${this.tableName('span')}`);
-      this.setup = false;
-    }
-  }
-}
-
-const MARGIN_TOP = 5;
-const RECT_HEIGHT = 30;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
-
-class ProcessSummaryTrack extends Track<Config, Data> {
-  static readonly kind = PROCESS_SUMMARY_TRACK;
-  static create(args: NewTrackArgs): ProcessSummaryTrack {
-    return new ProcessSummaryTrack(args);
-  }
-
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D): void {
-    const {
-      visibleTimeScale,
-      windowSpan,
-    } = globals.frontendLocalState;
-    const data = this.data();
-    if (data === undefined) return;  // Can't possibly draw anything.
-
-    checkerboardExcept(
-        ctx,
-        this.getHeight(),
-        windowSpan.start,
-        windowSpan.end,
-        visibleTimeScale.timeToPx(data.start),
-        visibleTimeScale.timeToPx(data.end));
-
-    this.renderSummary(ctx, data);
-  }
-
-  // TODO(dproy): Dedup with CPU slices.
-  renderSummary(ctx: CanvasRenderingContext2D, data: Data): void {
-    const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
-    const startPx = windowSpan.start;
-    const bottomY = TRACK_HEIGHT;
-
-    let lastX = startPx;
-    let lastY = bottomY;
-
-    // TODO(hjd): Dedupe this math.
-    const color = colorForTid(this.config.pidForColor);
-    color.l = Math.min(color.l + 10, 60);
-    color.s -= 20;
-
-    ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
-    ctx.beginPath();
-    ctx.moveTo(lastX, lastY);
-    for (let i = 0; i < data.utilizations.length; i++) {
-      // TODO(dproy): Investigate why utilization is > 1 sometimes.
-      const utilization = Math.min(data.utilizations[i], 1);
-      const startTime = Time.fromRaw(BigInt(i) * data.bucketSize + data.start);
-
-      lastX = Math.floor(visibleTimeScale.timeToPx(startTime));
-
-      ctx.lineTo(lastX, lastY);
-      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
-      ctx.lineTo(lastX, lastY);
-    }
-    ctx.lineTo(lastX, bottomY);
-    ctx.closePath();
-    ctx.fill();
-  }
-}
-
-class ProcessSummaryPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrack(ProcessSummaryTrack);
-    ctx.registerTrackController(ProcessSummaryTrackController);
+  getUuidUnchecked(utid: number, upid: number|null) {
+    return upid === null ? this.utidToUuid.get(utid) :
+                           this.upidToUuid.get(upid);
   }
 }
 
diff --git a/ui/src/tracks/process_scheduling/index.ts b/ui/src/tracks/process_summary/process_scheduling_track.ts
similarity index 91%
rename from ui/src/tracks/process_scheduling/index.ts
rename to ui/src/tracks/process_summary/process_scheduling_track.ts
index 631998c..8759260 100644
--- a/ui/src/tracks/process_scheduling/index.ts
+++ b/ui/src/tracks/process_summary/process_scheduling_track.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 The Android Open Source Project
+// 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.
@@ -20,16 +20,26 @@
 import {calcCachedBucketSize} from '../../common/cache_utils';
 import {drawTrackHoverTooltip} from '../../common/canvas_utils';
 import {colorForThread} from '../../common/colorizer';
-import {LONG, NUM, QueryResult} from '../../common/query_result';
+import {
+  LONG,
+  NUM,
+  QueryResult,
+} from '../../common/query_result';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+} 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';
 
 export const PROCESS_SCHEDULING_TRACK_KIND = 'ProcessSchedulingTrack';
 
+const MARGIN_TOP = 5;
+const RECT_HEIGHT = 30;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
 export interface Data extends TrackData {
   kind: 'slice';
   maxCpu: number;
@@ -49,9 +59,8 @@
 
 // This summary is displayed for any processes that have CPU scheduling activity
 // associated with them.
-class ProcessSchedulingTrackController extends TrackController<Config, Data> {
-  static readonly kind = PROCESS_SCHEDULING_TRACK_KIND;
-
+export class ProcessSchedulingTrackController extends
+    TrackControllerAdapter<Config, Data> {
   private maxCpu = 0;
   private maxDur = 0n;
   private cachedBucketSize = BIMath.INT64_MAX;
@@ -178,12 +187,7 @@
   }
 }
 
-const MARGIN_TOP = 5;
-const RECT_HEIGHT = 30;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-class ProcessSchedulingTrack extends Track<Config, Data> {
-  static readonly kind = PROCESS_SCHEDULING_TRACK_KIND;
+export class ProcessSchedulingTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): ProcessSchedulingTrack {
     return new ProcessSchedulingTrack(args);
   }
@@ -312,15 +316,3 @@
     this.mousePos = undefined;
   }
 }
-
-class ProcessSchedulingPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.registerTrackController(ProcessSchedulingTrackController);
-    ctx.registerTrack(ProcessSchedulingTrack);
-  }
-}
-
-export const plugin: PluginInfo = {
-  pluginId: 'perfetto.ProcessScheduling',
-  plugin: ProcessSchedulingPlugin,
-};
diff --git a/ui/src/tracks/process_summary/process_summary_track.ts b/ui/src/tracks/process_summary/process_summary_track.ts
new file mode 100644
index 0000000..eb62667
--- /dev/null
+++ b/ui/src/tracks/process_summary/process_summary_track.ts
@@ -0,0 +1,213 @@
+// 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.
+
+import {BigintMath} from '../../base/bigint_math';
+import {assertFalse} from '../../base/logging';
+import {duration, Time, time} from '../../base/time';
+import {colorForTid} from '../../common/colorizer';
+import {NUM} from '../../common/query_result';
+import {TrackAdapter, TrackControllerAdapter} from '../../common/track_adapter';
+import {LIMIT, TrackData} from '../../common/track_data';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {NewTrackArgs} from '../../frontend/track';
+
+export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
+
+// TODO(dproy): Consider deduping with CPU summary data.
+export interface Data extends TrackData {
+  bucketSize: duration;
+  utilizations: Float64Array;
+}
+
+export interface Config {
+  pidForColor: number;
+  upid: number|null;
+  utid: number;
+}
+
+// This is the summary displayed when a process only contains chrome slices
+// and no cpu scheduling.
+export class ProcessSummaryTrackController extends
+    TrackControllerAdapter<Config, Data> {
+  private setup = false;
+
+  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;
+    const windowStart = Time.quant(start, bucketSize);
+    const windowDur = BigintMath.max(1n, end - windowStart);
+
+    await this.query(`update ${this.tableName('window')} set
+      window_start=${windowStart},
+      window_dur=${windowDur},
+      quantum=${bucketSize}
+      where rowid = 0;`);
+
+    return this.computeSummary(windowStart, end, resolution, bucketSize);
+  }
+
+  private async computeSummary(
+      start: time, end: time, resolution: duration,
+      bucketSize: duration): Promise<Data> {
+    const duration = end - start;
+    const numBuckets = Math.min(Number(duration / bucketSize), LIMIT);
+
+    const query = `select
+      quantum_ts as bucket,
+      sum(dur)/cast(${bucketSize} as float) as utilization
+      from ${this.tableName('span')}
+      group by quantum_ts
+      limit ${LIMIT}`;
+
+    const summary: Data = {
+      start,
+      end,
+      resolution,
+      length: numBuckets,
+      bucketSize,
+      utilizations: new Float64Array(numBuckets),
+    };
+
+    const queryRes = await this.query(query);
+    const it = queryRes.iter({bucket: NUM, utilization: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
+      if (bucket > numBuckets) {
+        continue;
+      }
+      summary.utilizations[bucket] = it.utilization;
+    }
+
+    return summary;
+  }
+
+  onDestroy(): void {
+    if (this.setup) {
+      this.query(`drop table ${this.tableName('window')}`);
+      this.query(`drop table ${this.tableName('span')}`);
+      this.setup = false;
+    }
+  }
+}
+
+const MARGIN_TOP = 5;
+const RECT_HEIGHT = 30;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+const SUMMARY_HEIGHT = TRACK_HEIGHT - MARGIN_TOP;
+
+export class ProcessSummaryTrack extends TrackAdapter<Config, Data> {
+  static create(args: NewTrackArgs): ProcessSummaryTrack {
+    return new ProcessSummaryTrack(args);
+  }
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D): void {
+    const {
+      visibleTimeScale,
+      windowSpan,
+    } = globals.frontendLocalState;
+    const data = this.data();
+    if (data === undefined) return;  // Can't possibly draw anything.
+
+    checkerboardExcept(
+        ctx,
+        this.getHeight(),
+        windowSpan.start,
+        windowSpan.end,
+        visibleTimeScale.timeToPx(data.start),
+        visibleTimeScale.timeToPx(data.end));
+
+    this.renderSummary(ctx, data);
+  }
+
+  // TODO(dproy): Dedup with CPU slices.
+  renderSummary(ctx: CanvasRenderingContext2D, data: Data): void {
+    const {visibleTimeScale, windowSpan} = globals.frontendLocalState;
+    const startPx = windowSpan.start;
+    const bottomY = TRACK_HEIGHT;
+
+    let lastX = startPx;
+    let lastY = bottomY;
+
+    // TODO(hjd): Dedupe this math.
+    const color = colorForTid(this.config.pidForColor);
+    color.l = Math.min(color.l + 10, 60);
+    color.s -= 20;
+
+    ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
+    ctx.beginPath();
+    ctx.moveTo(lastX, lastY);
+    for (let i = 0; i < data.utilizations.length; i++) {
+      // TODO(dproy): Investigate why utilization is > 1 sometimes.
+      const utilization = Math.min(data.utilizations[i], 1);
+      const startTime = Time.fromRaw(BigInt(i) * data.bucketSize + data.start);
+
+      lastX = Math.floor(visibleTimeScale.timeToPx(startTime));
+
+      ctx.lineTo(lastX, lastY);
+      lastY = MARGIN_TOP + Math.round(SUMMARY_HEIGHT * (1 - utilization));
+      ctx.lineTo(lastX, lastY);
+    }
+    ctx.lineTo(lastX, bottomY);
+    ctx.closePath();
+    ctx.fill();
+  }
+}