Merge "Ignore the duration of dropped frames in android_frame_timeline_metric"
diff --git a/ui/src/controller/pivot_table_controller.ts b/ui/src/controller/pivot_table_controller.ts
index e91ec98..e8492df 100644
--- a/ui/src/controller/pivot_table_controller.ts
+++ b/ui/src/controller/pivot_table_controller.ts
@@ -197,7 +197,7 @@
     }
 
     // ES6 Set does not have .every method, only Array does.
-    for (const track in tracks) {
+    for (const track of tracks) {
       if (!this.lastQueryAreaTracks.has(track)) {
         return false;
       }
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_slice.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_slice.ts
new file mode 100644
index 0000000..046ef23
--- /dev/null
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_slice.ts
@@ -0,0 +1,189 @@
+// 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 {assertExists} from '../../base/logging';
+import {Actions} from '../../common/actions';
+import {EngineProxy} from '../../common/engine';
+import {LONG, NUM} from '../../common/query_result';
+import {TPDuration} from '../../common/time';
+import {Anchor} from '../../frontend/anchor';
+import {globals} from '../../frontend/globals';
+import {scrollToTrackAndTs} from '../../frontend/scroll_helper';
+import {Icons} from '../../frontend/semantic_icons';
+import {asTPTimestamp, SliceSqlId, TPTimestamp} from '../../frontend/sql_types';
+
+import {
+  EventLatencyTrack,
+} from './event_latency_track';
+import {ScrollJankPluginState} from './index';
+
+export interface EventLatencySlice {
+  // Chrome slice id for an EventLatency slice.
+  sliceId: SliceSqlId;
+  // Timestamp of the beginning of this slice in nanoseconds.
+  ts: TPTimestamp;
+  // Duration of this slice in nanoseconds.
+  dur: TPDuration;
+}
+
+export async function getEventLatencySlice(
+    engine: EngineProxy, id: number): Promise<EventLatencySlice|undefined> {
+  const eventLatencyTrack =
+      ScrollJankPluginState.getInstance().getTrack(EventLatencyTrack.kind);
+  if (eventLatencyTrack == undefined) {
+    throw new Error(`${EventLatencyTrack.kind} track is not registered.`);
+  }
+
+  const query = await engine.query(`
+    SELECT
+      id as sliceId,
+      ts,
+      dur as dur
+    FROM ${eventLatencyTrack.sqlTableName}
+    WHERE id=${id}`);
+  const it = query.iter({
+    sliceId: NUM,
+    ts: LONG,
+    dur: LONG,
+  });
+
+  const result: EventLatencySlice[] = [];
+
+  for (; it.valid(); it.next()) {
+    result.push({
+      sliceId: it.sliceId as SliceSqlId,
+      ts: asTPTimestamp(it.ts),
+      dur: it.dur,
+    });
+  }
+
+  if (result.length > 1) {
+    throw new Error(`${
+        eventLatencyTrack.sqlTableName} table has more than one row with id ${
+        id}`);
+  }
+  if (result.length === 0) {
+    return undefined;
+  }
+  return result[0];
+}
+
+export async function getEventLatencyDescendantSlice(
+    engine: EngineProxy, id: number, descendant: string|undefined):
+    Promise<EventLatencySlice|undefined> {
+  const query = await engine.query(`
+    SELECT
+      id as sliceId,
+      ts,
+      dur as dur
+    FROM descendant_slice(${id})
+    WHERE name='${descendant}'`);
+  const it = query.iter({
+    sliceId: NUM,
+    ts: LONG,
+    dur: LONG,
+  });
+
+  const result: EventLatencySlice[] = [];
+
+  for (; it.valid(); it.next()) {
+    result.push({
+      sliceId: it.sliceId as SliceSqlId,
+      ts: asTPTimestamp(it.ts),
+      dur: it.dur,
+    });
+  }
+
+  const eventLatencyTrack =
+      ScrollJankPluginState.getInstance().getTrack(EventLatencyTrack.kind);
+  if (eventLatencyTrack == undefined) {
+    throw new Error(`${EventLatencyTrack.kind} track is not registered.`);
+  }
+
+  if (result.length > 1) {
+    throw new Error(`
+        Slice table and track view ${
+        eventLatencyTrack
+            .sqlTableName} has more than one descendant of slice id ${
+        id} with name ${descendant}`);
+  }
+  if (result.length === 0) {
+    return undefined;
+  }
+  return result[0];
+}
+
+interface EventLatencySliceRefAttrs {
+  id: SliceSqlId;
+  ts: TPTimestamp;
+  // If not present, a placeholder name will be used.
+  name: string;
+  chromeSliceTrackId?: number;
+}
+
+export class EventLatencySliceRef implements
+    m.ClassComponent<EventLatencySliceRefAttrs> {
+  view(vnode: m.Vnode<EventLatencySliceRefAttrs>) {
+    return m(
+        Anchor,
+        {
+          icon: Icons.UpdateSelection,
+          onclick: () => {
+            const eventLatencyTrack =
+                ScrollJankPluginState.getInstance().getTrack(
+                    EventLatencyTrack.kind);
+            if (eventLatencyTrack == undefined) {
+              throw new Error(
+                  `${EventLatencyTrack.kind} track is not registered.`);
+            }
+
+            const trackIdx = vnode.attrs.chromeSliceTrackId as number;
+            assertExists(trackIdx);
+            const uiTrackId = globals.state.uiTrackIdByTraceTrackId[trackIdx];
+            if (uiTrackId === undefined) return;
+            globals.makeSelection(Actions.selectChromeSlice(
+                {id: vnode.attrs.id, trackId: uiTrackId, table: 'slice'}));
+
+            let trackId = '';
+            for (const track of Object.values(globals.state.tracks)) {
+              if (track.kind === EventLatencyTrack.kind) {
+                trackId = track.id;
+              }
+            }
+
+            if (trackId === '') {
+              throw new Error(
+                  `Track id for ${EventLatencyTrack.kind} track not found.`);
+            }
+
+            scrollToTrackAndTs(trackId, vnode.attrs.ts, true);
+          },
+        },
+        vnode.attrs.name,
+    );
+  }
+}
+
+export function eventLatencySlice(
+    state: EventLatencySlice, name: string, chromeSliceTrackId?: number):
+    m.Child {
+  return m(EventLatencySliceRef, {
+    id: state.sliceId,
+    ts: state.ts,
+    name: name,
+    chromeSliceTrackId: chromeSliceTrackId,
+  });
+}
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index c784527..7250819 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -19,12 +19,14 @@
   generateSqlWithInternalLayout,
 } from '../../common/internal_layout_utils';
 import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
+import {ChromeSliceDetailsTab} from '../../frontend/chrome_slice_details_tab';
 import {
   NamedSliceTrack,
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
 import {ScrollJankTracks as DecideTracksResult} from './index';
+import {ScrollJankPluginState} from './index';
 
 export interface EventLatencyTrackTypes extends NamedSliceTrackTypes {
   config: {baseTable: string;}
@@ -39,6 +41,23 @@
 
   constructor(args: NewTrackArgs) {
     super(args);
+    ScrollJankPluginState.getInstance().registerTrack({
+      kind: EventLatencyTrack.kind,
+      trackId: this.trackId,
+      tableName: this.tableName,
+      detailsPanelConfig: {
+        kind: ChromeSliceDetailsTab.kind,
+        config: {
+          title: 'Input Event Latency Slice',
+          sqlTableName: this.tableName,
+        },
+      },
+    });
+  }
+
+  onDestroy() {
+    super.onDestroy();
+    ScrollJankPluginState.getInstance().unregisterTrack(EventLatencyTrack.kind);
   }
 
   async initSqlTable(tableName: string) {
@@ -126,7 +145,7 @@
     engineId: engine.id,
     kind: EventLatencyTrack.kind,
     trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
-    name: 'Chrome Input Event Latencies',
+    name: 'Chrome Scroll Input Latencies',
     config: {baseTable: baseTable},
     trackGroup: SCROLLING_TRACK_GROUP,
   });
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index b6c816d..c72bb69 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -18,6 +18,8 @@
 import {
   PluginContext,
 } from '../../public';
+import {ObjectById} from '../../common/state';
+import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
 
 import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
 import {addLatencyTracks, EventLatencyTrack} from './event_latency_track';
@@ -50,6 +52,51 @@
   tracksToAdd: AddTrackArgs[],
 };
 
+export interface ScrollJankTrackSpec {
+  id: string;
+  sqlTableName: string;
+  detailsPanelConfig: CustomSqlDetailsPanelConfig;
+}
+
+// Global state for the scroll jank plugin.
+export class ScrollJankPluginState {
+  private static instance: ScrollJankPluginState;
+  private tracks: ObjectById<ScrollJankTrackSpec>;
+
+  private constructor() {
+    this.tracks = {};
+  }
+
+  public static getInstance(): ScrollJankPluginState {
+    if (!ScrollJankPluginState.instance) {
+      ScrollJankPluginState.instance = new ScrollJankPluginState();
+    }
+
+    return ScrollJankPluginState.instance;
+  }
+
+  public registerTrack(args: {
+    kind: string,
+    trackId: string,
+    tableName: string,
+    detailsPanelConfig: CustomSqlDetailsPanelConfig,
+  }): void {
+    this.tracks[args.kind] = {
+      id: args.trackId,
+      sqlTableName: args.tableName,
+      detailsPanelConfig: args.detailsPanelConfig,
+    };
+  }
+
+  public unregisterTrack(kind: string): void {
+    delete this.tracks[kind];
+  }
+
+  public getTrack(kind: string): ScrollJankTrackSpec|undefined {
+    return this.tracks[kind];
+  }
+}
+
 export async function getScrollJankTracks(engine: Engine):
     Promise<ScrollJankTracks> {
   const result: ScrollJankTracks = {
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index e63dfa1..be3d6f6 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -30,6 +30,7 @@
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
+import {ScrollJankPluginState} from './index';
 
 import {ScrollJankTracks as DecideTracksResult} from './index';
 
@@ -65,13 +66,22 @@
   constructor(args: NewTrackArgs) {
     super(args);
 
+    ScrollJankPluginState.getInstance().registerTrack({
+      kind: TopLevelScrollTrack.kind,
+      trackId: this.trackId,
+      tableName: this.tableName,
+      detailsPanelConfig: this.getDetailsPanel(),
+    });
+
     this.displayColumns['id'] = {displayName: 'Scroll Id (gesture_scroll_id)'};
     this.displayColumns['ts'] = {displayName: 'Start time'};
     this.displayColumns['dur'] = {displayName: 'Duration'};
   }
 
-  async initSqlTable(tableName: string) {
-    await super.initSqlTable(tableName);
+  onDestroy() {
+    super.onDestroy();
+    ScrollJankPluginState.getInstance().unregisterTrack(
+        TopLevelScrollTrack.kind);
   }
 }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/top_level_jank_track.ts b/ui/src/tracks/chrome_scroll_jank/top_level_jank_track.ts
index 848f1d3..050f08d 100644
--- a/ui/src/tracks/chrome_scroll_jank/top_level_jank_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/top_level_jank_track.ts
@@ -27,6 +27,7 @@
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
+import {ScrollJankPluginState} from './index';
 
 import {ScrollJankTracks as DecideTracksResult} from './index';
 
@@ -46,6 +47,13 @@
   constructor(args: NewTrackArgs) {
     super(args);
 
+    ScrollJankPluginState.getInstance().registerTrack({
+      kind: TopLevelJankTrack.kind,
+      trackId: this.trackId,
+      tableName: this.tableName,
+      detailsPanelConfig: this.getDetailsPanel(),
+    });
+
     this.displayColumns['name'] = {};
     this.displayColumns['id'] = {displayName: 'Interval ID'};
     this.displayColumns['ts'] = {displayName: 'Start time'};
@@ -69,8 +77,9 @@
     };
   }
 
-  async initSqlTable(tableName: string) {
-    await super.initSqlTable(tableName);
+  onDestroy() {
+    super.onDestroy();
+    ScrollJankPluginState.getInstance().unregisterTrack(TopLevelJankTrack.kind);
   }
 }
 
@@ -91,7 +100,7 @@
       FROM chrome_scrolling_intervals
       UNION ALL
       SELECT
-        "Janky Scrolling Time" AS name,
+        "Janky Frame Visible" AS name,
         ts,
         dur
       FROM chrome_scroll_jank_intervals_v3
diff --git a/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_details_panel.ts b/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_details_panel.ts
new file mode 100644
index 0000000..e6a895f
--- /dev/null
+++ b/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_details_panel.ts
@@ -0,0 +1,257 @@
+// 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 {exists} from '../../base/utils';
+import {EngineProxy} from '../../common/engine';
+import {LONG, NUM, STR, STR_NULL} from '../../common/query_result';
+import {
+  TPDuration,
+  tpDurationFromSql,
+  TPTime,
+  tpTimeFromSql,
+} from '../../common/time';
+import {
+  BottomTab,
+  bottomTabRegistry,
+  NewBottomTabArgs,
+} from '../../frontend/bottom_tab';
+import {
+  GenericSliceDetailsTabConfig,
+} from '../../frontend/generic_slice_details_tab';
+import {getSlice, SliceDetails, sliceRef} from '../../frontend/sql/slice';
+import {asSliceSqlId, asTPTimestamp} from '../../frontend/sql_types';
+import {sqlValueToString} from '../../frontend/sql_utils';
+import {DetailsShell} from '../../frontend/widgets/details_shell';
+import {Duration} from '../../frontend/widgets/duration';
+import {GridLayout} from '../../frontend/widgets/grid_layout';
+import {Section} from '../../frontend/widgets/section';
+import {SqlRef} from '../../frontend/widgets/sql_ref';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {dictToTreeNodes, Tree, TreeNode} from '../../frontend/widgets/tree';
+
+import {
+  eventLatencySlice,
+  EventLatencySlice,
+  getEventLatencyDescendantSlice,
+  getEventLatencySlice,
+} from './event_latency_slice';
+import {ScrollJankPluginState} from './index';
+import {
+  TopLevelEventLatencyTrack,
+} from './top_level_janky_event_latencies_track';
+import {raf} from '../../core/raf_scheduler';
+
+interface Data {
+  id: number;
+  // The slice display name - the cause + subcause of jank.
+  name: string;
+  // The stage of EventLatency that is the cause of jank.
+  jankCause: string;
+  // Where possible, the subcause of jank.
+  jankSubcause: string;
+  // The slice type - e.g. EventLatency
+  type: string;
+  // Timestamp of the beginning of this slice in nanoseconds.
+  ts: TPTime;
+  // Duration of this slice in nanoseconds.
+  dur: TPDuration;
+}
+
+async function getSliceDetails(
+    engine: EngineProxy, id: number): Promise<SliceDetails|undefined> {
+  return getSlice(engine, asSliceSqlId(id));
+}
+
+export class JankyEventLatenciesDetailsPanel extends
+    BottomTab<GenericSliceDetailsTabConfig> {
+  static readonly kind = 'org.perfetto.JankyEventLatenciesDetailsPanel';
+  static title = 'Chrome Scroll Jank Causes';
+  private loaded = false;
+  private sliceDetails?: SliceDetails;
+  private eventLatencySliceDetails?: EventLatencySlice;
+  private causeSliceDetails?: EventLatencySlice;
+  private subcauseSliceDetails?: EventLatencySlice;
+
+  data: Data|undefined;
+
+  static create(args: NewBottomTabArgs): JankyEventLatenciesDetailsPanel {
+    return new JankyEventLatenciesDetailsPanel(args);
+  }
+
+  constructor(args: NewBottomTabArgs) {
+    super(args);
+    this.loadData();
+  }
+
+  private async loadData() {
+    const trackDetails = ScrollJankPluginState.getInstance().getTrack(
+        TopLevelEventLatencyTrack.kind);
+
+    const queryResult = await this.engine.query(`
+        SELECT
+          id,
+          name,
+          jank_cause AS jankCause,
+          jank_subcause AS jankSubcause,
+          type,
+          ts,
+          dur
+        FROM ${trackDetails?.sqlTableName} where id = ${this.config.id}`);
+
+    const iter = queryResult.firstRow({
+      id: NUM,
+      name: STR,
+      jankCause: STR,
+      jankSubcause: STR_NULL,
+      type: STR,
+      ts: LONG,
+      dur: LONG,
+    });
+    this.data = {
+      id: iter.id,
+      name: iter.name,
+      jankCause: iter.jankCause,
+      jankSubcause: iter.jankSubcause,
+      type: iter.type,
+      ts: iter.ts,
+      dur: iter.dur,
+    } as Data;
+
+    await this.loadSlices();
+    this.loaded = true;
+
+    raf.scheduleFullRedraw();
+  }
+
+  private hasCause(): boolean {
+    if (this.data === undefined) {
+      return false;
+    }
+    return this.data.jankCause !== 'UNKNOWN';
+  }
+
+  private hasSubcause(): boolean {
+    return this.hasCause() && this.data?.jankSubcause !== undefined;
+  }
+
+  private async loadSlices() {
+    this.sliceDetails = await getSliceDetails(this.engine, this.config.id);
+    this.eventLatencySliceDetails =
+        await getEventLatencySlice(this.engine, this.config.id);
+
+    if (this.hasCause()) {
+      this.causeSliceDetails = await getEventLatencyDescendantSlice(
+          this.engine, this.config.id, this.data?.jankCause);
+    }
+
+    if (this.hasSubcause()) {
+      this.subcauseSliceDetails = await getEventLatencyDescendantSlice(
+          this.engine, this.config.id, this.data?.jankSubcause);
+    }
+  }
+
+  viewTab() {
+    if (this.data === undefined) {
+      return m('h2', 'Loading');
+    }
+
+    const detailsDict: {[key: string]: m.Child} = {
+      'Janked Event Latency stage':
+          exists(this.sliceDetails) && exists(this.causeSliceDetails) ?
+          eventLatencySlice(
+              this.causeSliceDetails,
+              this.data.jankCause,
+              this.sliceDetails.sqlTrackId) :
+          sqlValueToString(this.data.jankCause),
+    };
+
+    if (sqlValueToString(this.data.jankSubcause) != 'NULL') {
+      detailsDict['Sub-cause of Jank'] =
+          exists(this.sliceDetails) && exists(this.subcauseSliceDetails) ?
+          eventLatencySlice(
+              this.subcauseSliceDetails,
+              this.data.jankSubcause,
+              this.sliceDetails.sqlTrackId) :
+          sqlValueToString(this.data.jankSubcause);
+    }
+
+    detailsDict['Start time'] =
+        m(Timestamp, {ts: asTPTimestamp(tpTimeFromSql(this.data.ts))});
+    detailsDict['Duration'] =
+        m(Duration, {dur: tpDurationFromSql(this.data.dur)});
+    detailsDict['Slice Type'] = sqlValueToString(this.data.type as string);
+
+    const details = dictToTreeNodes(detailsDict);
+
+    if (exists(this.sliceDetails)) {
+      details.push(m(TreeNode, {
+        left: sliceRef(this.sliceDetails, 'Original EventLatency'),
+        right: '',
+      }));
+      if (exists(this.eventLatencySliceDetails)) {
+        details.push(m(TreeNode, {
+          left: eventLatencySlice(
+              this.eventLatencySliceDetails,
+              'Chrome Input Event Latencies',
+              this.sliceDetails.sqlTrackId),
+          right: '',
+        }));
+      }
+    }
+
+    // TODO(b/278844325): add links to the correct process/track for cause.
+
+    return m(
+        DetailsShell,
+        {
+          title: JankyEventLatenciesDetailsPanel.title,
+        },
+        m(
+            GridLayout,
+            m(
+                Section,
+                {title: 'Details'},
+                m(Tree, details),
+                ),
+            m(
+                Section,
+                {title: 'Metadata'},
+                m(Tree, [m(TreeNode, {
+                    left: 'SQL ID',
+                    right: m(SqlRef, {
+                      table: 'chrome_janky_event_latencies_v3',
+                      id: this.config.id,
+                    }),
+                  })]),
+                ),
+            ),
+    );
+  }
+
+  getTitle(): string {
+    return `Current Selection`;
+  }
+
+  isLoading() {
+    return this.loaded;
+  }
+
+  renderTabCanvas() {
+    return;
+  }
+}
+
+bottomTabRegistry.register(JankyEventLatenciesDetailsPanel);
diff --git a/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_track.ts b/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_track.ts
index 8577fd1..45bbae8 100644
--- a/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/top_level_janky_event_latencies_track.ts
@@ -19,10 +19,6 @@
   PrimaryTrackSortKey,
   SCROLLING_TRACK_GROUP,
 } from '../../common/state';
-import {
-  Columns,
-  GenericSliceDetailsTab,
-} from '../../frontend/generic_slice_details_tab';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, Track} from '../../frontend/track';
 import {
@@ -31,12 +27,15 @@
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
 
+import {ScrollJankPluginState} from './index';
 import {ScrollJankTracks as DecideTracksResult} from './index';
+import {
+  JankyEventLatenciesDetailsPanel,
+} from './top_level_janky_event_latencies_details_panel';
 
 export class TopLevelEventLatencyTrack extends
     CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
   static readonly kind = 'org.chromium.ScrollJank.top_level_event_latencies';
-  displayColumns: Columns = {};
 
   static create(args: NewTrackArgs): Track {
     return new TopLevelEventLatencyTrack(args);
@@ -44,27 +43,29 @@
 
   constructor(args: NewTrackArgs) {
     super(args);
-    this.displayColumns['cause_of_jank'] = {displayName: 'Cause of Jank'};
-    this.displayColumns['sub_cause_of_jank'] = {
-      displayName: 'Sub-cause of Jank',
-    };
-    this.displayColumns['id'] = {displayName: 'Slice ID'};
-    this.displayColumns['ts'] = {displayName: 'Start time'};
-    this.displayColumns['dur'] = {displayName: 'Duration'};
-    this.displayColumns['type'] = {displayName: 'Slice Type'};
+    ScrollJankPluginState.getInstance().registerTrack({
+      kind: TopLevelEventLatencyTrack.kind,
+      trackId: this.trackId,
+      tableName: this.tableName,
+      detailsPanelConfig: this.getDetailsPanel(),
+    });
   }
 
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       columns: [
-        'id',
-        'ts',
-        'dur',
-        'track_id',
-        'cause_of_jank || IIF(sub_cause_of_jank IS NOT NULL, "::" || sub_cause_of_jank, "") AS name',
-        'cause_of_jank',
-        'name AS type',
-        'sub_cause_of_jank',
+        `id`,
+        `ts`,
+        `dur`,
+        `track_id`,
+        `IIF(
+          cause_of_jank IS NOT NULL,
+          cause_of_jank || IIF(
+            sub_cause_of_jank IS NOT NULL, "::" || sub_cause_of_jank, ""
+            ), "UNKNOWN") AS name`,
+        `IFNULL(cause_of_jank, "UNKNOWN") AS jank_cause`,
+        `name AS type`,
+        `sub_cause_of_jank AS jank_subcause`,
       ],
       sqlTableName: 'chrome_janky_event_latencies_v3',
     };
@@ -72,17 +73,18 @@
 
   getDetailsPanel(): CustomSqlDetailsPanelConfig {
     return {
-      kind: GenericSliceDetailsTab.kind,
+      kind: JankyEventLatenciesDetailsPanel.kind,
       config: {
         sqlTableName: this.tableName,
         title: 'Chrome Scroll Jank Event Latency: Cause',
-        columns: this.displayColumns,
       },
     };
   }
 
-  async initSqlTable(tableName: string) {
-    super.initSqlTable(tableName);
+  onDestroy() {
+    super.onDestroy();
+    ScrollJankPluginState.getInstance().unregisterTrack(
+        TopLevelEventLatencyTrack.kind);
   }
 }
 
@@ -94,7 +96,6 @@
 
   await engine.query(`SELECT IMPORT('chrome.chrome_scroll_janks');`);
 
-
   result.tracksToAdd.push({
     id: uuidv4(),
     engineId: engine.id,