Merge "Reland "Make the mapping between panels information and VDOM elements explicit""
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);
       }
     }