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);