[CUI] Add SQL model and custom track for CUI page loads.

* Introduce two new data models
  - chrome_page_loads - stores fcp/lcp stats
  - chrome_critical_user_actions - stores all CUIs in a
    single table.
* Adds new diff tests for the new data model (fcl/lcp only)
  This will expand as more CUI metrics are added.
* Add a new CUI track with details panel to the UI, hidden
  behind a flag. This track summarizes the navigation duration
  based on fcp and lcp timings only.

https://screenshot.googleplex.com/C6BLruh9P6vkCPz
https://screenshot.googleplex.com/4WTpwsDhrd9ZNJr

Bug: b/303579550
Change-Id: I38bc341f2f5306060366b71dd23c8820fa805b65
diff --git a/Android.bp b/Android.bp
index bfa3740..1b8103f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11864,7 +11864,9 @@
         "src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/histograms.sql",
+        "src/trace_processor/perfetto_sql/stdlib/chrome/interactions.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/metadata.sql",
+        "src/trace_processor/perfetto_sql/stdlib/chrome/page_loads.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_intervals.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_v3.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/scroll_jank/scroll_jank_v3_cause.sql",
diff --git a/BUILD b/BUILD
index 1ba49a5..1315915 100644
--- a/BUILD
+++ b/BUILD
@@ -2242,7 +2242,9 @@
         "src/trace_processor/perfetto_sql/stdlib/chrome/chrome_scrolls.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/cpu_powerups.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/histograms.sql",
+        "src/trace_processor/perfetto_sql/stdlib/chrome/interactions.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/metadata.sql",
+        "src/trace_processor/perfetto_sql/stdlib/chrome/page_loads.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/speedometer.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/tasks.sql",
         "src/trace_processor/perfetto_sql/stdlib/chrome/vsync_intervals.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/chrome/BUILD.gn
index cac258f..04a1571 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/BUILD.gn
@@ -20,7 +20,9 @@
     "chrome_scrolls.sql",
     "cpu_powerups.sql",
     "histograms.sql",
+    "interactions.sql",
     "metadata.sql",
+    "page_loads.sql",
     "speedometer.sql",
     "tasks.sql",
     "vsync_intervals.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/interactions.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/interactions.sql
new file mode 100644
index 0000000..49ce0c8
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/interactions.sql
@@ -0,0 +1,46 @@
+-- Copyright 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
+--
+--     https://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.
+
+-- This file specifies common metrics/tables for critical user interactions. It
+-- is expected to be in flux as metrics are added across different CUI types.
+-- Currently we only track Chrome page loads and their associated metrics.
+
+INCLUDE PERFETTO MODULE chrome.page_loads;
+
+-- All critical user interaction events, including type and table with
+-- associated metrics.
+--
+-- @column scoped_id                 Identifier of the interaction; this is not
+--                                   guaranteed to be unique to the table -
+--                                   rather, it is unique within an individual
+--                                   interaction type. Combine with type to get
+--                                   a unique identifier in this table.
+-- @column type                      Type of this interaction, which together
+--                                   with scoped_id uniquely identifies this
+--                                   interaction. Also corresponds to a SQL
+--                                   table name containing more details specific
+--                                   to this type of interaction.
+-- @column name                      Interaction name - e.g. 'PageLoad', 'Tap',
+--                                   etc. Interactions will have unique metrics
+--                                   stored in other tables.
+-- @column ts                        Timestamp of the CUI event.
+-- @column dur                       Duration of the CUI event.
+CREATE PERFETTO TABLE chrome_interactions AS
+SELECT
+  navigation_id AS scoped_id,
+  'chrome_page_loads' AS type,
+  'PageLoad' AS name,
+  navigation_start_ts AS ts,
+  IFNULL(lcp, fcp) AS dur
+FROM chrome_page_loads;
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/page_loads.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/page_loads.sql
new file mode 100644
index 0000000..fcc5874
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/page_loads.sql
@@ -0,0 +1,73 @@
+-- Copyright 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
+--
+--     https://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.
+
+-- TODO(b/306300843): The recorded navigation ids are not guaranteed to be
+-- unique within a trace; they are only guaranteed to be unique within a single
+-- chrome instance. Chrome instance id needs to be recorded, and used here in
+-- combination with navigation id to uniquely identify page load metrics.
+
+INCLUDE PERFETTO MODULE common.slices;
+
+-- Chrome page loads, including associated high-level metrics and properties.
+--
+-- @column navigation_id             ID of the navigation associated with the
+--                                   page load (i.e. the cross-document
+--                                   navigation in primary main frame which
+--                                   created this page's main document). Also
+--                                   note that navigation_id is specific to a
+--                                   given Chrome browser process, and not
+--                                   globally unique.
+-- @column navigation_start_ts       Timestamp of the start of navigation.
+-- @column fcp                       Duration between the navigation start and
+--                                   the first contentful paint event
+--                                   (web.dev/fcp).
+-- @column fcp_ts                    Timestamp of the first contentful paint.
+-- @column lcp                       Duration between the navigation start and
+--                                   the largest contentful paint event
+--                                   (web.dev/lcp).
+-- @column lcp_ts                    Timestamp of the largest contentful paint.
+-- @column url                       URL at the page load event.
+-- @column browser_upid              The unique process id (upid) of the browser
+--                                   process where the page load occurred.
+CREATE PERFETTO TABLE chrome_page_loads AS
+WITH fcp AS (
+  SELECT
+    ts,
+    dur,
+    EXTRACT_ARG(arg_set_id, 'page_load.navigation_id') AS navigation_id,
+    EXTRACT_ARG(arg_set_id, 'page_load.url') AS url,
+    upid AS browser_upid
+  FROM process_slice
+  WHERE name = 'PageLoadMetrics.NavigationToFirstContentfulPaint'
+),
+lcp AS (
+  SELECT
+    ts,
+    dur,
+    EXTRACT_ARG(arg_set_id, 'page_load.navigation_id')
+      AS navigation_id
+  FROM slice
+  WHERE name = 'PageLoadMetrics.NavigationToLargestContentfulPaint'
+)
+SELECT
+ fcp.navigation_id,
+ fcp.ts AS navigation_start_ts,
+ fcp.dur AS fcp,
+ fcp.ts + fcp.dur AS fcp_ts,
+ lcp.dur AS lcp,
+ IFNULL(lcp.dur, 0) + IFNULL(lcp.ts, 0) AS lcp_ts,
+ fcp.url,
+ fcp.browser_upid
+FROM fcp
+LEFT JOIN lcp USING (navigation_id);
diff --git a/test/data/chrome_fcp_lcp_navigations.pftrace.sha256 b/test/data/chrome_fcp_lcp_navigations.pftrace.sha256
new file mode 100644
index 0000000..e4274e2
--- /dev/null
+++ b/test/data/chrome_fcp_lcp_navigations.pftrace.sha256
@@ -0,0 +1 @@
+ae01d849fbd75a98be1b7ddd5a8873217c377b393a1d5bbd788ed3364f7fefc3
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 0918d10..d8669d6 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -88,6 +88,7 @@
 from diff_tests.parser.ufs.tests import Ufs
 from diff_tests.stdlib.android.tests import AndroidStdlib
 from diff_tests.stdlib.chrome.tests import ChromeStdlib
+from diff_tests.stdlib.chrome.tests_chrome_interactions import ChromeInteractions
 from diff_tests.stdlib.chrome.tests_scroll_jank import ChromeScrollJankStdlib
 from diff_tests.stdlib.dynamic_tables.tests import DynamicTables
 from diff_tests.stdlib.pkvm.tests import Pkvm
@@ -202,6 +203,8 @@
 
   stdlib_tests = [
       *AndroidStdlib(index_path, 'stdlib/android', 'AndroidStdlib').fetch(),
+      *ChromeInteractions(index_path, 'stdlib/chrome',
+                                      'ChromeInteractions').fetch(),
       *ChromeScrollJankStdlib(index_path, 'stdlib/chrome',
                               'ChromeScrollJankStdlib').fetch(),
       *ChromeStdlib(index_path, 'stdlib/chrome', 'ChromeStdlib').fetch(),
diff --git a/test/trace_processor/diff_tests/stdlib/chrome/tests_chrome_interactions.py b/test/trace_processor/diff_tests/stdlib/chrome/tests_chrome_interactions.py
new file mode 100644
index 0000000..5019e54
--- /dev/null
+++ b/test/trace_processor/diff_tests/stdlib/chrome/tests_chrome_interactions.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# 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 a
+#
+#      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.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ChromeInteractions(TestSuite):
+  def test_chrome_fcp_lcp_navigations(self):
+    return DiffTestBlueprint(
+        trace=DataPath('chrome_fcp_lcp_navigations.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE chrome.page_loads;
+
+        SELECT
+          navigation_id,
+          navigation_start_ts,
+          fcp,
+          fcp_ts,
+          lcp,
+          lcp_ts,
+          browser_upid
+        FROM chrome_page_loads
+        ORDER by navigation_start_ts;
+        """,
+        out=Csv("""
+        "navigation_id","navigation_start_ts","fcp","fcp_ts","lcp","lcp_ts","browser_upid"
+        6,687425601436243,950000000,687426551436243,950000000,687426551436243,1
+        7,687427799068243,888000000,687428687068243,888000000,687428687068243,1
+        8,687429970749243,1031000000,687431001749243,1132000000,687431102749243,1
+        9,687432344113243,539000000,687432883113243,539000000,687432883113243,1
+        10,687434796215243,475000000,687435271215243,475000000,687435271215243,1
+        11,687435970742243,763000000,687436733742243,852000000,687436822742243,1
+        13,687438343638243,1005000000,687439348638243,1005000000,687439348638243,1
+        14,687440258111243,900000000,687441158111243,"[NULL]",0,1
+        """))
diff --git a/ui/src/tracks/chrome_critical_user_interactions/details_panel.ts b/ui/src/tracks/chrome_critical_user_interactions/details_panel.ts
new file mode 100644
index 0000000..9eea78f
--- /dev/null
+++ b/ui/src/tracks/chrome_critical_user_interactions/details_panel.ts
@@ -0,0 +1,236 @@
+// 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 {exists} from '../../base/utils';
+import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
+import {raf} from '../../core/raf_scheduler';
+import {
+  BottomTab,
+  bottomTabRegistry,
+  NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {
+  GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {sqlValueToString} from '../../frontend/sql_utils';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Anchor} from '../../widgets/anchor';
+import {DetailsShell} from '../../widgets/details_shell';
+import {DurationWidget} from '../../widgets/duration';
+import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {Section} from '../../widgets/section';
+import {SqlRef} from '../../widgets/sql_ref';
+import {dictToTreeNodes, Tree} from '../../widgets/tree';
+
+interface PageLoadMetrics {
+  url: string;
+  navigationId: number;
+  fcpDuration?: duration;
+  lcpDuration?: duration;
+  fcpTs: time, lcpTs?: time,
+}
+
+enum CriticalUserJourneyType {
+  UNKNOWN = 'Unknown',
+  PAGE_LOAD = 'PageLoad',
+}
+
+function convertToCriticalUserJourneyType(cujType: string):
+    CriticalUserJourneyType {
+  switch (cujType) {
+    case CriticalUserJourneyType.PAGE_LOAD:
+      return CriticalUserJourneyType.PAGE_LOAD;
+    default:
+      return CriticalUserJourneyType.UNKNOWN;
+  }
+}
+
+interface Data {
+  name: string;
+  // Timestamp of the beginning of this slice in nanoseconds.
+  ts: time;
+  // Duration of this slice in nanoseconds.
+  dur: duration;
+  type: CriticalUserJourneyType;
+  tableName: string;
+  // Metrics for |type| = CriticalUserJourney.PAGE_LOAD
+  pageLoadMetrics?: PageLoadMetrics;
+}
+
+export class CriticalUserInteractionDetailsPanel extends
+    BottomTab<GenericSliceDetailsTabConfig> {
+  static readonly kind = 'org.perfetto.CriticalUserInteractionDetailsPanel';
+  data: Data|undefined;
+  loaded = false;
+
+  static create(args: NewBottomTabArgs): CriticalUserInteractionDetailsPanel {
+    return new CriticalUserInteractionDetailsPanel(args);
+  }
+
+  constructor(args: NewBottomTabArgs) {
+    super(args);
+    this.loadData();
+  }
+
+  private async loadData() {
+    const queryResult = await this.engine.query(`
+      SELECT
+        name,
+        ts,
+        dur,
+        type AS tableName
+      FROM chrome_interactions
+      WHERE scoped_id = ${this.config.id}`);
+
+    const iter = queryResult.firstRow({
+      name: STR,
+      ts: LONG,
+      dur: LONG,
+      tableName: STR,
+    });
+
+    this.data = {
+      name: iter.name,
+      ts: Time.fromRaw(iter.ts),
+      dur: iter.dur,
+      type: convertToCriticalUserJourneyType(iter.name),
+      tableName: iter.tableName,
+    };
+
+    await this.loadMetrics();
+
+    this.loaded = true;
+    raf.scheduleFullRedraw();
+  }
+
+  private async loadMetrics() {
+    if (exists(this.data)) {
+      switch (this.data.type) {
+        case CriticalUserJourneyType.PAGE_LOAD:
+          await this.loadPageLoadMetrics();
+          break;
+        default:
+          break;
+      }
+    }
+  }
+
+  private async loadPageLoadMetrics() {
+    if (exists(this.data)) {
+      const queryResult = await this.engine.query(`
+      SELECT
+        navigation_id AS navigationId,
+        url,
+        fcp AS fcpDuration,
+        lcp AS lcpDuration,
+        fcp_ts AS fcpTs,
+        lcp_ts AS lcpTs
+      FROM chrome_page_loads
+      WHERE navigation_id = ${this.config.id}`);
+
+      const iter = queryResult.firstRow({
+        navigationId: NUM,
+        url: STR,
+        fcpDuration: LONG_NULL,
+        lcpDuration: LONG_NULL,
+        fcpTs: LONG,
+        lcpTs: LONG,
+      });
+
+      this.data.pageLoadMetrics = {
+        navigationId: iter.navigationId,
+        url: iter.url,
+        fcpTs: Time.fromRaw(iter.fcpTs),
+      };
+
+      if (exists(iter.fcpDuration)) {
+        this.data.pageLoadMetrics.fcpDuration = iter.fcpDuration;
+      }
+
+      if (exists(iter.lcpDuration)) {
+        this.data.pageLoadMetrics.lcpDuration = iter.lcpDuration;
+      }
+
+      if (Number(iter.lcpTs) != 0) {
+        this.data.pageLoadMetrics.lcpTs = Time.fromRaw(iter.lcpTs);
+      }
+    }
+  }
+
+  private renderDetailsDictionary(): m.Child[] {
+    const details: {[key: string]: m.Child} = {};
+    if (exists(this.data)) {
+      details['Name'] = sqlValueToString(this.data.name);
+      details['Timestamp'] = m(Timestamp, {ts: this.data.ts});
+      if (exists(this.data.pageLoadMetrics)) {
+        details['FCP Timestamp'] =
+            m(Timestamp, {ts: this.data.pageLoadMetrics.fcpTs});
+        if (exists(this.data.pageLoadMetrics.fcpDuration)) {
+          details['FCP Duration'] =
+              m(DurationWidget, {dur: this.data.pageLoadMetrics.fcpDuration});
+        }
+        if (exists(this.data.pageLoadMetrics.lcpTs)) {
+          details['LCP Timestamp'] =
+              m(Timestamp, {ts: this.data.pageLoadMetrics.lcpTs});
+        }
+        if (exists(this.data.pageLoadMetrics.lcpDuration)) {
+          details['LCP Duration'] =
+              m(DurationWidget, {dur: this.data.pageLoadMetrics.lcpDuration});
+        }
+        details['Navigation ID'] = this.data.pageLoadMetrics.navigationId;
+        const url = this.data.pageLoadMetrics.url;
+        details['URL'] =
+            m(Anchor, {href: url, target: '_blank', icon: 'open_in_new'}, url);
+      }
+      details['SQL ID'] =
+          m(SqlRef, {table: 'chrome_interactions', id: this.config.id});
+    }
+
+    return dictToTreeNodes(details);
+  }
+
+  viewTab() {
+    if (this.data === undefined) {
+      return m('h2', 'Loading');
+    }
+
+    return m(
+        DetailsShell,
+        {
+          title: this.getTitle(),
+        },
+        m(GridLayout,
+          m(
+              GridLayoutColumn,
+              m(
+                  Section,
+                  {title: 'Details'},
+                  m(Tree, this.renderDetailsDictionary()),
+                  ),
+              )));
+  }
+
+  getTitle(): string {
+    return this.config.title;
+  }
+
+  isLoading() {
+    return !this.loaded;
+  }
+}
+
+bottomTabRegistry.register(CriticalUserInteractionDetailsPanel);
diff --git a/ui/src/tracks/chrome_critical_user_interactions/index.ts b/ui/src/tracks/chrome_critical_user_interactions/index.ts
new file mode 100644
index 0000000..298368e
--- /dev/null
+++ b/ui/src/tracks/chrome_critical_user_interactions/index.ts
@@ -0,0 +1,110 @@
+// 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 {v4 as uuidv4} from 'uuid';
+
+import {Actions} from '../../common/actions';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
+import {globals} from '../../frontend/globals';
+import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
+import {NewTrackArgs, TrackBase} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+  PrimaryTrackSortKey,
+} from '../../public';
+import {
+  CustomSqlDetailsPanelConfig,
+  CustomSqlImportConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../custom_sql_table_slices';
+
+import {CriticalUserInteractionDetailsPanel} from './details_panel';
+
+export const CRITICAL_USER_INTERACTIONS_KIND =
+    'org.chromium.TopLevelScrolls.scrolls';
+
+export class CriticalUserInteractionTrack extends
+    CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
+  static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
+
+  static create(args: NewTrackArgs): TrackBase {
+    return new CriticalUserInteractionTrack(args);
+  }
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      columns: ['scoped_id AS id', 'name', 'ts', 'dur', 'type'],
+      sqlTableName: 'chrome_interactions',
+    };
+  }
+
+  getDetailsPanel(): CustomSqlDetailsPanelConfig {
+    return {
+      kind: CriticalUserInteractionDetailsPanel.kind,
+      config: {
+        sqlTableName: this.tableName,
+        title: 'Chrome Critical User Interaction',
+      },
+    };
+  }
+
+  getSqlImports(): CustomSqlImportConfig {
+    return {
+      modules: ['chrome.interactions'],
+    };
+  }
+}
+
+export function addCriticalUserInteractionTrack() {
+  const trackId = uuidv4();
+  globals.dispatchMultiple([
+    Actions.addTrack({
+      id: trackId,
+      uri: CriticalUserInteractionTrack.kind,
+      name: `Chrome Interactions`,
+      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+      trackGroup: SCROLLING_TRACK_GROUP,
+    }),
+    Actions.toggleTrackPinned({trackId}),
+  ]);
+}
+
+class CriticalUserInteractionPlugin implements Plugin {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    ctx.addTrack({
+      uri: CriticalUserInteractionTrack.kind,
+      kind: CriticalUserInteractionTrack.kind,
+      displayName: 'Chrome Interactions',
+      track: (trackCtx) => new CriticalUserInteractionTrack(
+          {engine: ctx.engine, trackId: trackCtx.trackInstanceId}),
+    });
+  }
+
+  onActivate(ctx: PluginContext): void {
+    ctx.addCommand({
+      id: 'perfetto.CriticalUserInteraction.AddInteractionTrack',
+      name: 'Add Chrome Interactions track',
+      callback: () => addCriticalUserInteractionTrack(),
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'perfetto.CriticalUserInteraction',
+  plugin: CriticalUserInteractionPlugin,
+};
diff --git a/ui/src/tracks/custom_sql_table_slices/index.ts b/ui/src/tracks/custom_sql_table_slices/index.ts
index 696bec8..3e63418 100644
--- a/ui/src/tracks/custom_sql_table_slices/index.ts
+++ b/ui/src/tracks/custom_sql_table_slices/index.ts
@@ -32,6 +32,10 @@
 import {NewTrackArgs} from '../../frontend/track';
 import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
+export interface CustomSqlImportConfig {
+  modules: string[];
+}
+
 export interface CustomSqlTableDefConfig {
   // Table name
   sqlTableName: string;
@@ -62,8 +66,14 @@
   // Override by subclasses.
   abstract getDetailsPanel(): CustomSqlDetailsPanelConfig;
 
+  getSqlImports(): CustomSqlImportConfig {
+    return {
+      modules: [] as string[],
+    };
+  }
 
   async onInit(): Promise<Disposable> {
+    await this.loadImports();
     const config = this.getSqlDataSource();
     let columns = ['*'];
     if (config.columns !== undefined) {
@@ -113,6 +123,12 @@
       },
     }));
   }
+
+  async loadImports() {
+    for (const importModule of this.getSqlImports().modules) {
+      await this.engine.query(`INCLUDE PERFETTO MODULE ${importModule};`);
+    }
+  }
 }
 
 class CustomSqlTrackPlugin implements Plugin {