Merge "[CUI] Add Chrome startups to the CUI plugin." into main am: b20762ff88
Original change: https://android-review.googlesource.com/c/platform/external/perfetto/+/2828891
Change-Id: I41b47d474e994ee7877b2f6a11f66b9f64f60891
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/ui/src/tracks/chrome_critical_user_interactions/index.ts b/ui/src/tracks/chrome_critical_user_interactions/index.ts
index 2e0a381..8ddb3d4 100644
--- a/ui/src/tracks/chrome_critical_user_interactions/index.ts
+++ b/ui/src/tracks/chrome_critical_user_interactions/index.ts
@@ -24,6 +24,7 @@
NamedSliceTrackTypes,
} from '../../frontend/named_slice_track';
import {
+ NUM,
Plugin,
PluginContext,
PluginContextTrace,
@@ -40,17 +41,20 @@
} from '../custom_sql_table_slices';
import {PageLoadDetailsPanel} from './page_load_details_panel';
+import {StartupDetailsPanel} from './startup_details_panel';
export const CRITICAL_USER_INTERACTIONS_KIND =
'org.chromium.CriticalUserInteraction.track';
export const CRITICAL_USER_INTERACTIONS_ROW = {
...NAMED_ROW,
+ scopedId: NUM,
type: STR,
};
export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
export interface CriticalUserInteractionSlice extends Slice {
+ scopedId: number;
type: string;
}
@@ -63,6 +67,7 @@
enum CriticalUserInteractionType {
UNKNOWN = 'Unknown',
PAGE_LOAD = 'chrome_page_loads',
+ STARTUP = 'chrome_startups',
}
function convertToCriticalUserInteractionType(cujType: string):
@@ -70,6 +75,8 @@
switch (cujType) {
case CriticalUserInteractionType.PAGE_LOAD:
return CriticalUserInteractionType.PAGE_LOAD;
+ case CriticalUserInteractionType.STARTUP:
+ return CriticalUserInteractionType.STARTUP;
default:
return CriticalUserInteractionType.UNKNOWN;
}
@@ -81,7 +88,17 @@
getSqlDataSource(): CustomSqlTableDefConfig {
return {
- columns: ['scoped_id AS id', 'name', 'ts', 'dur', 'type'],
+ columns: [
+ // The scoped_id is not a unique identifier within the table; generate
+ // a unique id from type and scoped_id on the fly to use for slice
+ // selection.
+ 'hash(type, scoped_id) AS id',
+ 'scoped_id AS scopedId',
+ 'name',
+ 'ts',
+ 'dur',
+ 'type',
+ ],
sqlTableName: 'chrome_interactions',
};
}
@@ -107,12 +124,37 @@
},
};
break;
+ case CriticalUserInteractionType.STARTUP:
+ detailsPanel = {
+ kind: StartupDetailsPanel.kind,
+ config: {
+ sqlTableName: this.tableName,
+ title: 'Chrome Startup',
+ },
+ };
+ break;
default:
break;
}
return detailsPanel;
}
+ onSliceClick(
+ args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>) {
+ const detailsPanelConfig = this.getDetailsPanel(args);
+ globals.makeSelection(Actions.selectGenericSlice({
+ id: args.slice.scopedId,
+ sqlTableName: this.tableName,
+ start: args.slice.ts,
+ duration: args.slice.dur,
+ trackKey: this.trackKey,
+ detailsPanelConfig: {
+ kind: detailsPanelConfig.kind,
+ config: detailsPanelConfig.config,
+ },
+ }));
+ }
+
getSqlImports(): CustomSqlImportConfig {
return {
modules: ['chrome.interactions'],
@@ -126,8 +168,9 @@
rowToSlice(row: CriticalUserInteractionSliceTrackTypes['row']):
CriticalUserInteractionSliceTrackTypes['slice'] {
const baseSlice = super.rowToSlice(row);
+ const scopedId = row.scopedId;
const type = row.type;
- return {...baseSlice, type};
+ return {...baseSlice, scopedId, type};
}
}
diff --git a/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts b/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
new file mode 100644
index 0000000..dbcde70
--- /dev/null
+++ b/ui/src/tracks/chrome_critical_user_interactions/startup_details_panel.ts
@@ -0,0 +1,147 @@
+// 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 m from 'mithril';
+
+import {duration, Time, time} from '../../base/time';
+import {
+ BottomTab,
+ bottomTabRegistry,
+ NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {
+ GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {DurationWidget} from '../../frontend/widgets/duration';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
+import {DetailsShell} from '../../widgets/details_shell';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {dictToTreeNodes, Tree} from '../../widgets/tree';
+import {asUpid, Upid} from '../../frontend/sql_types';
+
+interface Data {
+ startupId: number;
+ eventName: string;
+ startupBeginTs: time;
+ durToFirstVisibleContent: duration;
+ launchCause?: string;
+ upid: Upid;
+}
+
+export class StartupDetailsPanel extends
+ BottomTab<GenericSliceDetailsTabConfig> {
+ static readonly kind = 'org.perfetto.StartupDetailsPanel';
+ private loaded = false;
+ private data: Data|undefined;
+
+ static create(args: NewBottomTabArgs): StartupDetailsPanel {
+ return new StartupDetailsPanel(args);
+ }
+
+ constructor(args: NewBottomTabArgs) {
+ super(args);
+ this.loadData();
+ }
+
+ private async loadData() {
+ const queryResult = await this.engine.query(`
+ SELECT
+ activity_id AS startupId,
+ name,
+ startup_begin_ts AS startupBeginTs,
+ CASE
+ WHEN first_visible_content_ts IS NULL THEN 0
+ ELSE first_visible_content_ts - startup_begin_ts
+ END AS durTofirstVisibleContent,
+ launch_cause AS launchCause,
+ browser_upid AS upid
+ FROM chrome_startups
+ WHERE id = ${this.config.id};
+ `);
+
+ const iter = queryResult.firstRow({
+ startupId: NUM,
+ name: STR,
+ startupBeginTs: LONG,
+ durTofirstVisibleContent: LONG,
+ launchCause: STR_NULL,
+ upid: NUM,
+ });
+
+ this.data = {
+ startupId: iter.startupId,
+ eventName: iter.name,
+ startupBeginTs: Time.fromRaw(iter.startupBeginTs),
+ durToFirstVisibleContent: iter.durTofirstVisibleContent,
+ upid: asUpid(iter.upid),
+ };
+
+ if (iter.launchCause) {
+ this.data.launchCause = iter.launchCause;
+ }
+
+ this.loaded = true;
+ }
+
+ private getDetailsDictionary() {
+ const details: {[key: string]: m.Child} = {};
+ if (this.data === undefined) return details;
+ details['Activity ID'] = this.data.startupId;
+ details['Browser Upid'] = this.data.upid;
+ details['Startup Event'] = this.data.eventName;
+ details['Startup Timestamp'] = m(Timestamp, {ts: this.data.startupBeginTs});
+ details['Duration to First Visible Content'] =
+ m(DurationWidget, {dur: this.data.durToFirstVisibleContent});
+ if (this.data.launchCause) {
+ details['Launch Cause'] = this.data.launchCause;
+ }
+ details['SQL ID'] =
+ m(SqlRef, {table: 'chrome_startups', id: this.config.id});
+ return details;
+ }
+
+ viewTab() {
+ if (this.isLoading()) {
+ return m('h2', 'Loading');
+ }
+
+ return m(
+ DetailsShell,
+ {
+ title: this.getTitle(),
+ },
+ m(GridLayout,
+ m(
+ GridLayoutColumn,
+ m(
+ Section,
+ {title: 'Details'},
+ m(Tree, dictToTreeNodes(this.getDetailsDictionary())),
+ ),
+ )));
+ }
+
+ getTitle(): string {
+ return this.config.title;
+ }
+
+ isLoading() {
+ return !this.loaded;
+ }
+}
+
+bottomTabRegistry.register(StartupDetailsPanel);