Merge "trace_processor: Stop confusing upid with utid in systrace parsing"
diff --git a/buildtools/.gitignore b/buildtools/.gitignore
index 56cc991..1dd47b9 100644
--- a/buildtools/.gitignore
+++ b/buildtools/.gitignore
@@ -11,6 +11,7 @@
 libcxx/
 libcxxabi/
 libunwind/
+linenoise/
 linux/
 linux64/
 mac/
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index 33203c9..0415b3e 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -751,3 +751,22 @@
   configs -= [ "//gn/standalone:extra_warnings" ]
   public_configs = [ ":jsoncpp_config" ]
 }
+
+config("linenoise_config") {
+  cflags = [
+    # Using -isystem instead of include_dirs (-I), so we don't need to suppress
+    # warnings coming from third-party headers. Doing so would mask warnings in
+    # our own code.
+    "-isystem",
+    rebase_path("linenoise", root_build_dir),
+  ]
+}
+
+source_set("linenoise") {
+  sources = [
+    "linenoise/linenoise.c",
+    "linenoise/linenoise.h",
+  ]
+  configs -= [ "//gn/standalone:extra_warnings" ]
+  public_configs = [ ":linenoise_config" ]
+}
diff --git a/include/perfetto/base/build_config.h b/include/perfetto/base/build_config.h
index e083415..ef00b16 100644
--- a/include/perfetto/base/build_config.h
+++ b/include/perfetto/base/build_config.h
@@ -70,6 +70,13 @@
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_CHROMIUM_BUILD() 0
 #endif
 
+#if !defined(PERFETTO_BUILD_WITH_CHROMIUM) && \
+    !defined(PERFETTO_BUILD_WITH_ANDROID)
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_STANDALONE_BUILD() 1
+#else
+#define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_STANDALONE_BUILD() 0
+#endif
+
 #if defined(PERFETTO_START_DAEMONS_FOR_TESTING)
 #define PERFETTO_BUILDFLAG_DEFINE_PERFETTO_START_DAEMONS() 1
 #else
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 811443f..092c6bf 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -108,6 +108,9 @@
       "../../protos/perfetto/trace_processor:lite",
       "../base",
     ]
+    if (build_standalone) {
+      deps += [ "../../buildtools:linenoise" ]
+    }
     sources = [
       "trace_processor_shell.cc",
     ]
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index c485934..fd8af99 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -36,6 +36,10 @@
 #define PERFETTO_HAS_SIGNAL_H() 0
 #endif
 
+#if PERFETTO_BUILDFLAG(PERFETTO_STANDALONE_BUILD)
+#include <linenoise.h>
+#endif
+
 #if PERFETTO_HAS_SIGNAL_H()
 #include <signal.h>
 #endif
@@ -46,11 +50,45 @@
 namespace {
 TraceProcessor* g_tp;
 
-void PrintPrompt() {
-  printf("\r%80s\r> ", "");
-  fflush(stdout);
+#if PERFETTO_BUILDFLAG(PERFETTO_STANDALONE_BUILD)
+
+void SetupLineEditor() {
+  linenoiseSetMultiLine(true);
+  linenoiseHistorySetMaxLen(1000);
 }
 
+void FreeLine(char* line) {
+  linenoiseHistoryAdd(line);
+  linenoiseFree(line);
+}
+
+char* GetLine(const char* prompt) {
+  return linenoise(prompt);
+}
+
+#else
+
+void SetupLineEditor() {}
+
+void FreeLine(char* line) {
+  free(line);
+}
+
+char* GetLine(const char* prompt) {
+  printf("\r%80s\r%s", "", prompt);
+  fflush(stdout);
+  char* line = new char[1024];
+  if (!fgets(line, 1024 - 1, stdin)) {
+    FreeLine(line);
+    return nullptr;
+  }
+  if (strlen(line) > 0)
+    line[strlen(line) - 1] = 0;
+  return line;
+}
+
+#endif
+
 void OnQueryResult(base::TimeNanos t_start, const protos::RawQueryResult& res) {
   if (res.has_error()) {
     PERFETTO_ELOG("SQLite error: %s", res.error().c_str());
@@ -175,12 +213,13 @@
   signal(SIGINT, [](int) { g_tp->InterruptQuery(); });
 #endif
 
+  SetupLineEditor();
+
   for (;;) {
-    PrintPrompt();
-    char line[1024];
-    if (!fgets(line, sizeof(line) - 1, stdin) || strcmp(line, "q\n") == 0)
-      return 0;
-    if (strcmp(line, "\n") == 0)
+    char* line = GetLine("> ");
+    if (!line || strcmp(line, "q\n") == 0)
+      break;
+    if (strcmp(line, "") == 0)
       continue;
     protos::RawQueryArgs query;
     query.set_sql_query(line);
@@ -188,5 +227,9 @@
     g_tp->ExecuteQuery(query, [t_start](const protos::RawQueryResult& res) {
       OnQueryResult(t_start, res);
     });
+
+    FreeLine(line);
   }
+
+  return 0;
 }
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 5368905..d9512b5 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -178,6 +178,13 @@
    '4adec454b60a943dd58603d4be80d42b2db62cbd',
    'all',
   ),
+
+  # Linenoise
+  ('buildtools/linenoise',
+   'https://github.com/antirez/linenoise.git',
+   '4a961c0108720741e2683868eb10495f015ee422',
+   'all'
+  ),
 ]
 
 # Dependencies required to build Android code.
diff --git a/ui/src/assets/perfetto.scss b/ui/src/assets/perfetto.scss
index 1b5c774..cac3d75 100644
--- a/ui/src/assets/perfetto.scss
+++ b/ui/src/assets/perfetto.scss
@@ -321,3 +321,67 @@
   user-select: text;
   word-break: break-word;
 }
+
+.debug-panel-border {
+  position: absolute;
+  top: 0px;
+  height: 100%;
+  width: 100%;
+  border: 1px solid rgba(69, 187, 73, 0.5);
+  pointer-events: none;
+}
+
+.perf-stats {
+  --perfetto-orange: hsl(45, 100%, 48%);
+  --perfetto-red: hsl(6, 70%, 53%);
+  --stroke-color: hsl(217, 39%, 94%);
+  position: fixed;
+  bottom: 0;
+  color: var(--stroke-color);
+  font-family: monospace;
+  padding: 2px 0px;
+  z-index: 100;
+  button:hover {
+    color: var(--perfetto-red);
+  }
+  &[expanded=true] {
+    width: 600px;
+    background-color: rgba(27, 28, 29, 0.95);
+    button {
+      color: var(--perfetto-orange);
+      &:hover {
+        color: var(--perfetto-red);
+      }
+    }
+  }
+  &[expanded=false] {
+    width: var(--sidebar-width);
+    background-color: transparent;
+  }
+  i {
+    margin: 0px 24px;
+    font-size: 30px;
+  }
+  .perf-stats-content {
+    margin: 10px 24px;
+    & > section {
+      padding: 5px;
+      border-bottom: 1px solid var(--stroke-color);
+    }
+    button {
+      text-decoration: underline;
+    }
+    div {
+      margin: 2px 0px;
+    }
+    table, td, th {
+      border: 1px solid var(--stroke-color);
+      text-align: center;
+      padding: 4px;
+      margin: 4px 0px;
+    }
+    table {
+      border-collapse: collapse;
+    }
+  }
+}
diff --git a/ui/src/assets/topbar.scss b/ui/src/assets/topbar.scss
index 2ecd4ae..81a4a1b 100644
--- a/ui/src/assets/topbar.scss
+++ b/ui/src/assets/topbar.scss
@@ -180,4 +180,4 @@
             right: -90%;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index f38ee41..cbf444c 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -24,7 +24,8 @@
 
 // TrackController is a base class overridden by track implementations (e.g.,
 // sched slices, nestable slices, counters).
-export abstract class TrackController<Config = {}> extends Controller<'main'> {
+export abstract class TrackController<Config = {}, Data = {}> extends
+    Controller<'main'> {
   readonly trackId: string;
   readonly engine: Engine;
 
@@ -47,6 +48,10 @@
     return this.trackState.config as Config;
   }
 
+  publish(data: Data): void {
+    globals.publish('TrackData', {id: this.trackId, data});
+  }
+
   run() {
     const dataReq = this.trackState.dataReq;
     if (dataReq === undefined) return;
diff --git a/ui/src/frontend/frontend_local_state.ts b/ui/src/frontend/frontend_local_state.ts
index c295c43..b9aa740 100644
--- a/ui/src/frontend/frontend_local_state.ts
+++ b/ui/src/frontend/frontend_local_state.ts
@@ -27,6 +27,7 @@
   timeScale = new TimeScale(this.visibleWindowTime, [0, 0]);
   private _visibleTimeLastUpdate = 0;
   private pendingGlobalTimeUpdate?: TimeSpan;
+  perfDebug = false;
 
   // TODO: there is some redundancy in the fact that both |visibleWindowTime|
   // and a |timeScale| have a notion of time range. That should live in one
@@ -52,4 +53,9 @@
   get visibleTimeLastUpdate() {
     return this._visibleTimeLastUpdate;
   }
+
+  togglePerfDebug() {
+    this.perfDebug = !this.perfDebug;
+    globals.rafScheduler.scheduleFullRedraw();
+  }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 054df61..e370127 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -45,20 +45,24 @@
 class Globals {
   private _dispatch?: Dispatch = undefined;
   private _state?: State = undefined;
-  private _trackDataStore?: TrackDataStore = undefined;
-  private _queryResults?: QueryResultsStore = undefined;
   private _frontendLocalState?: FrontendLocalState = undefined;
   private _rafScheduler?: RafScheduler = undefined;
+
+  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
+  private _trackDataStore?: TrackDataStore = undefined;
+  private _queryResults?: QueryResultsStore = undefined;
   private _overviewStore?: OverviewStore = undefined;
   private _threadMap?: ThreadMap = undefined;
 
   initialize(dispatch?: Dispatch) {
     this._dispatch = dispatch;
     this._state = createEmptyState();
-    this._trackDataStore = new Map<string, {}>();
-    this._queryResults = new Map<string, {}>();
     this._frontendLocalState = new FrontendLocalState();
     this._rafScheduler = new RafScheduler();
+
+    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
+    this._trackDataStore = new Map<string, {}>();
+    this._queryResults = new Map<string, {}>();
     this._overviewStore = new Map<string, QuantizedLoad[]>();
     this._threadMap = new Map<number, ThreadDesc>();
   }
@@ -75,6 +79,15 @@
     return assertExists(this._dispatch);
   }
 
+  get frontendLocalState() {
+    return assertExists(this._frontendLocalState);
+  }
+
+  get rafScheduler() {
+    return assertExists(this._rafScheduler);
+  }
+
+  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
   get overviewStore(): OverviewStore {
     return assertExists(this._overviewStore);
   }
@@ -87,14 +100,6 @@
     return assertExists(this._queryResults);
   }
 
-  get frontendLocalState() {
-    return assertExists(this._frontendLocalState);
-  }
-
-  get rafScheduler() {
-    return assertExists(this._rafScheduler);
-  }
-
   get threads() {
     return assertExists(this._threadMap);
   }
@@ -102,11 +107,14 @@
   resetForTesting() {
     this._dispatch = undefined;
     this._state = undefined;
-    this._trackDataStore = undefined;
-    this._queryResults = undefined;
     this._frontendLocalState = undefined;
     this._rafScheduler = undefined;
+
+    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
+    this._trackDataStore = undefined;
+    this._queryResults = undefined;
     this._overviewStore = undefined;
+    this._threadMap = undefined;
   }
 }
 
diff --git a/ui/src/frontend/pages.ts b/ui/src/frontend/pages.ts
index d258656..a964952 100644
--- a/ui/src/frontend/pages.ts
+++ b/ui/src/frontend/pages.ts
@@ -27,10 +27,37 @@
       hash ? ['Permalink: ', m(`a[href=${url}]`, url)] : 'Uploading...');
 }
 
-const Alerts: m.Component = {
+class Alerts implements m.ClassComponent {
   view() {
     return m('.alerts', renderPermalink());
-  },
+  }
+}
+
+const TogglePerfDebugButton = {
+  view() {
+    return m(
+        '.perf-monitor-button',
+        m('button',
+          {
+            onclick: () => globals.frontendLocalState.togglePerfDebug(),
+          },
+          m('i.material-icons',
+            {
+              title: 'Toggle Perf Debug Mode',
+            },
+            'assessment')));
+  }
+};
+
+const PerfStats: m.Component = {
+  view() {
+    const perfDebug = globals.frontendLocalState.perfDebug;
+    const children = [m(TogglePerfDebugButton)];
+    if (perfDebug) {
+      children.unshift(m('.perf-stats-content'));
+    }
+    return m(`.perf-stats[expanded=${perfDebug}]`, children);
+  }
 };
 
 /**
@@ -44,6 +71,7 @@
         m(Topbar),
         m(component),
         m(Alerts),
+        m(PerfStats),
       ];
     },
   };
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() {
diff --git a/ui/src/frontend/perf.ts b/ui/src/frontend/perf.ts
new file mode 100644
index 0000000..6e4093b
--- /dev/null
+++ b/ui/src/frontend/perf.ts
@@ -0,0 +1,123 @@
+
+// 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.
+
+import * as m from 'mithril';
+
+import {globals} from './globals';
+import {PanelContainer} from './panel_container';
+
+/**
+ * Shorthand for if globals perf debug mode is on.
+ */
+export const perfDebug = () => globals.frontendLocalState.perfDebug;
+
+/**
+ * Returns performance.now() if perfDebug is enabled, otherwise 0.
+ * This is needed because calling performance.now is generally expensive
+ * and should not be done for every frame.
+ */
+export const debugNow = () => perfDebug() ? performance.now() : 0;
+
+/**
+ * Returns execution time of |fn| if perf debug mode is on. Returns 0 otherwise.
+ */
+export function measure(fn: () => void): number {
+  const start = debugNow();
+  fn();
+  return debugNow() - start;
+}
+
+/**
+ * Stores statistics about samples, and keeps a fixed size buffer of most recent
+ * samples.
+ */
+export class RunningStatistics {
+  private _count = 0;
+  private _mean = 0;
+  private _lastValue = 0;
+
+  private buffer: number[] = [];
+
+  constructor(private _maxBufferSize = 10) {}
+
+  addValue(value: number) {
+    this._lastValue = value;
+    this.buffer.push(value);
+    if (this.buffer.length > this._maxBufferSize) {
+      this.buffer.shift();
+    }
+    this._mean = (this._mean * this._count + value) / (this._count + 1);
+    this._count++;
+  }
+
+  get mean() {
+    return this._mean;
+  }
+  get count() {
+    return this._count;
+  }
+  get bufferMean() {
+    return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
+  }
+  get bufferSize() {
+    return this.buffer.length;
+  }
+  get maxBufferSize() {
+    return this._maxBufferSize;
+  }
+  get last() {
+    return this._lastValue;
+  }
+}
+
+/**
+ * Returns a summary string representation of a RunningStatistics object.
+ */
+export function runningStatStr(stat: RunningStatistics) {
+  return `Last: ${stat.last.toFixed(2)}ms | ` +
+      `Avg: ${stat.mean.toFixed(2)}ms | ` +
+      `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`;
+}
+
+/**
+ * Globals singleton class that renders performance stats for the whole app.
+ */
+class PerfDisplay {
+  private containers: PanelContainer[] = [];
+  addContainer(container: PanelContainer) {
+    this.containers.push(container);
+  }
+
+  removeContainer(container: PanelContainer) {
+    const i = this.containers.indexOf(container);
+    this.containers.splice(i, 1);
+  }
+
+  renderPerfStats() {
+    if (!perfDebug()) return;
+    const perfDisplayEl = this.getPerfDisplayEl();
+    if (!perfDisplayEl) return;
+    m.render(perfDisplayEl, [
+      m('section', globals.rafScheduler.renderPerfStats()),
+      this.containers.map((c, i) => m('section', c.renderPerfStats(i)))
+    ]);
+  }
+
+  getPerfDisplayEl() {
+    return document.querySelector('.perf-stats-content');
+  }
+}
+
+export const perfDisplay = new PerfDisplay();
diff --git a/ui/src/frontend/raf_scheduler.ts b/ui/src/frontend/raf_scheduler.ts
index 577ccca..bfe2017 100644
--- a/ui/src/frontend/raf_scheduler.ts
+++ b/ui/src/frontend/raf_scheduler.ts
@@ -12,6 +12,37 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import * as m from 'mithril';
+
+import {assertTrue} from '../base/logging';
+
+import {
+  debugNow,
+  measure,
+  perfDebug,
+  perfDisplay,
+  RunningStatistics,
+  runningStatStr
+} from './perf';
+
+function statTableHeader() {
+  return m(
+      'tr',
+      m('th', ''),
+      m('th', 'Last (ms)'),
+      m('th', 'Avg (ms)'),
+      m('th', 'Avg-10 (ms)'), );
+}
+
+function statTableRow(title: string, stat: RunningStatistics) {
+  return m(
+      'tr',
+      m('td', title),
+      m('td', stat.last.toFixed(2)),
+      m('td', stat.mean.toFixed(2)),
+      m('td', stat.bufferMean.toFixed(2)), );
+}
+
 export type ActionCallback = (nowMs: number) => void;
 export type RedrawCallback = (nowMs: number) => void;
 
@@ -31,6 +62,14 @@
   private requestedFullRedraw = false;
   private isRedrawing = false;
 
+  private perfStats = {
+    rafActions: new RunningStatistics(),
+    rafCanvas: new RunningStatistics(),
+    rafDom: new RunningStatistics(),
+    rafTotal: new RunningStatistics(),
+    domRedraw: new RunningStatistics(),
+  };
+
   start(cb: ActionCallback) {
     this.actionCallbacks.add(cb);
     this.maybeScheduleAnimationFrame();
@@ -61,11 +100,23 @@
     this.maybeScheduleAnimationFrame(true);
   }
 
+  syncDomRedraw(nowMs: number) {
+    const redrawStart = debugNow();
+    this._syncDomRedraw(nowMs);
+    if (perfDebug()) {
+      this.perfStats.domRedraw.addValue(debugNow() - redrawStart);
+    }
+  }
+
   private syncCanvasRedraw(nowMs: number) {
+    const redrawStart = debugNow();
     if (this.isRedrawing) return;
     this.isRedrawing = true;
     for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
     this.isRedrawing = false;
+    if (perfDebug()) {
+      this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
+    }
   }
 
   private maybeScheduleAnimationFrame(force = false) {
@@ -77,15 +128,64 @@
   }
 
   private onAnimationFrame(nowMs: number) {
+    const rafStart = debugNow();
     this.hasScheduledNextFrame = false;
 
     const doFullRedraw = this.requestedFullRedraw;
     this.requestedFullRedraw = false;
 
-    for (const action of this.actionCallbacks) action(nowMs);
-    if (doFullRedraw) this._syncDomRedraw(nowMs);
-    this.syncCanvasRedraw(nowMs);
+    const actionTime = measure(() => {
+      for (const action of this.actionCallbacks) action(nowMs);
+    });
+
+    const domTime = measure(() => {
+      if (doFullRedraw) this.syncDomRedraw(nowMs);
+    });
+    const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
+
+    const totalRafTime = debugNow() - rafStart;
+    this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
+    perfDisplay.renderPerfStats();
 
     this.maybeScheduleAnimationFrame();
   }
+
+  private updatePerfStats(
+      actionsTime: number, domTime: number, canvasTime: number,
+      totalRafTime: number) {
+    if (!perfDebug()) return;
+    this.perfStats.rafActions.addValue(actionsTime);
+    this.perfStats.rafDom.addValue(domTime);
+    this.perfStats.rafCanvas.addValue(canvasTime);
+    this.perfStats.rafTotal.addValue(totalRafTime);
+  }
+
+  renderPerfStats() {
+    assertTrue(perfDebug());
+    return m(
+        'div',
+        m('div',
+          [
+            m('button',
+              {onclick: () => this.scheduleRedraw()},
+              'Do Canvas Redraw'),
+            '   |   ',
+            m('button',
+              {onclick: () => this.scheduleFullRedraw()},
+              'Do Full Redraw'),
+          ]),
+        m('div',
+          'Raf Timing ' +
+              '(Total may not add up due to imprecision)'),
+        m('table',
+          statTableHeader(),
+          statTableRow('Actions', this.perfStats.rafActions),
+          statTableRow('Dom', this.perfStats.rafDom),
+          statTableRow('Canvas', this.perfStats.rafCanvas),
+          statTableRow('Total', this.perfStats.rafTotal), ),
+        m('div',
+          'Dom redraw: ' +
+              `Count: ${this.perfStats.domRedraw.count} | ` +
+              runningStatStr(this.perfStats.domRedraw)), );
+  }
 }
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index 694c8f5..79d8417 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -29,8 +29,12 @@
   }
 }
 
-const CodeSample: m.Component<{text: string}, {}> = {
-  view({attrs}) {
+interface CodeSampleAttrs {
+  text: string;
+}
+
+class CodeSample implements m.ClassComponent<CodeSampleAttrs> {
+  view({attrs}: m.CVnode<CodeSampleAttrs>) {
     return m(
         '.example-code',
         m('code', attrs.text),
@@ -39,8 +43,8 @@
             onclick: () => copyToClipboard(attrs.text),
           },
           'Copy to clipboard'), );
-  },
-};
+  }
+}
 
 export const RecordPage = createPage({
   view() {
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 15b9085..6f8e5d2 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -175,7 +175,7 @@
   globals.dispatch(createPermalink());
 }
 
-export const Sidebar: m.Component = {
+export class Sidebar implements m.ClassComponent {
   view() {
     const vdomSections = [];
     for (const section of SECTIONS) {
@@ -206,5 +206,5 @@
         m('header', 'Perfetto'),
         m('input[type=file]', {onchange: onInputElementFileSelectionChanged}),
         ...vdomSections);
-  },
-};
+  }
+}
diff --git a/ui/src/frontend/topbar.ts b/ui/src/frontend/topbar.ts
index a04d2c6..03b142a 100644
--- a/ui/src/frontend/topbar.ts
+++ b/ui/src/frontend/topbar.ts
@@ -89,13 +89,14 @@
 }
 
 
-const Omnibox: m.Component = {
-  oncreate(vnode) {
+class Omnibox implements m.ClassComponent {
+  oncreate(vnode: m.VnodeDOM) {
     const txt = vnode.dom.querySelector('input') as HTMLInputElement;
     txt.addEventListener('blur', clearOmniboxResults);
     txt.addEventListener('keydown', onKeyDown);
     txt.addEventListener('keyup', onKeyUp);
-  },
+  }
+
   view() {
     const msgTTL = globals.state.status.timestamp + 3 - Date.now() / 1e3;
     let enginesAreBusy = false;
@@ -131,10 +132,10 @@
         `.omnibox${commandMode ? '.command-mode' : ''}`,
         m(`input[placeholder=${placeholder[mode]}]`),
         m('.omnibox-results', results));
-  },
-};
+  }
+}
 
-export const Topbar: m.Component = {
+export class Topbar implements m.ClassComponent {
   view() {
     const progBar = [];
     const engine: EngineConfig = globals.state.engines['0'];
@@ -142,7 +143,6 @@
         (engine !== undefined && !engine.ready)) {
       progBar.push(m('.progress'));
     }
-
     return m('.topbar', m(Omnibox), ...progBar);
-  },
-};
+  }
+}
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 64dc3b3..c543ad8 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {TrackState} from '../common/state';
+import {globals} from './globals';
 
 /**
  * This interface forces track implementations to have some static properties.
@@ -32,7 +33,7 @@
 /**
  * The abstract class that needs to be implemented by all tracks.
  */
-export abstract class Track<Config = {}> {
+export abstract class Track<Config = {}, Data = {}> {
   /**
    * Receive data published by the TrackController of this track.
    */
@@ -43,6 +44,10 @@
     return this.trackState.config as Config;
   }
 
+  data(): Data {
+    return globals.trackDataStore.get(this.trackState.id) as Data;
+  }
+
   getHeight(): number {
     return 40;
   }
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index ccf6041..a12024d 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -32,8 +32,11 @@
   return globals.state.pinnedTracks.indexOf(id) !== -1;
 }
 
-const TrackShell = {
-  view({attrs}) {
+interface TrackShellAttrs {
+  trackState: TrackState;
+}
+class TrackShell implements m.ClassComponent<TrackShellAttrs> {
+  view({attrs}: m.CVnode<TrackShellAttrs>) {
     return m(
         '.track-shell',
         m('h1', attrs.trackState.name),
@@ -49,11 +52,14 @@
           action: toggleTrackPinned(attrs.trackState.id),
           i: isPinned(attrs.trackState.id) ? 'star' : 'star_border',
         }));
-  },
-} as m.Component<{trackState: TrackState}>;
+  }
+}
 
-const TrackContent = {
-  view({attrs}) {
+interface TrackContentAttrs {
+  track: Track;
+}
+class TrackContent implements m.ClassComponent<TrackContentAttrs> {
+  view({attrs}: m.CVnode<TrackContentAttrs>) {
     return m('.track-content', {
       onmousemove: (e: MouseEvent) => {
         attrs.track.onMouseMove({x: e.layerX, y: e.layerY});
@@ -65,19 +71,27 @@
       },
     }, );
   }
-} as m.Component<{track: Track}>;
+}
 
-const TrackComponent = {
-  view({attrs}) {
+interface TrackComponentAttrs {
+  trackState: TrackState;
+  track: Track;
+}
+class TrackComponent implements m.ClassComponent<TrackComponentAttrs> {
+  view({attrs}: m.CVnode<TrackComponentAttrs>) {
     return m('.track', [
       m(TrackShell, {trackState: attrs.trackState}),
       m(TrackContent, {track: attrs.track})
     ]);
   }
-} as m.Component<{trackState: TrackState, track: Track}>;
+}
 
-const TrackButton = {
-  view({attrs}) {
+interface TrackButtonAttrs {
+  action: Action;
+  i: string;
+}
+class TrackButton implements m.ClassComponent<TrackButtonAttrs> {
+  view({attrs}: m.CVnode<TrackButtonAttrs>) {
     return m(
         'i.material-icons.track-button',
         {
@@ -85,11 +99,7 @@
         },
         attrs.i);
   }
-} as m.Component<{
-  action: Action,
-  i: string,
-},
-                    {}>;
+}
 
 interface TrackPanelAttrs {
   id: string;
@@ -121,6 +131,7 @@
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
+    ctx.save();
     ctx.translate(TRACK_SHELL_WIDTH, 0);
     drawGridLines(
         ctx,
@@ -129,5 +140,6 @@
         size.height);
 
     this.track.renderCanvas(ctx);
+    ctx.restore();
   }
 }
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 94ee184..e801992 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -30,7 +30,7 @@
 
 const MAX_ZOOM_SPAN_SEC = 1e-4;  // 0.1 ms.
 
-const QueryTable: m.Component<{}, {}> = {
+class QueryTable implements m.ClassComponent {
   view() {
     const resp = globals.queryResults.get('command') as QueryResponse;
     if (resp === undefined) {
@@ -58,25 +58,23 @@
         resp.error ?
             m('.query-error', `SQL error: ${resp.error}`) :
             m('table.query-table', m('thead', header), m('tbody', rows)));
-  },
-};
+  }
+}
 
 /**
  * Top-most level component for the viewer page. Holds tracks, brush timeline,
  * panels, and everything else that's part of the main trace viewer page.
  */
-const TraceViewer = {
-  oninit() {
-    this.width = 0;
-  },
+class TraceViewer implements m.ClassComponent {
+  private onResize: () => void = () => {};
+  private zoomContent?: PanAndZoomHandler;
 
-  oncreate(vnode) {
+  oncreate(vnode: m.CVnodeDOM) {
     const frontendLocalState = globals.frontendLocalState;
     const updateDimensions = () => {
       const rect = vnode.dom.getBoundingClientRect();
-      this.width = rect.width;
       frontendLocalState.timeScale.setLimitsPx(
-          0, this.width - TRACK_SHELL_WIDTH);
+          0, rect.width - TRACK_SHELL_WIDTH);
     };
 
     updateDimensions();
@@ -124,12 +122,12 @@
             new TimeSpan(newStartSec, newEndSec));
       }
     });
-  },
+  }
 
   onremove() {
     window.removeEventListener('resize', this.onResize);
-    this.zoomContent.shutdown();
-  },
+    if (this.zoomContent) this.zoomContent.shutdown();
+  }
 
   view() {
     const scrollingPanels = globals.state.scrollingTracks.length > 0 ?
@@ -158,15 +156,8 @@
               doesScroll: true,
               panels: scrollingPanels,
             }))));
-  },
-
-} as m.Component<{}, {
-  onResize: () => void,
-  width: number,
-  zoomContent: PanAndZoomHandler,
-  overviewQueryExecuted: boolean,
-  overviewQueryResponse: QueryResponse,
-}>;
+  }
+}
 
 export const ViewerPage = createPage({
   view() {
diff --git a/ui/src/tracks/chrome_slices/common.ts b/ui/src/tracks/chrome_slices/common.ts
index b907735..1256dc7 100644
--- a/ui/src/tracks/chrome_slices/common.ts
+++ b/ui/src/tracks/chrome_slices/common.ts
@@ -14,13 +14,13 @@
 
 export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
 
-export interface ChromeSliceTrackConfig {
+export interface Config {
   maxDepth: number;
   upid: number;
   utid: number;
 }
 
-export interface ChromeSliceTrackData {
+export interface Data {
   start: number;
   end: number;
   resolution: number;
diff --git a/ui/src/tracks/chrome_slices/controller.ts b/ui/src/tracks/chrome_slices/controller.ts
index 7c0c814..186491a 100644
--- a/ui/src/tracks/chrome_slices/controller.ts
+++ b/ui/src/tracks/chrome_slices/controller.ts
@@ -13,20 +13,14 @@
 // limitations under the License.
 
 import {fromNs} from '../../common/time';
-import {globals} from '../../controller/globals';
 import {
   TrackController,
   trackControllerRegistry
 } from '../../controller/track_controller';
 
-import {
-  ChromeSliceTrackConfig,
-  ChromeSliceTrackData,
-  SLICE_TRACK_KIND
-} from './common';
+import {Config, Data, SLICE_TRACK_KIND} from './common';
 
-class ChromeSliceTrackController extends
-    TrackController<ChromeSliceTrackConfig> {
+class ChromeSliceTrackController extends TrackController<Config, Data> {
   static readonly kind = SLICE_TRACK_KIND;
   private busy = false;
 
@@ -55,7 +49,7 @@
 
       const numRows = +rawResult.numRecords;
 
-      const slices: ChromeSliceTrackData = {
+      const slices: Data = {
         start,
         end,
         resolution,
@@ -89,7 +83,7 @@
       if (numRows === LIMIT) {
         slices.end = slices.ends[slices.ends.length - 1];
       }
-      globals.publish('TrackData', {id: this.trackId, data: slices});
+      this.publish(slices);
     });
   }
 }
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index 6ff9499..aba1e7f 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -18,11 +18,7 @@
 import {Track} from '../../frontend/track';
 import {trackRegistry} from '../../frontend/track_registry';
 
-import {
-  ChromeSliceTrackConfig,
-  ChromeSliceTrackData,
-  SLICE_TRACK_KIND,
-} from './common';
+import {Config, Data, SLICE_TRACK_KIND} from './common';
 
 const SLICE_HEIGHT = 30;
 const TRACK_PADDING = 5;
@@ -42,7 +38,7 @@
   return Math.pow(10, Math.floor(Math.log10(resolution)));
 }
 
-class ChromeSliceTrack extends Track<ChromeSliceTrackConfig> {
+class ChromeSliceTrack extends Track<Config, Data> {
   static readonly kind = SLICE_TRACK_KIND;
   static create(trackState: TrackState): ChromeSliceTrack {
     return new ChromeSliceTrack(trackState);
@@ -69,27 +65,27 @@
     // TODO: fonts and colors should come from the CSS and not hardcoded here.
 
     const {timeScale, visibleWindowTime} = globals.frontendLocalState;
-    const trackData = this.trackData;
+    const data = this.data();
 
-    // If there aren't enough cached slices data in |trackData| request more to
+    // If there aren't enough cached slices data in |data| request more to
     // the controller.
-    const inRange = trackData !== undefined &&
-        (visibleWindowTime.start >= trackData.start &&
-         visibleWindowTime.end <= trackData.end);
-    if (!inRange || trackData.resolution > getCurResolution()) {
+    const inRange = data !== undefined &&
+        (visibleWindowTime.start >= data.start &&
+         visibleWindowTime.end <= data.end);
+    if (!inRange || data.resolution > getCurResolution()) {
       if (!this.reqPending) {
         this.reqPending = true;
         setTimeout(() => this.reqDataDeferred(), 50);
       }
-      if (trackData === undefined) return;  // Can't possibly draw anything.
+      if (data === undefined) return;  // Can't possibly draw anything.
     }
 
     // If the cached trace slices don't fully cover the visible time range,
     // show a gray rectangle with a "Loading..." label.
     ctx.font = '12px Google Sans';
-    if (trackData.start > visibleWindowTime.start) {
+    if (data.start > visibleWindowTime.start) {
       const rectWidth =
-          timeScale.timeToPx(Math.min(trackData.start, visibleWindowTime.end));
+          timeScale.timeToPx(Math.min(data.start, visibleWindowTime.end));
       ctx.fillStyle = '#eee';
       ctx.fillRect(0, TRACK_PADDING, rectWidth, SLICE_HEIGHT);
       ctx.fillStyle = '#666';
@@ -99,9 +95,9 @@
           TRACK_PADDING + SLICE_HEIGHT / 2,
           rectWidth);
     }
-    if (trackData.end < visibleWindowTime.end) {
+    if (data.end < visibleWindowTime.end) {
       const rectX =
-          timeScale.timeToPx(Math.max(trackData.end, visibleWindowTime.start));
+          timeScale.timeToPx(Math.max(data.end, visibleWindowTime.start));
       const rectWidth = timeScale.timeToPx(visibleWindowTime.end) - rectX;
       ctx.fillStyle = '#eee';
       ctx.fillRect(rectX, TRACK_PADDING, rectWidth, SLICE_HEIGHT);
@@ -120,13 +116,13 @@
     const charWidth = ctx.measureText('abcdefghij').width / 10;
     const pxEnd = timeScale.timeToPx(visibleWindowTime.end);
 
-    for (let i = 0; i < trackData.starts.length; i++) {
-      const tStart = trackData.starts[i];
-      const tEnd = trackData.ends[i];
-      const depth = trackData.depths[i];
-      const cat = trackData.strings[trackData.categories[i]];
-      const titleId = trackData.titles[i];
-      const title = trackData.strings[titleId];
+    for (let i = 0; i < data.starts.length; i++) {
+      const tStart = data.starts[i];
+      const tEnd = data.ends[i];
+      const depth = data.depths[i];
+      const cat = data.strings[data.categories[i]];
+      const titleId = data.titles[i];
+      const title = data.strings[titleId];
       if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
         continue;
       }
@@ -162,18 +158,18 @@
   }
 
   onMouseMove({x, y}: {x: number, y: number}) {
-    const trackData = this.trackData;
+    const data = this.data();
     this.hoveredTitleId = -1;
-    if (trackData === undefined) return;
+    if (data === undefined) return;
     const {timeScale} = globals.frontendLocalState;
     if (y < TRACK_PADDING) return;
     const t = timeScale.pxToTime(x);
     const depth = Math.floor(y / SLICE_HEIGHT);
-    for (let i = 0; i < trackData.starts.length; i++) {
-      const tStart = trackData.starts[i];
-      const tEnd = trackData.ends[i];
-      const titleId = trackData.titles[i];
-      if (tStart <= t && t <= tEnd && depth === trackData.depths[i]) {
+    for (let i = 0; i < data.starts.length; i++) {
+      const tStart = data.starts[i];
+      const tEnd = data.ends[i];
+      const titleId = data.titles[i];
+      if (tStart <= t && t <= tEnd && depth === data.depths[i]) {
         this.hoveredTitleId = titleId;
         break;
       }
@@ -187,11 +183,6 @@
   getHeight() {
     return SLICE_HEIGHT * (this.config.maxDepth + 1) + 2 * TRACK_PADDING;
   }
-
-  private get trackData(): ChromeSliceTrackData {
-    return globals.trackDataStore.get(this.trackState.id) as
-        ChromeSliceTrackData;
-  }
 }
 
 trackRegistry.register(ChromeSliceTrack);
diff --git a/ui/src/tracks/cpu_slices/common.ts b/ui/src/tracks/cpu_slices/common.ts
index c6802b4..38844f1 100644
--- a/ui/src/tracks/cpu_slices/common.ts
+++ b/ui/src/tracks/cpu_slices/common.ts
@@ -14,7 +14,7 @@
 
 export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
 
-export interface CpuSliceTrackData {
+export interface Data {
   start: number;
   end: number;
   resolution: number;
@@ -25,4 +25,4 @@
   utids: Uint32Array;
 }
 
-export interface CpuSliceTrackConfig { cpu: number; }
+export interface Config { cpu: number; }
diff --git a/ui/src/tracks/cpu_slices/controller.ts b/ui/src/tracks/cpu_slices/controller.ts
index 2519caa..17a1b0f 100644
--- a/ui/src/tracks/cpu_slices/controller.ts
+++ b/ui/src/tracks/cpu_slices/controller.ts
@@ -13,19 +13,14 @@
 // limitations under the License.
 
 import {fromNs} from '../../common/time';
-import {globals} from '../../controller/globals';
 import {
   TrackController,
   trackControllerRegistry
 } from '../../controller/track_controller';
 
-import {
-  CPU_SLICE_TRACK_KIND,
-  CpuSliceTrackConfig,
-  CpuSliceTrackData
-} from './common';
+import {Config, CPU_SLICE_TRACK_KIND, Data} from './common';
 
-class CpuSliceTrackController extends TrackController<CpuSliceTrackConfig> {
+class CpuSliceTrackController extends TrackController<Config, Data> {
   static readonly kind = CPU_SLICE_TRACK_KIND;
   private busy = false;
 
@@ -50,7 +45,7 @@
       }
       const numRows = +rawResult.numRecords;
 
-      const slices: CpuSliceTrackData = {
+      const slices: Data = {
         start,
         end,
         resolution,
@@ -69,7 +64,7 @@
       if (numRows === LIMIT) {
         slices.end = slices.ends[slices.ends.length - 1];
       }
-      globals.publish('TrackData', {id: this.trackId, data: slices});
+      this.publish(slices);
     });
   }
 }
diff --git a/ui/src/tracks/cpu_slices/frontend.ts b/ui/src/tracks/cpu_slices/frontend.ts
index cdfb1c6..34b84d5 100644
--- a/ui/src/tracks/cpu_slices/frontend.ts
+++ b/ui/src/tracks/cpu_slices/frontend.ts
@@ -19,11 +19,7 @@
 import {Track} from '../../frontend/track';
 import {trackRegistry} from '../../frontend/track_registry';
 
-import {
-  CPU_SLICE_TRACK_KIND,
-  CpuSliceTrackConfig,
-  CpuSliceTrackData
-} from './common';
+import {Config, CPU_SLICE_TRACK_KIND, Data} from './common';
 
 const MARGIN_TOP = 5;
 const RECT_HEIGHT = 30;
@@ -50,7 +46,7 @@
   return Math.pow(10, Math.floor(Math.log10(resolution)));
 }
 
-class CpuSliceTrack extends Track<CpuSliceTrackConfig> {
+class CpuSliceTrack extends Track<Config, Data> {
   static readonly kind = CPU_SLICE_TRACK_KIND;
   static create(trackState: TrackState): CpuSliceTrack {
     return new CpuSliceTrack(trackState);
@@ -78,19 +74,19 @@
     // TODO: fonts and colors should come from the CSS and not hardcoded here.
 
     const {timeScale, visibleWindowTime} = globals.frontendLocalState;
-    const trackData = this.trackData;
+    const data = this.data();
 
-    // If there aren't enough cached slices data in |trackData| request more to
+    // If there aren't enough cached slices data in |data| request more to
     // the controller.
-    const inRange = trackData !== undefined &&
-        (visibleWindowTime.start >= trackData.start &&
-         visibleWindowTime.end <= trackData.end);
-    if (!inRange || trackData.resolution > getCurResolution()) {
+    const inRange = data !== undefined &&
+        (visibleWindowTime.start >= data.start &&
+         visibleWindowTime.end <= data.end);
+    if (!inRange || data.resolution > getCurResolution()) {
       if (!this.reqPending) {
         this.reqPending = true;
         setTimeout(() => this.reqDataDeferred(), 50);
       }
-      if (trackData === undefined) return;  // Can't possibly draw anything.
+      if (data === undefined) return;  // Can't possibly draw anything.
     }
     ctx.textAlign = 'center';
     ctx.font = '12px Google Sans';
@@ -103,18 +99,18 @@
     // If the cached trace slices don't fully cover the visible time range,
     // show a gray rectangle with a "Loading..." label.
     ctx.font = '12px Google Sans';
-    if (trackData.start > visibleWindowTime.start) {
+    if (data.start > visibleWindowTime.start) {
       const rectWidth =
-          timeScale.timeToPx(Math.min(trackData.start, visibleWindowTime.end));
+          timeScale.timeToPx(Math.min(data.start, visibleWindowTime.end));
       ctx.fillStyle = '#eee';
       ctx.fillRect(0, MARGIN_TOP, rectWidth, RECT_HEIGHT);
       ctx.fillStyle = '#666';
       ctx.fillText(
           'loading...', rectWidth / 2, MARGIN_TOP + RECT_HEIGHT / 2, rectWidth);
     }
-    if (trackData.end < visibleWindowTime.end) {
+    if (data.end < visibleWindowTime.end) {
       const rectX =
-          timeScale.timeToPx(Math.max(trackData.end, visibleWindowTime.start));
+          timeScale.timeToPx(Math.max(data.end, visibleWindowTime.start));
       const rectWidth = timeScale.timeToPx(visibleWindowTime.end) - rectX;
       ctx.fillStyle = '#eee';
       ctx.fillRect(rectX, MARGIN_TOP, rectWidth, RECT_HEIGHT);
@@ -126,12 +122,12 @@
           rectWidth);
     }
 
-    assertTrue(trackData.starts.length === trackData.ends.length);
-    assertTrue(trackData.starts.length === trackData.utids.length);
-    for (let i = 0; i < trackData.starts.length; i++) {
-      const tStart = trackData.starts[i];
-      const tEnd = trackData.ends[i];
-      const utid = trackData.utids[i];
+    assertTrue(data.starts.length === data.ends.length);
+    assertTrue(data.starts.length === data.utids.length);
+    for (let i = 0; i < data.starts.length; i++) {
+      const tStart = data.starts[i];
+      const tEnd = data.ends[i];
+      const utid = data.utids[i];
       if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
         continue;
       }
@@ -189,9 +185,9 @@
   }
 
   onMouseMove({x, y}: {x: number, y: number}) {
-    const trackData = this.trackData;
+    const data = this.data();
     this.mouseXpos = x;
-    if (trackData === undefined) return;
+    if (data === undefined) return;
     const {timeScale} = globals.frontendLocalState;
     if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
       this.hoveredUtid = -1;
@@ -200,10 +196,10 @@
     const t = timeScale.pxToTime(x);
     this.hoveredUtid = -1;
 
-    for (let i = 0; i < trackData.starts.length; i++) {
-      const tStart = trackData.starts[i];
-      const tEnd = trackData.ends[i];
-      const utid = trackData.utids[i];
+    for (let i = 0; i < data.starts.length; i++) {
+      const tStart = data.starts[i];
+      const tEnd = data.ends[i];
+      const utid = data.utids[i];
       if (tStart <= t && t <= tEnd) {
         this.hoveredUtid = utid;
         break;
@@ -215,10 +211,6 @@
     this.hoveredUtid = -1;
     this.mouseXpos = 0;
   }
-
-  private get trackData(): CpuSliceTrackData {
-    return globals.trackDataStore.get(this.trackState.id) as CpuSliceTrackData;
-  }
 }
 
 trackRegistry.register(CpuSliceTrack);