Merge "pprof_builder: only attempt demangling itanium function names"
diff --git a/Android.bp b/Android.bp
index fc9f86f..3c5e89b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -8398,6 +8398,7 @@
         "src/trace_processor/metrics/sql/android/startup/launches.sql",
         "src/trace_processor/metrics/sql/android/startup/launches_maxsdk28.sql",
         "src/trace_processor/metrics/sql/android/startup/launches_minsdk29.sql",
+        "src/trace_processor/metrics/sql/android/startup/launches_minsdk33.sql",
         "src/trace_processor/metrics/sql/android/thread_counter_span_view.sql",
         "src/trace_processor/metrics/sql/android/unsymbolized_frames.sql",
         "src/trace_processor/metrics/sql/chrome/actual_power_by_category.sql",
diff --git a/BUILD b/BUILD
index 5d93094..43e1649 100644
--- a/BUILD
+++ b/BUILD
@@ -1109,6 +1109,7 @@
         "src/trace_processor/metrics/sql/android/startup/launches.sql",
         "src/trace_processor/metrics/sql/android/startup/launches_maxsdk28.sql",
         "src/trace_processor/metrics/sql/android/startup/launches_minsdk29.sql",
+        "src/trace_processor/metrics/sql/android/startup/launches_minsdk33.sql",
         "src/trace_processor/metrics/sql/android/thread_counter_span_view.sql",
         "src/trace_processor/metrics/sql/android/unsymbolized_frames.sql",
         "src/trace_processor/metrics/sql/chrome/actual_power_by_category.sql",
@@ -1353,6 +1354,8 @@
         ":include_perfetto_ext_base_base",
         ":include_perfetto_ext_trace_processor_demangle",
     ],
+    deps = [
+    ] + PERFETTO_CONFIG.deps.llvm_demangle,
     linkstatic = True,
 )
 
diff --git a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
index 53c423e..e0958d4 100644
--- a/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
+++ b/include/perfetto/base/build_configs/bazel/perfetto_build_flags.h
@@ -44,7 +44,7 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_HEAPPROFD() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_STDERR_CRASH_DUMP() (0)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_X64_CPU_OPT() (0)
-#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LLVM_DEMANGLE() (0)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_LLVM_DEMANGLE() (1)
 
 // clang-format on
 #endif  // GEN_BUILD_CONFIG_PERFETTO_BUILD_FLAGS_H_
diff --git a/src/trace_processor/metrics/sql/BUILD.gn b/src/trace_processor/metrics/sql/BUILD.gn
index 5c07ca1..0ec76e6 100644
--- a/src/trace_processor/metrics/sql/BUILD.gn
+++ b/src/trace_processor/metrics/sql/BUILD.gn
@@ -79,6 +79,7 @@
   "android/unsymbolized_frames.sql",
   "android/startup/launches_maxsdk28.sql",
   "android/startup/launches_minsdk29.sql",
+  "android/startup/launches_minsdk33.sql",
   "android/startup/launches.sql",
   "android/startup/hsc.sql",
   "chrome/actual_power_by_category.sql",
diff --git a/src/trace_processor/metrics/sql/android/startup/launches.sql b/src/trace_processor/metrics/sql/android/startup/launches.sql
index 999ba6e..60c1cab 100644
--- a/src/trace_processor/metrics/sql/android/startup/launches.sql
+++ b/src/trace_processor/metrics/sql/android/startup/launches.sql
@@ -30,25 +30,31 @@
 AND (process.name IS NULL OR process.name = 'system_server');
 
 SELECT CREATE_FUNCTION(
-  'ANDROID_SDK_LEVEL()',
-  'INT', "
-    SELECT int_value
-    FROM metadata
-    WHERE name = 'android_sdk_version'
-  ");
-
-SELECT CREATE_FUNCTION(
-  'METRICS_LOGGER_SLICE_COUNT()',
+  'SLICE_COUNT(slice_glob STRING)',
   'INT',
-  "SELECT COUNT(1) FROM slice WHERE name GLOB 'MetricsLogger:*'"
+  'SELECT COUNT(1) FROM slice WHERE name GLOB $slice_glob'
+);
+
+-- All activity launches in the trace, keyed by ID.
+-- Populated by different scripts depending on the platform version / contents.
+-- See android/startup/launches*.sql
+DROP TABLE IF EXISTS launches;
+CREATE TABLE launches(
+  id INTEGER PRIMARY KEY,
+  ts BIG INT,
+  ts_end BIG INT,
+  dur BIG INT,
+  package STRING
 );
 
 -- Note: on Q, we didn't have Android fingerprints but we *did*
 -- have ActivityMetricsLogger events so we will use this approach
 -- if we see any such events.
 SELECT CASE
-  WHEN (ANDROID_SDK_LEVEL() >= 29 OR METRICS_LOGGER_SLICE_COUNT() > 0)
-  THEN RUN_METRIC('android/startup/launches_minsdk29.sql')
+  WHEN SLICE_COUNT('launchingActivity#*:*') > 0
+    THEN RUN_METRIC('android/startup/launches_minsdk33.sql')
+  WHEN SLICE_COUNT('MetricsLogger:*') > 0
+    THEN RUN_METRIC('android/startup/launches_minsdk29.sql')
   ELSE RUN_METRIC('android/startup/launches_maxsdk28.sql')
 END;
 
diff --git a/src/trace_processor/metrics/sql/android/startup/launches_maxsdk28.sql b/src/trace_processor/metrics/sql/android/startup/launches_maxsdk28.sql
index 8753da1..35c797e 100644
--- a/src/trace_processor/metrics/sql/android/startup/launches_maxsdk28.sql
+++ b/src/trace_processor/metrics/sql/android/startup/launches_maxsdk28.sql
@@ -14,23 +14,15 @@
 -- limitations under the License.
 --
 
--- All activity launches in the trace, keyed by ID.
-DROP TABLE IF EXISTS launches;
-CREATE TABLE launches(
-  id INTEGER PRIMARY KEY,
-  ts BIG INT,
-  ts_end BIG INT,
-  dur BIG INT,
-  package STRING
-);
-
 -- Cold/warm starts emitted launching slices on API level 28-.
-INSERT INTO launches(ts, ts_end, dur, package)
+INSERT INTO launches(id, ts, ts_end, dur, package)
 SELECT
+  ROW_NUMBER() OVER(ORDER BY ts) AS id,
   launching_events.ts AS ts,
   launching_events.ts_end AS ts_end,
   launching_events.ts_end - launching_events.ts AS dur,
   package_name AS package
-FROM launching_events;
+FROM launching_events
+ORDER BY ts;
 
 -- TODO(lalitm): add handling of hot starts using frame timings.
diff --git a/src/trace_processor/metrics/sql/android/startup/launches_minsdk29.sql b/src/trace_processor/metrics/sql/android/startup/launches_minsdk29.sql
index 1f4c0f7..e5631bf 100644
--- a/src/trace_processor/metrics/sql/android/startup/launches_minsdk29.sql
+++ b/src/trace_processor/metrics/sql/android/startup/launches_minsdk29.sql
@@ -53,23 +53,14 @@
 SELECT ts FROM slice
 WHERE name = 'MetricsLogger:launchObserverNotifyActivityLaunchFinished';
 
--- All activity launches in the trace, keyed by ID.
-DROP TABLE IF EXISTS launches;
-CREATE TABLE launches(
-  ts BIG INT,
-  ts_end BIG INT,
-  dur BIG INT,
-  id INT,
-  package STRING);
-
 -- Use the starting event package name. The finish event package name
 -- is not reliable in the case of failed launches.
-INSERT INTO launches
+INSERT INTO launches(id, ts, ts_end, dur, package)
 SELECT
+  lpart.id AS id,
   lpart.ts AS ts,
   launching_events.ts_end AS ts_end,
   launching_events.ts_end - lpart.ts AS dur,
-  lpart.id AS id,
   package_name AS package
 FROM launch_partitions AS lpart
 JOIN launching_events ON
diff --git a/src/trace_processor/metrics/sql/android/startup/launches_minsdk33.sql b/src/trace_processor/metrics/sql/android/startup/launches_minsdk33.sql
new file mode 100644
index 0000000..f3773bb
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/startup/launches_minsdk33.sql
@@ -0,0 +1,47 @@
+--
+-- Copyright 2022 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.
+--
+
+DROP VIEW IF EXISTS launch_async_events;
+CREATE VIEW launch_async_events AS
+SELECT
+  ts,
+  dur,
+  SUBSTR(name, 19) id
+FROM slice
+WHERE
+  name GLOB 'launchingActivity#*'
+  AND dur != 0
+  AND INSTR(name, ':') = 0;
+
+DROP VIEW IF EXISTS launch_complete_events;
+CREATE VIEW launch_complete_events AS
+SELECT
+  STR_SPLIT(completed, ':completed:', 0) id,
+  STR_SPLIT(completed, ':completed:', 1) package_name
+FROM (
+  SELECT SUBSTR(name, 19) completed
+  FROM slice
+  WHERE dur = 0 AND name GLOB 'launchingActivity#*:completed:*'
+);
+
+INSERT INTO launches(id, ts, ts_end, dur, package)
+SELECT
+  id,
+  ts,
+  ts + dur ts_end,
+  dur,
+  package_name
+FROM launch_async_events JOIN launch_complete_events USING (id);
diff --git a/test/synth_common.py b/test/synth_common.py
index 92d8e3f..be52735 100644
--- a/test/synth_common.py
+++ b/test/synth_common.py
@@ -187,6 +187,9 @@
   def add_atrace_async_end(self, ts, tid, pid, buf):
     self.add_print(ts, tid, 'F|{}|{}|0'.format(pid, buf))
 
+  def add_atrace_instant(self, ts, tid, pid, buf):
+    self.add_print(ts, tid, 'I|{}|{}'.format(pid, buf))
+
   def add_process(self, pid, ppid, cmdline, uid=None):
     process = self.packet.process_tree.processes.add()
     process.pid = pid
diff --git a/test/trace_processor/startup/android_startup_minsdk33.out b/test/trace_processor/startup/android_startup_minsdk33.out
new file mode 100644
index 0000000..e7022b5
--- /dev/null
+++ b/test/trace_processor/startup/android_startup_minsdk33.out
@@ -0,0 +1,25 @@
+android_startup {
+  startup {
+    startup_id: 1
+    package_name: "com.google.android.calendar"
+    zygote_new_process: false
+    to_first_frame {
+      dur_ns: 100
+      main_thread_by_task_state {
+        running_dur_ns: 0
+        runnable_dur_ns: 0
+        uninterruptible_sleep_dur_ns: 0
+        interruptible_sleep_dur_ns: 0
+      }
+      other_processes_spawned_count: 0
+      dur_ms: 0.0001
+      mcycles_by_core_type {
+      }
+    }
+    activity_hosting_process_count: 0
+    event_timestamps {
+      intent_received: 110
+      first_frame: 210
+    }
+  }
+}
diff --git a/test/trace_processor/startup/android_startup_minsdk33.py b/test/trace_processor/startup/android_startup_minsdk33.py
new file mode 100644
index 0000000..dddc503
--- /dev/null
+++ b/test/trace_processor/startup/android_startup_minsdk33.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+# Copyright (C) 2018 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.
+
+from os import sys, path
+
+import synth_common
+
+trace = synth_common.create_trace()
+trace.add_packet()
+trace.add_process(1, 0, 'init')
+trace.add_process(2, 1, 'system_server')
+trace.add_process(3, 1, 'com.google.android.calendar', 10001)
+
+trace.add_package_list(
+    ts=1, name='com.google.android.calendar', uid=10001, version_code=123)
+
+trace.add_ftrace_packet(cpu=0)
+trace.add_atrace_async_begin(ts=110, tid=2, pid=2, buf='launchingActivity#1')
+trace.add_atrace_async_end(ts=210, tid=2, pid=2, buf='launchingActivity#1')
+trace.add_atrace_instant(
+    ts=211,
+    tid=2,
+    pid=2,
+    buf='launchingActivity#1:completed:com.google.android.calendar')
+
+sys.stdout.buffer.write(trace.trace.SerializeToString())
diff --git a/test/trace_processor/startup/index b/test/trace_processor/startup/index
index 1780fa8..ee6003e 100644
--- a/test/trace_processor/startup/index
+++ b/test/trace_processor/startup/index
@@ -2,6 +2,7 @@
 
 # Startup metric tests.
 android_startup.py android_startup android_startup.out
+android_startup_minsdk33.py android_startup android_startup_minsdk33.out
 android_startup_breakdown.py android_startup android_startup_breakdown.out
 android_startup_process_track.py android_startup android_startup_process_track.out
 android_startup_attribution.py android_startup android_startup_attribution.out
diff --git a/tools/gen_bazel b/tools/gen_bazel
index cf02e0d..76f1b6e 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -46,7 +46,7 @@
     'enable_perfetto_heapprofd=false',
     'enable_perfetto_traced_perf=false',
     'perfetto_force_dcheck="off"',
-    'enable_perfetto_llvm_demangle=false',
+    'enable_perfetto_llvm_demangle=true',
 ])
 
 # Default targets to translate to the blueprint file.
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index 8e9254c..20ae793 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -64,6 +64,8 @@
         beginSliceStartTs: NUM,
         beginSliceEndTs: NUM,
         beginDepth: NUM,
+        beginThreadName: STR_NULL,
+        beginProcessName: STR_NULL,
         endSliceId: NUM,
         endTrackId: NUM,
         endSliceName: STR_NULL,
@@ -71,6 +73,8 @@
         endSliceStartTs: NUM,
         endSliceEndTs: NUM,
         endDepth: NUM,
+        endThreadName: STR_NULL,
+        endProcessName: STR_NULL,
         name: STR_NULL,
         category: STR_NULL,
         id: NUM,
@@ -85,6 +89,10 @@
         const beginSliceStartTs = fromNs(it.beginSliceStartTs);
         const beginSliceEndTs = fromNs(it.beginSliceEndTs);
         const beginDepth = it.beginDepth;
+        const beginThreadName =
+            it.beginThreadName === null ? 'NULL' : it.beginThreadName;
+        const beginProcessName =
+            it.beginProcessName === null ? 'NULL' : it.beginProcessName;
 
         const endSliceId = it.endSliceId;
         const endTrackId = it.endTrackId;
@@ -95,6 +103,10 @@
         const endSliceStartTs = fromNs(it.endSliceStartTs);
         const endSliceEndTs = fromNs(it.endSliceEndTs);
         const endDepth = it.endDepth;
+        const endThreadName =
+            it.endThreadName === null ? 'NULL' : it.endThreadName;
+        const endProcessName =
+            it.endProcessName === null ? 'NULL' : it.endProcessName;
 
         // Category and name present only in version 1 flow events
         // It is most likelly NULL for all other versions
@@ -111,7 +123,9 @@
             sliceCategory: beginSliceCategory,
             sliceStartTs: beginSliceStartTs,
             sliceEndTs: beginSliceEndTs,
-            depth: beginDepth
+            depth: beginDepth,
+            threadName: beginThreadName,
+            processName: beginProcessName
           },
           end: {
             trackId: endTrackId,
@@ -120,7 +134,9 @@
             sliceCategory: endSliceCategory,
             sliceStartTs: endSliceStartTs,
             sliceEndTs: endSliceEndTs,
-            depth: endDepth
+            depth: endDepth,
+            threadName: endThreadName,
+            processName: endProcessName
           },
           dur: endSliceStartTs - beginSliceEndTs,
           category,
@@ -156,6 +172,8 @@
       t1.ts as beginSliceStartTs,
       (t1.ts+t1.dur) as beginSliceEndTs,
       t1.depth as beginDepth,
+      (thread_out.name || ' ' || thread_out.tid) as beginThreadName,
+      (process_out.name || ' ' || process_out.pid) as beginProcessName,
       f.slice_in as endSliceId,
       t2.track_id as endTrackId,
       t2.name as endSliceName,
@@ -163,12 +181,20 @@
       t2.ts as endSliceStartTs,
       (t2.ts+t2.dur) as endSliceEndTs,
       t2.depth as endDepth,
+      (thread_in.name || ' ' || thread_in.tid) as endThreadName,
+      (process_in.name || ' ' || process_in.pid) as endProcessName,
       extract_arg(f.arg_set_id, 'cat') as category,
       extract_arg(f.arg_set_id, 'name') as name,
       f.id as id
     from ${connectedFlows} f
     join slice t1 on f.slice_out = t1.slice_id
     join slice t2 on f.slice_in = t2.slice_id
+    join thread_track track_out on track_out.id = t1.track_id
+    join thread thread_out on thread_out.utid = track_out.utid
+    join thread_track track_in on track_in.id = t2.track_id
+    join thread thread_in on thread_in.utid = track_in.utid
+    join process process_out on process_out.upid = thread_out.upid
+    join process process_in on process_in.upid = thread_in.upid
     `;
     this.queryFlowEvents(
         query, (flows: Flow[]) => publishConnectedFlows(flows));
@@ -217,6 +243,8 @@
       t1.ts as beginSliceStartTs,
       (t1.ts+t1.dur) as beginSliceEndTs,
       t1.depth as beginDepth,
+      NULL as beginThreadName,
+      NULL as beginProcessName,
       f.slice_in as endSliceId,
       t2.track_id as endTrackId,
       t2.name as endSliceName,
@@ -224,6 +252,8 @@
       t2.ts as endSliceStartTs,
       (t2.ts+t2.dur) as endSliceEndTs,
       t2.depth as endDepth,
+      NULL as endThreadName,
+      NULL as endProcessName,
       extract_arg(f.arg_set_id, 'cat') as category,
       extract_arg(f.arg_set_id, 'name') as name,
       f.id as id
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index 500148c..c7783b9 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -61,7 +61,11 @@
       m('th', 'Direction'),
       m('th', 'Duration'),
       m('th', 'Connected Slice ID'),
-      m('th', 'Connected Slice Name')
+      m('th', 'Connected Slice Name'),
+      m('th', 'Thread Out'),
+      m('th', 'Thread In'),
+      m('th', 'Process Out'),
+      m('th', 'Process In')
     ];
 
     if (haveCategories) {
@@ -93,7 +97,11 @@
         m('td.flow-link', args, outgoing ? 'Outgoing' : 'Incoming'),
         m('td.flow-link', args, timeToCode(flow.dur)),
         m('td.flow-link', args, otherEnd.sliceId.toString()),
-        m('td.flow-link', args, otherEnd.sliceName)
+        m('td.flow-link', args, otherEnd.sliceName),
+        m('td.flow-link', args, flow.begin.threadName),
+        m('td.flow-link', args, flow.end.threadName),
+        m('td.flow-link', args, flow.begin.processName),
+        m('td.flow-link', args, flow.end.processName)
       ];
 
       if (haveCategories) {
@@ -106,7 +114,7 @@
 
     return m('.details-panel', [
       m('.details-panel-heading', m('h2', `Flow events`)),
-      m('.flow-events-table', m('table.half-width', rows))
+      m('.flow-events-table', m('table', rows))
     ]);
   }
 
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 473a945..71e2a34 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -77,6 +77,12 @@
   sliceId: number;
   sliceStartTs: number;
   sliceEndTs: number;
+  // Thread and process info. Only set in sliceSelected not in areaSelected as
+  // the latter doesn't display per-flow info and it'd be a waste to join
+  // additional tables for undisplayed info in that case. Nothing precludes
+  // adding this in a future iteration however.
+  threadName: string;
+  processName: string;
 
   depth: number;
 }
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 2131c10..662cd54 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -14,7 +14,7 @@
 
 import * as m from 'mithril';
 
-import {assertExists, assertTrue} from '../base/logging';
+import {assertExists, assertFalse, assertTrue} from '../base/logging';
 
 import {TOPBAR_HEIGHT, TRACK_SHELL_WIDTH} from './css_constants';
 import {
@@ -22,7 +22,7 @@
   FlowEventsRendererArgs
 } from './flow_events_renderer';
 import {globals} from './globals';
-import {isPanelVNode, Panel, PanelSize, PanelVNode} from './panel';
+import {isPanelVNode, Panel, PanelSize} from './panel';
 import {
   debugNow,
   perfDebug,
@@ -47,8 +47,9 @@
   kind: 'TRACKS'|'OVERVIEW'|'DETAILS';
 }
 
-interface PanelPosition {
-  id: string;
+interface PanelInfo {
+  id: string;  // Can be == '' for singleton panels.
+  vnode: AnyAttrsVnode;
   height: number;
   width: number;
   x: number;
@@ -60,7 +61,8 @@
   private parentWidth = 0;
   private parentHeight = 0;
   private scrollTop = 0;
-  private panelPositions: PanelPosition[] = [];
+  private panelInfos: PanelInfo[] = [];
+  private panelByKey = new Map<string, AnyAttrsVnode>();
   private totalPanelHeight = 0;
   private canvasHeight = 0;
 
@@ -95,13 +97,13 @@
     const minY = Math.min(startY, endY);
     const maxY = Math.max(startY, endY);
     const panels: AnyAttrsVnode[] = [];
-    for (let i = 0; i < this.panelPositions.length; i++) {
-      const pos = this.panelPositions[i];
+    for (let i = 0; i < this.panelInfos.length; i++) {
+      const pos = this.panelInfos[i];
       const realPosX = pos.x - TRACK_SHELL_WIDTH;
       if (realPosX + pos.width >= minX && realPosX <= maxX &&
           pos.y + pos.height >= minY && pos.y <= maxY &&
-          this.attrs.panels[i].attrs.selectable) {
-        panels.push(this.attrs.panels[i]);
+          pos.vnode.attrs.selectable) {
+        panels.push(pos.vnode);
       }
     }
     return panels;
@@ -114,15 +116,14 @@
     if (area === undefined ||
         globals.frontendLocalState.areaY.start === undefined ||
         globals.frontendLocalState.areaY.end === undefined ||
-        this.panelPositions.length === 0) {
+        this.panelInfos.length === 0) {
       return;
     }
     // Only get panels from the current panel container if the selection began
     // in this container.
-    const panelContainerTop = this.panelPositions[0].y;
-    const panelContainerBottom =
-        this.panelPositions[this.panelPositions.length - 1].y +
-        this.panelPositions[this.panelPositions.length - 1].height;
+    const panelContainerTop = this.panelInfos[0].y;
+    const panelContainerBottom = this.panelInfos[this.panelInfos.length - 1].y +
+        this.panelInfos[this.panelInfos.length - 1].height;
     if (globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT <
             panelContainerTop ||
         globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT >
@@ -217,18 +218,26 @@
 
   view({attrs}: m.CVnode<Attrs>) {
     this.attrs = attrs;
-    const renderPanel = (panel: m.Vnode) => perfDebug() ?
-        m('.panel',
-          {key: panel.key},
-          [panel, m('.debug-panel-border', {key: 'debug-panel-border'})]) :
-        m('.panel', {key: panel.key}, panel);
+    this.panelByKey.clear();
+    const children = [];
+    for (const panel of attrs.panels) {
+      const key = assertExists(panel.key) as string;
+      assertFalse(this.panelByKey.has(key));
+      this.panelByKey.set(key, panel);
+      children.push(
+          m('.panel',
+            {key: panel.key, 'data-key': panel.key},
+            perfDebug() ?
+                [panel, m('.debug-panel-border', {key: 'debug-panel-border'})] :
+                panel));
+    }
 
     return [
       m(
           '.scroll-limiter',
           m('canvas.main-canvas'),
           ),
-      m('.panels', attrs.panels.map(renderPanel))
+      m('.panels', children)
     ];
   }
 
@@ -301,19 +310,26 @@
    */
   private readPanelHeightsFromDom(dom: Element): boolean {
     const prevHeight = this.totalPanelHeight;
-    this.panelPositions = [];
+    this.panelInfos = [];
     this.totalPanelHeight = 0;
 
-    const panels = dom.parentElement!.querySelectorAll('.panel');
-    assertTrue(panels.length === this.attrs.panels.length);
-    for (let i = 0; i < panels.length; i++) {
-      const rect = panels[i].getBoundingClientRect();
-      const id = this.attrs.panels[i].attrs.id ||
-          this.attrs.panels[i].attrs.trackGroupId;
-      this.panelPositions[i] =
-          {id, height: rect.height, width: rect.width, x: rect.x, y: rect.y};
+    dom.parentElement!.querySelectorAll('.panel').forEach(panel => {
+      const key = assertExists(panel.getAttribute('data-key'));
+      const vnode = assertExists(this.panelByKey.get(key));
+
+      // NOTE: the id can be undefined for singletons like overview timeline.
+      const id = vnode.attrs.id || vnode.attrs.trackGroupId || '';
+      const rect = panel.getBoundingClientRect();
+      this.panelInfos.push({
+        id,
+        height: rect.height,
+        width: rect.width,
+        x: rect.x,
+        y: rect.y,
+        vnode
+      });
       this.totalPanelHeight += rect.height;
-    }
+    });
 
     return this.totalPanelHeight !== prevHeight;
   }
@@ -332,23 +348,19 @@
     this.handleAreaSelection();
 
     let panelYStart = 0;
-    const panels = assertExists(this.attrs).panels;
-    assertTrue(panels.length === this.panelPositions.length);
     let totalOnCanvas = 0;
     const flowEventsRendererArgs =
         new FlowEventsRendererArgs(this.parentWidth, this.canvasHeight);
-    for (let i = 0; i < panels.length; i++) {
-      const panel = panels[i];
-      const panelHeight = this.panelPositions[i].height;
+    for (let i = 0; i < this.panelInfos.length; i++) {
+      const panel = this.panelInfos[i].vnode;
+      const panelHeight = this.panelInfos[i].height;
       const yStartOnCanvas = panelYStart - canvasYStart;
 
       if (!isPanelVNode(panel)) {
         throw new Error('Vnode passed to panel container is not a panel');
       }
 
-      // TODO(hjd): This cast should be unnecessary given the type guard above.
-      const p = panel as PanelVNode<{}>;
-      flowEventsRendererArgs.registerPanel(p, yStartOnCanvas, panelHeight);
+      flowEventsRendererArgs.registerPanel(panel, yStartOnCanvas, panelHeight);
 
       if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
         panelYStart += panelHeight;
@@ -364,9 +376,9 @@
       clipRect.rect(0, 0, size.width, size.height);
       this.ctx.clip(clipRect);
       const beforeRender = debugNow();
-      p.state.renderCanvas(this.ctx, size, p);
+      panel.state.renderCanvas(this.ctx, size, panel);
       this.updatePanelStats(
-          i, p.state, debugNow() - beforeRender, this.ctx, size);
+          i, panel.state, debugNow() - beforeRender, this.ctx, size);
       this.ctx.restore();
       panelYStart += panelHeight;
     }
@@ -375,7 +387,7 @@
     this.flowEventsRenderer.render(this.ctx, flowEventsRendererArgs);
     // Collect performance as the last thing we do.
     const redrawDur = debugNow() - redrawStart;
-    this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
+    this.updatePerfStats(redrawDur, this.panelInfos.length, totalOnCanvas);
   }
 
   // The panels each draw on the canvas but some details need to be drawn across
@@ -388,24 +400,22 @@
         globals.frontendLocalState.areaY.end === undefined) {
       return;
     }
-    if (this.panelPositions.length === 0 || area.tracks.length === 0) return;
+    if (this.panelInfos.length === 0 || area.tracks.length === 0) return;
 
     // Find the minY and maxY of the selected tracks in this panel container.
-    const panelContainerTop = this.panelPositions[0].y;
-    const panelContainerBottom =
-        this.panelPositions[this.panelPositions.length - 1].y +
-        this.panelPositions[this.panelPositions.length - 1].height;
+    const panelContainerTop = this.panelInfos[0].y;
+    const panelContainerBottom = this.panelInfos[this.panelInfos.length - 1].y +
+        this.panelInfos[this.panelInfos.length - 1].height;
     let selectedTracksMinY = panelContainerBottom;
     let selectedTracksMaxY = panelContainerTop;
     let trackFromCurrentContainerSelected = false;
-    for (let i = 0; i < this.panelPositions.length; i++) {
-      if (area.tracks.includes(this.panelPositions[i].id)) {
+    for (let i = 0; i < this.panelInfos.length; i++) {
+      if (area.tracks.includes(this.panelInfos[i].id)) {
         trackFromCurrentContainerSelected = true;
-        selectedTracksMinY =
-            Math.min(selectedTracksMinY, this.panelPositions[i].y);
+        selectedTracksMinY = Math.min(selectedTracksMinY, this.panelInfos[i].y);
         selectedTracksMaxY = Math.max(
             selectedTracksMaxY,
-            this.panelPositions[i].y + this.panelPositions[i].height);
+            this.panelInfos[i].y + this.panelInfos[i].height);
       }
     }