Merge "Support new startup tracepoint format"
diff --git a/Android.bp b/Android.bp
index d24098c..3c5e89b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -4204,6 +4204,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -4430,6 +4431,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -4489,6 +4491,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/net.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/oom.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.gen.cc",
@@ -4548,6 +4551,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -4607,6 +4611,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/net.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/oom.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.gen.h",
@@ -4670,6 +4675,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -4728,6 +4734,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/net.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pb.cc",
@@ -4787,6 +4794,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -4845,6 +4853,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/net.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pb.h",
@@ -4908,6 +4917,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -4967,6 +4977,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/net.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pbzero.cc",
@@ -5026,6 +5037,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
@@ -5085,6 +5097,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/net.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/regulator.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/sched.pbzero.h",
@@ -9506,6 +9519,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
diff --git a/BUILD b/BUILD
index 7411995..42b652d 100644
--- a/BUILD
+++ b/BUILD
@@ -2960,6 +2960,7 @@
         "protos/perfetto/trace/ftrace/net.proto",
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/power.proto",
+        "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
         "protos/perfetto/trace/ftrace/regulator.proto",
         "protos/perfetto/trace/ftrace/sched.proto",
diff --git a/CHANGELOG b/CHANGELOG
index 955527a..382f468 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -8,6 +8,9 @@
       message.
   Trace Processor:
     * Added prebuilts for mac-arm64.
+    * Changed LIKE comparisions to be case-insenstive. This reverts the change
+      introduced in v22. GLOB should be used where case senstive searches are
+      desired; built-in metrics continue to require the use of GLOB.
     * Added an optional dependency from trace processor onto a subset of
       sources from llvm-project for function name demangling. Bazel embedders
       might need to update their PERFETTO_CONFIG in perfetto_cfg.bzl to opt in
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 9330b8d..a51a05f 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -50,6 +50,7 @@
   "net.proto",
   "oom.proto",
   "power.proto",
+  "printk.proto",
   "raw_syscalls.proto",
   "regulator.proto",
   "sched.proto",
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index e917203..b58950f 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -50,6 +50,7 @@
 import "protos/perfetto/trace/ftrace/net.proto";
 import "protos/perfetto/trace/ftrace/oom.proto";
 import "protos/perfetto/trace/ftrace/power.proto";
+import "protos/perfetto/trace/ftrace/printk.proto";
 import "protos/perfetto/trace/ftrace/raw_syscalls.proto";
 import "protos/perfetto/trace/ftrace/regulator.proto";
 import "protos/perfetto/trace/ftrace/sched.proto";
@@ -504,5 +505,6 @@
     WakeupSourceDeactivateFtraceEvent wakeup_source_deactivate = 405;
     UfshcdCommandFtraceEvent ufshcd_command = 406;
     UfshcdClkGatingFtraceEvent ufshcd_clk_gating = 407;
+    ConsoleFtraceEvent console = 408;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/printk.proto b/protos/perfetto/trace/ftrace/printk.proto
new file mode 100644
index 0000000..45261e6
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/printk.proto
@@ -0,0 +1,10 @@
+// Autogenerated by:
+// ../../tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message ConsoleFtraceEvent {
+  optional string msg = 1;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 27c0af2..7bee482 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -5559,6 +5559,14 @@
 
 // End of protos/perfetto/trace/ftrace/power.proto
 
+// Begin of protos/perfetto/trace/ftrace/printk.proto
+
+message ConsoleFtraceEvent {
+  optional string msg = 1;
+}
+
+// End of protos/perfetto/trace/ftrace/printk.proto
+
 // Begin of protos/perfetto/trace/ftrace/raw_syscalls.proto
 
 message SysEnterFtraceEvent {
@@ -6378,6 +6386,7 @@
     WakeupSourceDeactivateFtraceEvent wakeup_source_deactivate = 405;
     UfshcdCommandFtraceEvent ufshcd_command = 406;
     UfshcdClkGatingFtraceEvent ufshcd_clk_gating = 407;
+    ConsoleFtraceEvent console = 408;
   }
 }
 
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index c30f4cd..debe2a7 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -581,6 +581,7 @@
     NO_RESPONSE_HEAD = 52;
     ACTIVATION_NAVIGATION_DISALLOWED_FOR_BUG_1234857 = 53;
     ERROR_DOCUMENT = 54;
+    FENCED_FRAMES_EMBEDDER = 55;
   }
 
   optional BackForwardCacheNotRestoredReason
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index f83aede..862f21d 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -2020,7 +2020,8 @@
                                       protozero::ConstBytes blob) {
   protos::pbzero::UfshcdCommandFtraceEvent::Decoder evt(blob.data, blob.size);
   uint32_t num = evt.doorbell() > 0 ?
-      static_cast<uint32_t>(PERFETTO_POPCOUNT(evt.doorbell())) : 1;
+      static_cast<uint32_t>(PERFETTO_POPCOUNT(evt.doorbell())) :
+      (evt.str_t() == 1 ? 0 : 1);
 
   TrackId track = context_->track_tracker->InternGlobalCounterTrack(
       ufs_command_count_id_);
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 9ace9ee..a1660ff 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -108,11 +108,6 @@
   if (error) {
     PERFETTO_FATAL("Error setting pragma temp_store: %s", error);
   }
-  sqlite3_exec(db, "PRAGMA case_sensitive_like = 1", 0, 0, &error);
-  if (error) {
-    PERFETTO_FATAL("Error setting pragma case_sensitive_like: %s", error);
-  }
-
   sqlite3_str_split_init(db);
 // In Android tree builds, we don't have the percentile module.
 // Just don't include it.
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index ae030ab..65012de 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -6648,6 +6648,16 @@
        kUnsetFtraceId,
        405,
        kUnsetSize},
+      {"console",
+       "printk",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "msg", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       408,
+       kUnsetSize},
       {"sys_enter",
        "raw_syscalls",
        {
diff --git a/tools/ftrace_proto_gen/event_list b/tools/ftrace_proto_gen/event_list
index 11bd455..9189c95 100644
--- a/tools/ftrace_proto_gen/event_list
+++ b/tools/ftrace_proto_gen/event_list
@@ -402,3 +402,4 @@
 power/wakeup_source_deactivate
 ufs/ufshcd_command
 ufs/ufshcd_clk_gating
+printk/console
diff --git a/ui/release/channels.json b/ui/release/channels.json
index bdcd0ce..68d1252 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,7 +2,7 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "b574f45ca4f3ae63100c9868a65f9381b0d16c82"
+      "rev": "525b3b84e74e372dd33969edd2f43dbb7080b23c"
     },
     {
       "name": "canary",
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 55eed50..0b11366 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -109,6 +109,12 @@
   private computedSliceHeight = 0;
   private computedRowSpacing = 0;
 
+  // True if this track (and any views tables it might have created) has been
+  // destroyed. This is unfortunately error prone (since we must manually check
+  // this between each query).
+  // TODO(hjd): Replace once we have cancellable query sequences.
+  private isDestroyed = false;
+
   // TODO(hjd): Remove when updating selection.
   // We shouldn't know here about CHROME_SLICE. Maybe should be set by
   // whatever deals with that. Dunno the namespace of selection is weird. For
@@ -357,6 +363,12 @@
     }  // if (howSlice)
   }
 
+  onDestroy() {
+    super.onDestroy();
+    this.isDestroyed = true;
+    this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
+  }
+
   // This method figures out if the visible window is outside the bounds of
   // the cached data and if so issues new queries (i.e. sorta subsumes the
   // onBoundsChange).
@@ -366,16 +378,14 @@
     if (this.sqlState === 'UNINITIALIZED') {
       this.sqlState = 'INITIALIZING';
 
-      // TODO(hjd): we need an onDestroy. Right now if you contract and expand a
-      // track group this will crash, because the 2nd time we create the track
-      // we end up re-issuing the CREATE VIEW table_name.
-      // Right now this DROP VIEW is a hack, because it: (1) assumes that
-      // tableName is a VIEW and not a TABLE; (2) assume the impl track didn't
-      // create any other TABLE/VIEW (which happens to be true right now but
-      // might now be in future).
-      await this.engine.query(`DROP VIEW IF EXISTS ${this.tableName}`);
+      if (this.isDestroyed) {
+        return;
+      }
       await this.initSqlTable(this.tableName);
 
+      if (this.isDestroyed) {
+        return;
+      }
       const queryRes = await this.engine.query(`select
           ifnull(max(dur), 0) as maxDur, count(1) as rowCount
           from ${this.tableName}`);
@@ -424,6 +434,9 @@
     // - Materialize the unfinished slices one off.
     // - Avoid the union if we know we don't have any -1 slices.
     // - Maybe we don't need the union at all and can deal in TS?
+    if (this.isDestroyed) {
+      return;
+    }
     const queryRes = await this.engine.query(`
     with q1 as (
       select
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index da5298e..30f536e 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -332,9 +332,10 @@
       detailsPanels.push({
         key: 'pivot_table_redux',
         name: 'Pivot Table',
-        vnode:
-            m(PivotTableRedux,
-              {selectionArea: globals.state.pivotTableRedux.selectionArea})
+        vnode: m(PivotTableRedux, {
+          key: 'pivot_table_redux',
+          selectionArea: globals.state.pivotTableRedux.selectionArea
+        })
       });
     }
 
@@ -379,7 +380,7 @@
       detailsPanels.push({
         key: 'selected_flows',
         name: 'Flow Events',
-        vnode: m(FlowEventsAreaSelectedPanel)
+        vnode: m(FlowEventsAreaSelectedPanel, {key: 'flow_events_area'})
       });
     }
 
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 662cd54..2131c10 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, assertFalse, assertTrue} from '../base/logging';
+import {assertExists, 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} from './panel';
+import {isPanelVNode, Panel, PanelSize, PanelVNode} from './panel';
 import {
   debugNow,
   perfDebug,
@@ -47,9 +47,8 @@
   kind: 'TRACKS'|'OVERVIEW'|'DETAILS';
 }
 
-interface PanelInfo {
-  id: string;  // Can be == '' for singleton panels.
-  vnode: AnyAttrsVnode;
+interface PanelPosition {
+  id: string;
   height: number;
   width: number;
   x: number;
@@ -61,8 +60,7 @@
   private parentWidth = 0;
   private parentHeight = 0;
   private scrollTop = 0;
-  private panelInfos: PanelInfo[] = [];
-  private panelByKey = new Map<string, AnyAttrsVnode>();
+  private panelPositions: PanelPosition[] = [];
   private totalPanelHeight = 0;
   private canvasHeight = 0;
 
@@ -97,13 +95,13 @@
     const minY = Math.min(startY, endY);
     const maxY = Math.max(startY, endY);
     const panels: AnyAttrsVnode[] = [];
-    for (let i = 0; i < this.panelInfos.length; i++) {
-      const pos = this.panelInfos[i];
+    for (let i = 0; i < this.panelPositions.length; i++) {
+      const pos = this.panelPositions[i];
       const realPosX = pos.x - TRACK_SHELL_WIDTH;
       if (realPosX + pos.width >= minX && realPosX <= maxX &&
           pos.y + pos.height >= minY && pos.y <= maxY &&
-          pos.vnode.attrs.selectable) {
-        panels.push(pos.vnode);
+          this.attrs.panels[i].attrs.selectable) {
+        panels.push(this.attrs.panels[i]);
       }
     }
     return panels;
@@ -116,14 +114,15 @@
     if (area === undefined ||
         globals.frontendLocalState.areaY.start === undefined ||
         globals.frontendLocalState.areaY.end === undefined ||
-        this.panelInfos.length === 0) {
+        this.panelPositions.length === 0) {
       return;
     }
     // Only get panels from the current panel container if the selection began
     // in this container.
-    const panelContainerTop = this.panelInfos[0].y;
-    const panelContainerBottom = this.panelInfos[this.panelInfos.length - 1].y +
-        this.panelInfos[this.panelInfos.length - 1].height;
+    const panelContainerTop = this.panelPositions[0].y;
+    const panelContainerBottom =
+        this.panelPositions[this.panelPositions.length - 1].y +
+        this.panelPositions[this.panelPositions.length - 1].height;
     if (globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT <
             panelContainerTop ||
         globals.frontendLocalState.areaY.start + TOPBAR_HEIGHT >
@@ -218,26 +217,18 @@
 
   view({attrs}: m.CVnode<Attrs>) {
     this.attrs = attrs;
-    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));
-    }
+    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);
 
     return [
       m(
           '.scroll-limiter',
           m('canvas.main-canvas'),
           ),
-      m('.panels', children)
+      m('.panels', attrs.panels.map(renderPanel))
     ];
   }
 
@@ -310,26 +301,19 @@
    */
   private readPanelHeightsFromDom(dom: Element): boolean {
     const prevHeight = this.totalPanelHeight;
-    this.panelInfos = [];
+    this.panelPositions = [];
     this.totalPanelHeight = 0;
 
-    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
-      });
+    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};
       this.totalPanelHeight += rect.height;
-    });
+    }
 
     return this.totalPanelHeight !== prevHeight;
   }
@@ -348,19 +332,23 @@
     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 < this.panelInfos.length; i++) {
-      const panel = this.panelInfos[i].vnode;
-      const panelHeight = this.panelInfos[i].height;
+    for (let i = 0; i < panels.length; i++) {
+      const panel = panels[i];
+      const panelHeight = this.panelPositions[i].height;
       const yStartOnCanvas = panelYStart - canvasYStart;
 
       if (!isPanelVNode(panel)) {
         throw new Error('Vnode passed to panel container is not a panel');
       }
 
-      flowEventsRendererArgs.registerPanel(panel, yStartOnCanvas, panelHeight);
+      // TODO(hjd): This cast should be unnecessary given the type guard above.
+      const p = panel as PanelVNode<{}>;
+      flowEventsRendererArgs.registerPanel(p, yStartOnCanvas, panelHeight);
 
       if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
         panelYStart += panelHeight;
@@ -376,9 +364,9 @@
       clipRect.rect(0, 0, size.width, size.height);
       this.ctx.clip(clipRect);
       const beforeRender = debugNow();
-      panel.state.renderCanvas(this.ctx, size, panel);
+      p.state.renderCanvas(this.ctx, size, p);
       this.updatePanelStats(
-          i, panel.state, debugNow() - beforeRender, this.ctx, size);
+          i, p.state, debugNow() - beforeRender, this.ctx, size);
       this.ctx.restore();
       panelYStart += panelHeight;
     }
@@ -387,7 +375,7 @@
     this.flowEventsRenderer.render(this.ctx, flowEventsRendererArgs);
     // Collect performance as the last thing we do.
     const redrawDur = debugNow() - redrawStart;
-    this.updatePerfStats(redrawDur, this.panelInfos.length, totalOnCanvas);
+    this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
   }
 
   // The panels each draw on the canvas but some details need to be drawn across
@@ -400,22 +388,24 @@
         globals.frontendLocalState.areaY.end === undefined) {
       return;
     }
-    if (this.panelInfos.length === 0 || area.tracks.length === 0) return;
+    if (this.panelPositions.length === 0 || area.tracks.length === 0) return;
 
     // Find the minY and maxY of the selected tracks in this panel container.
-    const panelContainerTop = this.panelInfos[0].y;
-    const panelContainerBottom = this.panelInfos[this.panelInfos.length - 1].y +
-        this.panelInfos[this.panelInfos.length - 1].height;
+    const panelContainerTop = this.panelPositions[0].y;
+    const panelContainerBottom =
+        this.panelPositions[this.panelPositions.length - 1].y +
+        this.panelPositions[this.panelPositions.length - 1].height;
     let selectedTracksMinY = panelContainerBottom;
     let selectedTracksMaxY = panelContainerTop;
     let trackFromCurrentContainerSelected = false;
-    for (let i = 0; i < this.panelInfos.length; i++) {
-      if (area.tracks.includes(this.panelInfos[i].id)) {
+    for (let i = 0; i < this.panelPositions.length; i++) {
+      if (area.tracks.includes(this.panelPositions[i].id)) {
         trackFromCurrentContainerSelected = true;
-        selectedTracksMinY = Math.min(selectedTracksMinY, this.panelInfos[i].y);
+        selectedTracksMinY =
+            Math.min(selectedTracksMinY, this.panelPositions[i].y);
         selectedTracksMaxY = Math.max(
             selectedTracksMaxY,
-            this.panelInfos[i].y + this.panelInfos[i].height);
+            this.panelPositions[i].y + this.panelPositions[i].height);
       }
     }
 
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 5cd1e9b..0c5716a 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -77,6 +77,10 @@
     this.lastTrackState = assertExists(globals.state.tracks[this.trackId]);
   }
 
+  // Last call the track will receive. Called just before the last reference to
+  // this object is removed.
+  onDestroy() {}
+
   protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
 
   protected get trackState(): TrackState {
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index e5f6631..7f0b4e9 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -172,6 +172,13 @@
     }
   }
 
+  onremove() {
+    if (this.summaryTrack !== undefined) {
+      this.summaryTrack.onDestroy();
+      this.summaryTrack = undefined;
+    }
+  }
+
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const localState = globals.frontendLocalState;
     const selection = globals.state.currentSelection;
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index a916e93..e479628 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -326,6 +326,13 @@
     }
   }
 
+  onremove() {
+    if (this.track !== undefined) {
+      this.track.onDestroy();
+      this.track = undefined;
+    }
+  }
+
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const localState = globals.frontendLocalState;
     const selection = globals.state.currentSelection;