Add performance debug mode

Shows realtime rendering performance stats when turned on.

See preview at https://deepanjan.me/previews/aosp/761918
The button on the top right corner toggle perf debug mode.

Change-Id: I02fcc984833ef42c1280c873f295c60a7cdca8c2
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index a3866bb..4fb18f5 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -17,7 +17,14 @@
 import {assertExists, assertTrue} from '../base/logging';
 
 import {globals} from './globals';
-import {isPanelVNode} from './panel';
+import {isPanelVNode, Panel, PanelSize} from './panel';
+import {
+  debugNow,
+  perfDebug,
+  perfDisplay,
+  RunningStatistics,
+  runningStatStr
+} from './perf';
 
 /**
  * If the panel container scrolls, the backing canvas height is
@@ -41,6 +48,13 @@
   private totalPanelHeight = 0;
   private canvasHeight = 0;
 
+  private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
+  private perfStats = {
+    totalPanels: 0,
+    panelsOnCanvas: 0,
+    renderStats: new RunningStatistics(10),
+  };
+
   // attrs received in the most recent mithril redraw.
   private attrs?: Attrs;
 
@@ -56,6 +70,7 @@
         vnode.attrs.doesScroll ? SCROLLING_CANVAS_OVERDRAW_FACTOR : 1;
     this.canvasRedrawer = () => this.redrawCanvas();
     globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
+    perfDisplay.addContainer(this);
   }
 
   oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
@@ -113,16 +128,21 @@
     if (attrs.doesScroll) {
       dom.parentElement!.removeEventListener('scroll', this.parentOnScroll);
     }
+    perfDisplay.removeContainer(this);
   }
 
   view({attrs}: m.CVnode<Attrs>) {
     // We receive a new vnode object with new attrs on every mithril redraw. We
     // store the latest attrs so redrawCanvas can use it.
     this.attrs = attrs;
+    const renderPanel = (panel: m.Vnode) => perfDebug() ?
+        m('.panel', panel, m('.debug-panel-border')) :
+        m('.panel', panel);
+
     return m(
         '.scroll-limiter',
         m('canvas.main-canvas'),
-        attrs.panels.map(panel => m('.panel', panel)));
+        attrs.panels.map(renderPanel));
   }
 
   onupdate(vnodeDom: m.CVnodeDOM<Attrs>) {
@@ -186,6 +206,7 @@
   }
 
   private redrawCanvas() {
+    const redrawStart = debugNow();
     if (!this.ctx) return;
     this.ctx.clearRect(0, 0, this.parentWidth, this.canvasHeight);
     const canvasYStart = this.scrollTop - this.getCanvasOverdrawHeightPerSide();
@@ -193,6 +214,7 @@
     let panelYStart = 0;
     const panels = assertExists(this.attrs).panels;
     assertTrue(panels.length === this.panelHeights.length);
+    let totalOnCanvas = 0;
     for (let i = 0; i < panels.length; i++) {
       const panel = panels[i];
       const panelHeight = this.panelHeights[i];
@@ -203,6 +225,8 @@
         continue;
       }
 
+      totalOnCanvas++;
+
       if (!isPanelVNode(panel)) {
         throw Error('Vnode passed to panel container is not a panel');
       }
@@ -213,10 +237,53 @@
       const size = {width: this.parentWidth, height: panelHeight};
       clipRect.rect(0, 0, size.width, size.height);
       this.ctx.clip(clipRect);
+      const beforeRender = debugNow();
       panel.state.renderCanvas(this.ctx, size, panel);
+      this.updatePanelStats(
+          i, panel.state, debugNow() - beforeRender, this.ctx, size);
       this.ctx.restore();
       panelYStart += panelHeight;
     }
+    const redrawDur = debugNow() - redrawStart;
+    this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
+  }
+
+  private updatePanelStats(
+      panelIndex: number, panel: Panel, renderTime: number,
+      ctx: CanvasRenderingContext2D, size: PanelSize) {
+    if (!perfDebug()) return;
+    let renderStats = this.panelPerfStats.get(panel);
+    if (renderStats === undefined) {
+      renderStats = new RunningStatistics();
+      this.panelPerfStats.set(panel, renderStats);
+    }
+    renderStats.addValue(renderTime);
+
+    const statW = 300;
+    ctx.fillStyle = 'hsl(97, 100%, 96%)';
+    ctx.fillRect(size.width - statW, size.height - 20, statW, 20);
+    ctx.fillStyle = 'hsla(122, 77%, 22%)';
+    const statStr = `Panel ${panelIndex + 1} | ` + runningStatStr(renderStats);
+    ctx.fillText(statStr, size.width - statW, size.height - 10);
+  }
+
+  private updatePerfStats(
+      renderTime: number, totalPanels: number, panelsOnCanvas: number) {
+    if (!perfDebug()) return;
+    this.perfStats.renderStats.addValue(renderTime);
+    this.perfStats.totalPanels = totalPanels;
+    this.perfStats.panelsOnCanvas = panelsOnCanvas;
+  }
+
+  renderPerfStats(index: number) {
+    assertTrue(perfDebug());
+    return [m(
+        'section',
+        m('div', `Panel Container ${index + 1}`),
+        m('div',
+          `${this.perfStats.totalPanels} panels, ` +
+              `${this.perfStats.panelsOnCanvas} on canvas.`),
+        m('div', runningStatStr(this.perfStats.renderStats)), )];
   }
 
   private getCanvasOverdrawHeightPerSide() {