[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();
+ }
+}