Update to latest trace viewer

Upstream trace viewer has changed substantially since last pull.

First, the old flattening into js + css + html workflow has been replaced with
a new flatten into single html file workflow.

Second, trace viewer has moved to git.

Some pieces that were previously only in systrace are now upstream as well.
In particular, minification is now upstream. Thus, the minifying features in
systrace can be removed.

Change-Id: Ibc6a46fa3dccff8b771a95aae1909cf178157264
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_link.html b/trace-viewer/trace_viewer/core/analysis/analysis_link.html
new file mode 100644
index 0000000..89adbc0
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_link.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/base/ui.html">
+
+<polymer-element name="tv-c-analysis-link" is="a">
+  <template>
+    <style>
+    :host {
+      display: inline;
+      color: -webkit-link;
+      cursor: pointer;
+      text-decoration: underline;
+      /* TODO(nduca): Whitespace is forced to normal here because the
+         analysis_results.css forces everything under it to pre. This is insane.
+         When that horrible evil class dies, then we can rip this white-space
+         restriction out.
+       */
+      white-space: normal;
+      cursor: pointer;
+    }
+    </style>
+    <content></content>
+  </template>
+  <script>
+  'use strict';
+  Polymer({
+    ready: function() {
+      this.addEventListener('click', this.onClicked_.bind(this));
+      this.selection_ = undefined;
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      this.selection_ = selection;
+      this.textContent = selection.userFriendlyName;
+    },
+
+    setSelectionAndContent: function(selection, opt_textContent) {
+      this.selection_ = selection;
+      if (opt_textContent)
+        this.textContent = opt_textContent;
+    },
+
+    onClicked_: function() {
+      if (!this.selection_)
+        return;
+
+      var event = new tv.c.RequestSelectionChangeEvent();
+      if (typeof this.selection_ === 'function')
+        event.selection = this.selection_();
+      else
+        event.selection = this.selection_;
+      this.dispatchEvent(event);
+    }
+  });
+  </script>
+</polymer-element>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_link_test.html b/trace-viewer/trace_viewer/core/analysis/analysis_link_test.html
new file mode 100644
index 0000000..7aca7cc
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_link_test.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('testBasic', function() {
+    var link = document.createElement('tv-c-analysis-link');
+
+    var i10 = new tv.c.trace_model.ObjectInstance(
+    {}, '0x1000', 'cat', 'name', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+
+    link.selection = new tv.c.Selection(s10);
+    this.addHTMLOutput(link);
+
+    var didRSC = false;
+    link.addEventListener('requestSelectionChange', function(e) {
+      didRSC = true;
+      assert.equal(e.selection[0], s10);
+    });
+    link.click();
+    assert.isTrue(didRSC);
+  });
+
+  test('testGeneratorVersion', function() {
+    var link = document.createElement('tv-c-analysis-link');
+
+    var i10 = new tv.c.trace_model.ObjectInstance(
+    {}, '0x1000', 'cat', 'name', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+
+    function selectionGenerator() {
+      return new tv.c.Selection(s10);
+    }
+    selectionGenerator.userFriendlyName = 'hello world';
+    link.selection = selectionGenerator;
+    this.addHTMLOutput(link);
+
+    var didRSC = false;
+    link.addEventListener('requestSelectionChange', function(e) {
+      assert.equal(e.selection[0], s10);
+      didRSC = true;
+    });
+    link.click();
+    assert.isTrue(didRSC);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_results.css b/trace-viewer/trace_viewer/core/analysis/analysis_results.css
new file mode 100644
index 0000000..1a62480
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_results.css
@@ -0,0 +1,72 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.analysis-header {
+  font-weight: bold;
+}
+
+.analysis-results {
+  font-family: monospace;
+  white-space: pre;
+}
+
+.analysis-results * {
+  -webkit-user-select: text !important;
+  cursor: text;
+}
+
+.analysis-table {
+  border-collapse: collapse;
+  border-width: 0;
+  margin-bottom: 25px;
+  width: 100%;
+}
+
+.analysis-table tr > td:first-child {
+  padding-left: 2px;
+}
+
+.analysis-table tr > td {
+  padding: 2px 4px 2px 4px;
+  vertical-align: text-top;
+  width: 150px;
+}
+
+/* Shrink back nested cells (used to display Args) */
+.analysis-table td td {
+  padding: 0 0 0 0;
+  width: auto;
+}
+
+.analysis-table-header {
+  text-align: left;
+}
+
+.analysis-table-row {
+  vertical-align: top;
+}
+
+.analysis-table-row:nth-child(2n+0) {
+  background-color: #e2e2e2;
+}
+
+.analysis-table-row-inverted:nth-child(2n+1) {
+  background-color: #e2e2e2;
+}
+
+.selection-changing-link {
+  color: -webkit-link;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+.analysis-table thead {
+  background-color: #e2e2e2;
+  font-weight: bold;
+}
+
+.analysis-table tfoot {
+  font-weight: bold;
+}
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_results.html b/trace-viewer/trace_viewer/core/analysis/analysis_results.html
new file mode 100644
index 0000000..c604824
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_results.html
@@ -0,0 +1,420 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="stylesheet" href="/core/analysis/analysis_results.css">
+
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.analysis', function() {
+  var AnalysisResults = tv.b.ui.define('div');
+
+  AnalysisResults.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function() {
+      this.className = 'analysis-results';
+    },
+
+    get requiresTallView() {
+      return true;
+    },
+
+    clear: function() {
+      this.textContent = '';
+    },
+
+    createSelectionChangingLink: function(text, selectionGenerator,
+                                          opt_tooltip) {
+      var el = this.ownerDocument.createElement('tv-c-analysis-link');
+      function wrap() {
+        return selectionGenerator();
+      }
+      wrap.userFriendlyName = text;
+      el.selection = wrap;
+      if (opt_tooltip)
+        el.title = opt_tooltip;
+      return el;
+    },
+
+    appendElement_: function(parent, tagName, opt_text) {
+      var n = parent.ownerDocument.createElement(tagName);
+      parent.appendChild(n);
+      if (opt_text != undefined)
+        n.textContent = opt_text;
+      return n;
+    },
+
+    appendText_: function(parent, text) {
+      var textElement = parent.ownerDocument.createTextNode(text);
+      parent.appendChild(textNode);
+      return textNode;
+    },
+
+    appendTableCell_: function(table, row, cellnum, text, opt_warning) {
+      var td = this.appendElement_(row, 'td', text);
+      td.className = table.className + '-col-' + cellnum;
+      if (opt_warning) {
+        var span = document.createElement('span');
+        span.textContent = ' ' + String.fromCharCode(9888);
+        span.title = opt_warning;
+        td.appendChild(span);
+      }
+      return td;
+    },
+
+    /**
+     * Creates and append a table cell at the end of the given row.
+     */
+    appendTableCell: function(table, row, text) {
+      return this.appendTableCell_(table, row, row.children.length, text);
+    },
+
+    appendTableCellWithTooltip_: function(table, row, cellnum, text, tooltip) {
+      if (tooltip) {
+        var td = this.appendElement_(row, 'td');
+        td.className = table.className + '-col-' + cellnum;
+        var span = this.appendElement_(td, 'span', text);
+        span.className = 'tooltip';
+        span.title = tooltip;
+        return td;
+      } else {
+        return this.appendTableCell_(table, row, cellnum, text);
+      }
+    },
+
+    /**
+     * Creates and appends a section header element.
+     */
+    appendHeader: function(label) {
+      var header = this.appendElement_(this, 'span', label);
+      header.className = 'analysis-header';
+      return header;
+    },
+
+    /**
+     * Creates and appends a info element of the format "<b>label</b>value".
+     */
+    appendInfo: function(label, value) {
+      var div = this.appendElement_(this, 'div');
+      div.label = this.appendElement_(div, 'b', label);
+      div.value = this.appendElement_(div, 'span', value);
+      return div;
+    },
+
+    /**
+     * Adds a table with the given className.
+     *
+     * @return {HTMLTableElement} The newly created table.
+     */
+    appendTable: function(className, numColumns) {
+      var table = this.appendElement_(this, 'table');
+      table.className = className + ' analysis-table';
+      table.numColumns = numColumns;
+      return table;
+    },
+
+    /**
+     * Creates and appends a |tr| in |thead|, if |thead| does not exist, create
+     * it as well.
+     */
+    appendHeadRow: function(table) {
+      if (table.headerRow)
+        throw new Error('Only one header row allowed.');
+      if (table.tbody || table.tfoot)
+        throw new Error(
+            'Cannot add a header row after data rows have been added.');
+      table.headerRow = this.appendElement_(
+                                  this.appendElement_(table, 'thead'), 'tr');
+      table.headerRow.className = 'analysis-table-header';
+      return table.headerRow;
+    },
+
+    /**
+     * Creates and appends a |tr| in |tbody|, if |tbody| does not exist, create
+     * it as well.
+     */
+    appendBodyRow: function(table) {
+      if (table.tfoot)
+        throw new Error(
+            'Cannot add a tbody row after footer rows have been added.');
+      if (!table.tbody)
+        table.tbody = this.appendElement_(table, 'tbody');
+      var row = this.appendElement_(table.tbody, 'tr');
+      if (table.headerRow)
+        row.className = 'analysis-table-row';
+      else
+        row.className = 'analysis-table-row-inverted';
+      return row;
+    },
+
+    /**
+     * Creates and appends a |tr| in |tfoot|, if |tfoot| does not exist, create
+     * it as well.
+     */
+    appendFootRow: function(table) {
+      if (!table.tfoot) {
+        table.tfoot = this.appendElement_(table, 'tfoot');
+        table.tfoot.rowsClassName = (
+            (table.headerRow ? 1 : 0) +
+            (table.tbody ? table.tbody.rows.length : 0)) % 2 ?
+                'analysis-table-row' : 'analysis-table-row-inverted';
+      }
+
+      var row = this.appendElement_(table.tfoot, 'tr');
+      row.className = table.tfoot.rowsClassName;
+      return row;
+    },
+
+    /**
+     * Adds a spacing row to spread out results.
+     */
+    appendSpacingRow: function(table, opt_inFoot) {
+      if (table.tfoot || opt_inFoot)
+        var row = this.appendFootRow(table);
+      else
+        var row = this.appendBodyRow(table);
+      for (var i = 0; i < table.numColumns; i++)
+        this.appendTableCell_(table, row, i, ' ');
+    },
+
+    /**
+     * Creates and appends a row to |table| with a left-aligned |label] in the
+     * first column and an optional |opt_value| in the second column.
+     */
+    appendInfoRow: function(table, label, opt_value, opt_inFoot) {
+      if (table.tfoot || opt_inFoot)
+        var row = this.appendFootRow(table);
+      else
+        var row = this.appendBodyRow(table);
+      this.appendTableCell_(table, row, 0, label);
+      if (opt_value !== undefined) {
+        var objectView =
+            document.createElement('tv-c-analysis-generic-object-view');
+        objectView.object = opt_value;
+        objectView.classList.add('analysis-table-col-1');
+        objectView.style.display = 'table-cell';
+        row.appendChild(objectView);
+      } else {
+        this.appendTableCell_(table, row, 1, '');
+      }
+      for (var i = 2; i < table.numColumns; i++)
+        this.appendTableCell_(table, row, i, '');
+    },
+
+    /**
+     * Creates and appends a row to |table| with a left-aligned |label] in the
+     * first column and a millisecond |time| value in the second column.
+     */
+    appendInfoRowTime: function(table, label, time, opt_inFoot, opt_warning) {
+      if (table.tfoot || opt_inFoot)
+        var row = this.appendFootRow(table);
+      else
+        var row = this.appendBodyRow(table);
+      this.appendTableCell_(table, row, 0, label);
+      this.appendTableCell_(
+          table, row, 1, tv.c.analysis.tsString(time), opt_warning);
+    },
+
+    /**
+     * Creates and appends a row to |table| that summarizes a single slice or a
+     * single counter. The row has a left-aligned |start| in the first column,
+     * the |duration| of the data in the second, the number of |occurrences| in
+     * the third.
+     *
+     * @param {object=} opt_statistics May be undefined, or an object which
+     *          contains calculated statistics containing min/max/avg for
+     *          slices, or min/max/avg/start/end for counters.
+     */
+    appendDetailsRow: function(table, start, duration, selfTime, args,
+        opt_selectionGenerator, opt_cpuDuration, opt_inFoot) {
+      if (opt_inFoot) {
+        // If inFoot is true, then we're reporting Totals.
+        var row = this.appendFootRow(table);
+        this.appendTableCell(table, row, 'Totals');
+      }
+      else {
+        var row = this.appendBodyRow(table);
+
+        if (opt_selectionGenerator) {
+          var labelEl = this.appendTableCell(table, row,
+                                             tv.c.analysis.tsString(start));
+          labelEl.textContent = '';
+          labelEl.appendChild(this.createSelectionChangingLink(
+                                      tv.c.analysis.tsString(start),
+                                      opt_selectionGenerator, ''));
+        } else {
+          this.appendTableCell(table, row, tv.c.analysis.tsString(start));
+        }
+      }
+      if (duration !== null)
+        this.appendTableCell(table, row, tv.c.analysis.tsString(duration));
+
+      if (opt_cpuDuration)
+        this.appendTableCell(table, row,
+                             opt_cpuDuration != '' ?
+                             tv.c.analysis.tsString(opt_cpuDuration) :
+                             '');
+
+      if (selfTime !== null)
+        this.appendTableCell(table, row, tv.c.analysis.tsString(selfTime));
+
+      var argsCell = this.appendTableCell(table, row, '');
+      var n = 0;
+      for (var argName in args) {
+        n += 1;
+      }
+
+      if (n > 0) {
+        for (var argName in args) {
+          var argVal = args[argName];
+          var objectView =
+              document.createElement('tv-c-analysis-generic-object-view');
+          objectView.object = argVal;
+          var argsRow = this.appendElement_(
+              this.appendElement_(argsCell, 'table'), 'tr');
+          this.appendElement_(argsRow, 'td', argName + ':');
+          this.appendElement_(argsRow, 'td').appendChild(objectView);
+        }
+      }
+    },
+
+    /**
+     * Creates and appends a row to |table| that summarizes one or more slices,
+     * or one or more counters. The row has a left-aligned |label| in the first
+     * column, the |duration| of the data in the second, the number of
+     * |occurrences| in the third.
+     *
+     * @param {object=} opt_statistics May be undefined, or an object which
+     *          contains calculated statistics containing min/max/avg for
+     *          slices, or min/max/avg/start/end for counters.
+     */
+    appendDataRow: function(table, label, opt_duration, opt_cpuDuration,
+                            opt_selfTime, opt_cpuSelfTime, opt_occurrences,
+                            opt_percentage, opt_statistics,
+                            opt_selectionGenerator, opt_inFoot) {
+
+      var tooltip = undefined;
+      if (opt_statistics) {
+        tooltip = 'Min Duration:\u0009' +
+                  tv.c.analysis.tsString(opt_statistics.min) +
+                  ' ms \u000DMax Duration:\u0009' +
+                  tv.c.analysis.tsString(opt_statistics.max) +
+                  ' ms \u000DAvg Duration:\u0009' +
+                  tv.c.analysis.tsString(opt_statistics.avg) +
+                  ' ms (\u03C3 = ' +
+                  tv.c.analysis.tsRound(opt_statistics.avg_stddev) + ')';
+
+        if (opt_statistics.start) {
+          tooltip += '\u000DStart Time:\u0009' +
+              tv.c.analysis.tsString(opt_statistics.start);
+        }
+        if (opt_statistics.end) {
+          tooltip += '\u000DEnd Time:\u0009' +
+              tv.c.analysis.tsString(opt_statistics.end);
+        }
+        if (opt_statistics.frequency && opt_statistics.frequency_stddev) {
+          tooltip += '\u000DFrequency:\u0009' +
+              tv.c.analysis.tsRound(opt_statistics.frequency) +
+              ' occurrences/s (\u03C3 = ' +
+              tv.c.analysis.tsRound(opt_statistics.frequency_stddev) + ')';
+        }
+      }
+
+      if (table.tfoot || opt_inFoot)
+        var row = this.appendFootRow(table);
+      else
+        var row = this.appendBodyRow(table);
+
+      var cellNum = 0;
+      if (!opt_selectionGenerator) {
+        this.appendTableCellWithTooltip_(table, row, cellNum, label, tooltip);
+      } else {
+        var labelEl = this.appendTableCellWithTooltip_(
+            table, row, cellNum, label, tooltip);
+        if (labelEl) {
+          labelEl.textContent = '';
+          labelEl.appendChild(
+              this.createSelectionChangingLink(label, opt_selectionGenerator,
+                                               tooltip));
+        }
+      }
+      cellNum++;
+
+      if (opt_duration !== null) {
+        if (opt_duration) {
+          if (opt_duration instanceof Array) {
+            this.appendTableCellWithTooltip_(table, row, cellNum,
+                '[' + opt_duration.join(', ') + ']', tooltip);
+          } else {
+            this.appendTableCellWithTooltip_(table, row, cellNum,
+                tv.c.analysis.tsString(opt_duration), tooltip);
+          }
+        } else {
+          this.appendTableCell_(table, row, cellNum, '');
+        }
+        cellNum++;
+      }
+
+      if (opt_cpuDuration !== null) {
+        if (opt_cpuDuration != '') {
+          this.appendTableCellWithTooltip_(table, row, cellNum,
+              tv.c.analysis.tsString(opt_cpuDuration), tooltip);
+        } else {
+          this.appendTableCell_(table, row, cellNum, '');
+        }
+        cellNum++;
+      }
+
+      if (opt_selfTime !== null) {
+        if (opt_selfTime) {
+          this.appendTableCellWithTooltip_(table, row, cellNum,
+              tv.c.analysis.tsString(opt_selfTime), tooltip);
+        } else {
+          this.appendTableCell_(table, row, cellNum, '');
+        }
+        cellNum++;
+      }
+
+      if (opt_cpuSelfTime !== null) {
+        if (opt_cpuSelfTime) {
+          this.appendTableCellWithTooltip_(table, row, cellNum,
+              tv.c.analysis.tsString(opt_cpuSelfTime), tooltip);
+        } else {
+          this.appendTableCell_(table, row, cellNum, '');
+        }
+        cellNum++;
+      }
+
+      if (opt_percentage !== null) {
+        if (opt_percentage) {
+          this.appendTableCellWithTooltip_(table, row, cellNum,
+                                           opt_percentage, tooltip);
+        } else {
+          this.appendTableCell_(table, row, cellNum, '');
+        }
+        cellNum++;
+      }
+
+      if (opt_occurrences) {
+        this.appendTableCellWithTooltip_(table, row, cellNum,
+            String(opt_occurrences), tooltip);
+      } else {
+        this.appendTableCell_(table, row, cellNum, '');
+      }
+      cellNum++;
+    }
+  };
+  return {
+    AnalysisResults: AnalysisResults
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_results_test.html b/trace-viewer/trace_viewer/core/analysis/analysis_results_test.html
new file mode 100644
index 0000000..44f2503
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_results_test.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/analysis/stub_analysis_table.html">
+<link rel="import" href="/core/selection.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('selectionChangingLink', function() {
+    var r = tv.c.analysis.AnalysisResults();
+    var track = {};
+    var linkEl = r.createSelectionChangingLink('hello', function() {
+      var selection = new tv.c.Selection();
+      selection.push({guid: 1});
+      return selection;
+    });
+    var didRequestSelectionChange = false;
+    linkEl.addEventListener('requestSelectionChange', function(e) {
+      didRequestSelectionChange = true;
+    });
+    linkEl.click();
+    assert.isTrue(didRequestSelectionChange);
+  });
+
+  test('displayValuesInInfoRow', function() {
+    var r = new tv.c.analysis.AnalysisResults();
+    var table = new tv.c.analysis.StubAnalysisTable();
+    var node;
+    var sectionNode;
+    assert.equal(table.nodeCount, 0);
+
+    r.appendInfoRow(table, 'false_value', false);
+    assert.equal(table.nodeCount, 1);
+    sectionNode = table.lastNode;
+    assert.equal(sectionNode.nodeCount, 1);
+    node = sectionNode.lastNode;
+    assert.equal(node.children[0].innerText, 'false_value');
+    assert.equal(node.children[1].shadowRoot.textContent, 'false');
+
+    r.appendInfoRow(table, 'true_value', true);
+
+    assert.equal(sectionNode.nodeCount, 1);
+    node = sectionNode.lastNode;
+    assert.equal(node.children[0].innerText, 'true_value');
+    assert.equal(node.children[1].shadowRoot.textContent, 'true');
+
+    r.appendInfoRow(table, 'string_value', 'a string');
+    assert.equal(sectionNode.nodeCount, 1);
+    node = sectionNode.lastNode;
+    assert.equal(node.children[0].innerText, 'string_value');
+    assert.equal(node.children[1].shadowRoot.textContent, '"a string"');
+
+    r.appendInfoRow(table, 'number_value', 12345);
+    assert.equal(sectionNode.nodeCount, 1);
+    node = sectionNode.lastNode;
+    assert.equal(node.children[0].innerText, 'number_value');
+    assert.equal(node.children[1].shadowRoot.textContent, '12345');
+
+    r.appendInfoRow(table, 'undefined', undefined);
+    assert.equal(sectionNode.nodeCount, 1);
+    node = sectionNode.lastNode;
+    assert.equal(node.children[0].innerText, 'undefined');
+    assert.equal(node.children[1].innerText, '');
+
+    assert.equal(sectionNode.nodeCount, 0);
+    assert.equal(table.nodeCount, 0);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_sub_view.html b/trace-viewer/trace_viewer/core/analysis/analysis_sub_view.html
new file mode 100644
index 0000000..349d44e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_sub_view.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+@fileoverview Polymer element for various analysis sub-views.
+-->
+<polymer-element name="tracing-analysis-sub-view"
+    constructor="TracingAnalysisSubView">
+  <script>
+  'use strict';
+  Polymer({
+    set tabLabel(label) {
+      return this.setAttribute('tab-label', label);
+    },
+
+    get tabLabel() {
+      return this.getAttribute('tab-label');
+    },
+
+    get requiresTallView() {
+      return false;
+    },
+
+    /**
+     * Each element extending this one must implement
+     * a 'selection' property.
+     */
+    set selection(selection) {
+      throw new Error('Not implemented!');
+    },
+
+    get selection() {
+      throw new Error('Not implemented!');
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/analysis_sub_view_test.html
new file mode 100644
index 0000000..2aaa0bc
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_sub_view_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/selection.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('subViewThrowsNotImplementedErrors', function() {
+    var subView = new TracingAnalysisSubView();
+
+    assert.throw(function() {
+      subView.selection = new tv.c.Selection();
+    }, 'Not implemented!');
+
+    assert.throw(function() {
+      var viewSelection = subView.selection;
+    }, 'Not implemented!');
+
+    subView.tabLabel = 'Tab Label';
+    assert.equal(subView.getAttribute('tab-label'), 'Tab Label');
+    assert.equal(subView.tabLabel, 'Tab Label');
+
+    subView.tabLabel = 'New Label';
+    assert.equal(subView.getAttribute('tab-label'), 'New Label');
+    assert.equal(subView.tabLabel, 'New Label');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_view.html b/trace-viewer/trace_viewer/core/analysis/analysis_view.html
new file mode 100644
index 0000000..82edb01
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_view.html
@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/polymer_utils.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/core/analysis/tab_view.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+
+<!-- Sub Views. -->
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+<link rel="import" href="/core/analysis/multi_slice_sub_view.html">
+
+<link rel="import" href="/core/analysis/multi_event_sub_view.html">
+
+<link rel="import" href="/core/analysis/single_thread_time_slice_sub_view.html">
+<link rel="import" href="/core/analysis/single_cpu_slice_sub_view.html">
+
+<link rel="import" href="/core/analysis/single_instant_event_sub_view.html">
+<link rel="import" href="/core/analysis/multi_instant_event_sub_view.html">
+
+<link rel="import" href="/core/analysis/counter_sample_sub_view.html">
+
+<link rel="import" href="/core/analysis/single_flow_event_sub_view.html">
+<link rel="import" href="/core/analysis/multi_flow_event_sub_view.html">
+
+<link rel="import" href="/core/analysis/single_object_instance_sub_view.html">
+<link rel="import" href="/core/analysis/single_object_snapshot_sub_view.html">
+<link rel="import" href="/core/analysis/multi_object_sub_view.html">
+
+<link rel="import" href="/core/analysis/single_sample_sub_view.html">
+<link rel="import" href="/core/analysis/multi_sample_sub_view.html">
+
+<link rel="import"
+    href="/core/analysis/single_interaction_record_sub_view.html">
+<link rel="import"
+    href="/core/analysis/multi_interaction_record_sub_view.html">
+
+<link rel="import"
+    href="/core/analysis/single_alert_sub_view.html">
+<link rel="import"
+    href="/core/analysis/multi_alert_sub_view.html">
+
+<link rel="import"
+    href="/core/analysis/single_global_memory_dump_sub_view.html">
+<link rel="import" href="/core/analysis/multi_global_memory_dump_sub_view.html">
+
+<!--
+@fileoverview A component used to display an analysis of a selection,
+using custom elements specialized for different event types.
+-->
+<polymer-element name="tracing-analysis-view"
+    constructor="TracingAnalysisView">
+  <template>
+    <style>
+      :host {
+        background-color: white;
+        display: flex;
+        flex-direction: column;
+        height: 275px;
+        overflow: auto;
+      }
+
+      :host(.tall-mode) {
+        height: 525px;
+      }
+
+      ::content > * {
+        flex: 1 0 auto;
+      }
+    </style>
+    <content></content>
+  </template>
+  <script>
+  'use strict';
+  (function() {
+    var EventRegistry = tv.c.trace_model.EventRegistry;
+
+    Polymer({
+      ready: function() {
+        this.tabView_ = document.createElement(
+            'tracing-analysis-tab-view');
+        this.tabView_.style.flex = '1 1 auto';
+        this.tabView_.addEventListener(
+          'selected-tab-change',
+          this.onSelectedTabChange_.bind(this));
+        this.appendChild(this.tabView_);
+        this.currentSelection_ = undefined;
+      },
+
+      set tallMode(value) {
+        if (value)
+          this.classList.add('tall-mode');
+        else
+          this.classList.remove('tall-mode');
+      },
+
+      get tallMode() {
+        return this.classList.contains('tall-mode');
+      },
+
+      get tabView() {
+        return this.tabView_;
+      },
+
+      get selection() {
+        return this.currentSelection_;
+      },
+
+      set selection(selection) {
+        var lastSelectedTabTagName;
+        var lastSelectedTabTypeName;
+        if (this.tabView_.selectedTab) {
+          lastSelectedTabTagName = this.tabView_.selectedTab.tagName;
+          lastSelectedTabTypeName = this.tabView_.selectedTab._eventTypeName;
+        }
+
+        this.tallMode = false;
+        this.tabView_.textContent = '';
+
+        var eventsByBaseTypeName = selection.getEventsOrganizedByBaseType(true);
+
+        var numBaseTypesToAnalyze = tv.b.dictionaryLength(eventsByBaseTypeName);
+
+        for (var eventTypeName in eventsByBaseTypeName) {
+          var subSelection = eventsByBaseTypeName[eventTypeName];
+          var subView = this.createSubViewForSelection_(
+            eventTypeName, subSelection);
+          // Store the eventTypeName for future tab restoration.
+          subView._eventTypeName = eventTypeName;
+          this.tabView_.appendChild(subView);
+
+          subView.selection = subSelection;
+        }
+
+        // Restore the tab type that was previously selected. First try by tag
+        // name.
+        var tab;
+        if (lastSelectedTabTagName)
+          tab = this.tabView_.querySelector(lastSelectedTabTagName);
+
+        // If that fails, look for a tab with that typeName.
+        if (!tab && lastSelectedTabTypeName) {
+          var tab = tv.b.findFirstInArray(
+              this.tabView_.children, function(tab) {
+            return tab._eventTypeName === lastSelectedTabTypeName;
+          });
+        }
+        // If all else fails, pick the first tab.
+        if (!tab)
+          tab = this.tabView_.firstChild;
+        this.tabView_.selectedTab = tab;
+      },
+
+      createSubViewForSelection_: function(eventTypeName, subSelection) {
+        // Find.
+        var eventTypeInfo = EventRegistry.getEventTypeInfoByTypeName(
+            eventTypeName);
+        var singleMode = subSelection.length == 1;
+        var tagName;
+        if (subSelection.length === 1)
+          tagName = eventTypeInfo.metadata.singleViewElementName;
+        else
+          tagName = eventTypeInfo.metadata.multiViewElementName;
+
+        if (!tv.b.getPolymerElementNamed(tagName))
+          throw new Error('Element not registered: ' + tagName);
+
+        // Create.
+        var subView = document.createElement(tagName);
+
+        // Set label.
+        var camelLabel;
+        if (subSelection.length === 1)
+          camelLabel = EventRegistry.getUserFriendlySingularName(eventTypeName);
+        else
+          camelLabel = EventRegistry.getUserFriendlyPluralName(eventTypeName);
+        subView.tabLabel = camelLabel;
+
+        return subView;
+      },
+
+      onSelectedTabChange_: function() {
+        if (this.tabView_.selectedTab)
+          this.tallMode = this.tabView_.selectedTab.requiresTallView;
+        else
+          this.tallMode = false;
+      }
+    });
+  })();
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/analysis_view_test.html b/trace-viewer/trace_viewer/core/analysis/analysis_view_test.html
new file mode 100644
index 0000000..8bd0ca8
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/analysis_view_test.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/selection.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/counter_sample_sub_view.html b/trace-viewer/trace_viewer/core/analysis/counter_sample_sub_view.html
new file mode 100644
index 0000000..6dff632
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/counter_sample_sub_view.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/trace_model/counter_sample.html">
+
+<polymer-element name="tv-c-counter-sample-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+  (function() {
+    var CounterSample = tv.c.trace_model.CounterSample;
+
+    Polymer({
+      created: function() {
+        this.currentSelection_ = undefined;
+      },
+
+      get selection() {
+        return this.currentSelection_;
+      },
+
+      set selection(selection) {
+        var results = new tv.c.analysis.AnalysisResults();
+        this.appendChild(results);
+        this.analyzeCounterSamples_(results, selection);
+      },
+
+      analyzeCounterSamples_: function(results, allSamples) {
+        var samplesByCounter = {};
+        for (var i = 0; i < allSamples.length; i++) {
+          var ctr = allSamples[i].series.counter;
+          if (!samplesByCounter[ctr.guid])
+            samplesByCounter[ctr.guid] = [];
+          samplesByCounter[ctr.guid].push(allSamples[i]);
+        }
+
+        for (var guid in samplesByCounter) {
+          var samples = samplesByCounter[guid];
+          var ctr = samples[0].series.counter;
+
+          var timestampGroups = CounterSample.groupByTimestamp(samples);
+          if (timestampGroups.length == 1)
+            this.analyzeSingleCounterTimestamp_(results, ctr,
+                                                timestampGroups[0]);
+          else
+            this.analyzeMultipleCounterTimestamps_(results, ctr,
+                                                   timestampGroups);
+        }
+      },
+
+      analyzeSingleCounterTimestamp_: function(
+          results, ctr, samplesWithSameTimestamp) {
+        results.appendHeader('Selected counter:');
+        var table = results.appendTable('analysis-counter-table', 2);
+        results.appendInfoRow(table, 'Title', ctr.name);
+        results.appendInfoRowTime(
+            table, 'Timestamp', samplesWithSameTimestamp[0].timestamp);
+        for (var i = 0; i < samplesWithSameTimestamp.length; i++) {
+          var sample = samplesWithSameTimestamp[i];
+          results.appendInfoRow(table, sample.series.name, sample.value);
+        }
+      },
+
+      analyzeMultipleCounterTimestamps_: function(results, ctr,
+                                             samplesByTimestamp) {
+        results.appendHeader('Counter ' + ctr.name);
+        var table = results.appendTable('analysis-counter-table', 2);
+
+        var sampleIndices = [];
+        for (var i = 0; i < samplesByTimestamp.length; i++)
+          sampleIndices.push(samplesByTimestamp[i][0].getSampleIndex());
+
+        var stats = ctr.getSampleStatistics(sampleIndices);
+        for (var i = 0; i < stats.length; i++) {
+          var samples = [];
+          for (var k = 0; k < sampleIndices.length; ++k)
+            samples.push(ctr.getSeries(i).getSample(sampleIndices[k]).value);
+
+          results.appendDataRow(
+              table,
+              ctr.name + ': series(' + ctr.getSeries(i).name + ')',
+              samples,
+              null,
+              null,
+              null,
+              samples.length,
+              null,
+              stats[i]);
+        }
+      }
+    });
+  })();
+  </script>
+</polymer>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/analysis/counter_sample_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/counter_sample_sub_view_test.html
new file mode 100644
index 0000000..78de05e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/counter_sample_sub_view_test.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/analysis/stub_analysis_results.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Counter = tv.c.trace_model.Counter;
+  var CounterSeries = tv.c.trace_model.CounterSeries;
+
+  var Selection = tv.c.Selection;
+  var StubAnalysisResults = tv.c.analysis.StubAnalysisResults;
+
+  function createSeries(ctr) {
+    var allocatedSeries = new CounterSeries('bytesallocated', 0);
+    var freeSeries = new CounterSeries('bytesfree', 1);
+
+    ctr.addSeries(allocatedSeries);
+    ctr.addSeries(freeSeries);
+
+    allocatedSeries.addCounterSample(0, 0);
+    allocatedSeries.addCounterSample(10, 25);
+    allocatedSeries.addCounterSample(20, 10);
+
+    freeSeries.addCounterSample(0, 15);
+    freeSeries.addCounterSample(10, 20);
+    freeSeries.addCounterSample(20, 5);
+  }
+
+  var createSelectionWithTwoSeriesSingleCounter = function() {
+    var ctr = new Counter(null, 0, 'foo', 'ctr[0]');
+    createSeries(ctr);
+
+    var selection = new Selection();
+    var t1track = {};
+
+    selection.push(ctr.getSeries(0).samples[1]);
+    selection.push(ctr.getSeries(1).samples[1]);
+    return selection;
+  };
+
+  test('instantiate_singleCounterWithTwoSeries', function() {
+    var selection = createSelectionWithTwoSeriesSingleCounter();
+
+    var analysisEl = document.createElement('tv-c-counter-sample-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  var createSelectionWithTwoSeriesTwoCounters = function() {
+    var ctr1 = new Counter(null, 0, '', 'ctr1');
+    createSeries(ctr1);
+
+    var ctr2 = new Counter(null, 0, '', 'ctr2');
+    createSeries(ctr2);
+
+    var selection = new Selection();
+    var t1track = {};
+
+    selection.push(ctr1.getSeries(0).samples[1]);
+    selection.push(ctr1.getSeries(1).samples[1]);
+
+
+    selection.push(ctr2.getSeries(0).samples[2]);
+    selection.push(ctr2.getSeries(1).samples[2]);
+    return selection;
+  };
+
+  test('instantiate_twoCountersWithTwoSeries', function() {
+    var selection = createSelectionWithTwoSeriesTwoCounters();
+
+    var analysisEl = document.createElement('tv-c-counter-sample-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('analyzeSelectionWithSingleCounter', function() {
+    var ctr = new Counter(null, 0, '', 'ctr');
+    var series = new CounterSeries('value', 0);
+    ctr.addSeries(series);
+
+    series.addCounterSample(0, 0);
+    series.addCounterSample(10, 10);
+
+    var selection = new Selection();
+    var t1track = {};
+    selection.push(ctr.getSeries(0).samples[1]);
+
+    var view = document.createElement('tv-c-counter-sample-sub-view');
+    var results = new StubAnalysisResults();
+    view.analyzeCounterSamples_(results, selection);
+
+    assert.equal(results.tables.length, 1);
+    assert.equal(results.headers[0].label, 'Selected counter:');
+    var table = results.tables[0];
+    assert.equal(table.rows.length, 3);
+
+    assert.equal(table.rows[0].label, 'Title');
+    assert.equal(table.rows[1].label, 'Timestamp');
+    assert.equal(table.rows[2].label, 'value');
+    assert.equal(table.rows[2].text, 10);
+  });
+
+  function createSelectionWithTwoCountersDiffSeriesDiffEvents() {
+    var ctr1 = new Counter(null, 0, '', 'a');
+    var ctr1AllocatedSeries = new CounterSeries('bytesallocated', 0);
+    ctr1.addSeries(ctr1AllocatedSeries);
+
+    ctr1AllocatedSeries.addCounterSample(0, 0);
+    ctr1AllocatedSeries.addCounterSample(10, 25);
+    ctr1AllocatedSeries.addCounterSample(20, 15);
+
+    assert.equal(ctr1.name, 'a');
+    assert.equal(ctr1.numSamples, 3);
+    assert.equal(ctr1.numSeries, 1);
+
+    var ctr2 = new Counter(null, 0, '', 'b');
+    var ctr2AllocatedSeries = new CounterSeries('bytesallocated', 0);
+    var ctr2FreeSeries = new CounterSeries('bytesfree', 1);
+
+    ctr2.addSeries(ctr2AllocatedSeries);
+    ctr2.addSeries(ctr2FreeSeries);
+
+    ctr2AllocatedSeries.addCounterSample(0, 0);
+    ctr2AllocatedSeries.addCounterSample(10, 25);
+    ctr2AllocatedSeries.addCounterSample(20, 10);
+    ctr2AllocatedSeries.addCounterSample(30, 15);
+
+    ctr2FreeSeries.addCounterSample(0, 20);
+    ctr2FreeSeries.addCounterSample(10, 5);
+    ctr2FreeSeries.addCounterSample(20, 25);
+    ctr2FreeSeries.addCounterSample(30, 0);
+
+    assert.equal(ctr2.name, 'b');
+    assert.equal(ctr2.numSamples, 4);
+    assert.equal(ctr2.numSeries, 2);
+
+    var selection = new Selection();
+    var t1track = {};
+    var t2track = {};
+
+    selection.push(ctr1AllocatedSeries.samples[1]);
+    selection.push(ctr2AllocatedSeries.samples[2]);
+    selection.push(ctr2FreeSeries.samples[2]);
+
+    return selection;
+  };
+
+  test('analyzeSelectionWithComplexSeriesTwoCounters', function() {
+    var selection = createSelectionWithTwoCountersDiffSeriesDiffEvents();
+
+    var view = document.createElement('tv-c-counter-sample-sub-view');
+    var results = new StubAnalysisResults();
+    view.analyzeCounterSamples_(results, selection);
+
+    assert.equal(results.tables.length, 2);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/generic_object_view.css b/trace-viewer/trace_viewer/core/analysis/generic_object_view.css
new file mode 100644
index 0000000..0196959
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/generic_object_view.css
@@ -0,0 +1,13 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+x-generic-object-view {
+  display: block;
+  font-family: monospace;
+}
+
+x-generic-object-view-with-label {
+  display: block;
+}
diff --git a/trace-viewer/trace_viewer/core/analysis/generic_object_view.html b/trace-viewer/trace_viewer/core/analysis/generic_object_view.html
new file mode 100644
index 0000000..dcadeae
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/generic_object_view.html
@@ -0,0 +1,230 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/ui.html">
+
+<polymer-element name="tv-c-analysis-generic-object-view"
+    is="HTMLUnknownElement">
+  <template>
+    <style>
+    :host {
+      display: block;
+      font-family: monospace;
+    }
+    </style>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.object_ = undefined;
+    },
+
+    get object() {
+      return this.object_;
+    },
+
+    set object(object) {
+      this.object_ = object;
+      this.updateContents_();
+    },
+
+    updateContents_: function() {
+      this.shadowRoot.textContent = '';
+      this.appendElementsForType_('', this.object_, 0, 0, 5, '');
+    },
+
+    appendElementsForType_: function(
+        label, object, indent, depth, maxDepth, suffix) {
+      if (depth > maxDepth) {
+        this.appendSimpleText_(
+            label, indent, '<recursion limit reached>', suffix);
+        return;
+      }
+
+      if (object === undefined) {
+        this.appendSimpleText_(label, indent, 'undefined', suffix);
+        return;
+      }
+
+      if (object === null) {
+        this.appendSimpleText_(label, indent, 'null', suffix);
+        return;
+      }
+
+      if (!(object instanceof Object)) {
+        var type = typeof object;
+        if (type == 'string') {
+          var objectReplaced = false;
+          if ((object[0] == '{' && object[object.length - 1] == '}') ||
+              (object[0] == '[' && object[object.length - 1] == ']')) {
+            try {
+              object = JSON.parse(object);
+              objectReplaced = true;
+            } catch (e) {
+            }
+          }
+          if (!objectReplaced)
+            return this.appendSimpleText_(
+                label, indent, '"' + object + '"', suffix);
+          else {
+            /* Fall through to the flow below */
+          }
+        } else {
+          return this.appendSimpleText_(label, indent, object, suffix);
+        }
+      }
+
+      if (object instanceof tv.c.trace_model.ObjectSnapshot) {
+        var link = document.createElement('tv-c-analysis-link');
+        link.selection = new tv.c.Selection(object);
+        this.appendElementWithLabel_(label, indent, link, suffix);
+        return;
+      }
+
+      if (object instanceof tv.c.trace_model.ObjectInstance) {
+        var link = document.createElement('tv-c-analysis-link');
+        link.selection = new tv.c.Selection(object);
+        this.appendElementWithLabel_(label, indent, link, suffix);
+        return;
+      }
+
+      if (object instanceof tv.b.Rect) {
+        this.appendSimpleText_(label, indent, object.toString(), suffix);
+        return;
+      }
+
+      if (object instanceof Array) {
+        this.appendElementsForArray_(
+            label, object, indent, depth, maxDepth, suffix);
+        return;
+      }
+
+      this.appendElementsForObject_(
+          label, object, indent, depth, maxDepth, suffix);
+    },
+
+    appendElementsForArray_: function(
+        label, object, indent, depth, maxDepth, suffix) {
+      if (object.length == 0) {
+        this.appendSimpleText_(label, indent, '[]', suffix);
+        return;
+      }
+
+      this.appendElementsForType_(
+          label + '[',
+          object[0],
+          indent, depth + 1, maxDepth,
+          object.length > 1 ? ',' : ']' + suffix);
+      for (var i = 1; i < object.length; i++) {
+        this.appendElementsForType_(
+            '',
+            object[i],
+            indent + label.length + 1, depth + 1, maxDepth,
+            i < object.length - 1 ? ',' : ']' + suffix);
+      }
+      return;
+    },
+
+    appendElementsForObject_: function(
+        label, object, indent, depth, maxDepth, suffix) {
+      var keys = tv.b.dictionaryKeys(object);
+      if (keys.length == 0) {
+        this.appendSimpleText_(label, indent, '{}', suffix);
+        return;
+      }
+
+      this.appendElementsForType_(
+          label + '{' + keys[0] + ': ',
+          object[keys[0]],
+          indent, depth, maxDepth,
+          keys.length > 1 ? ',' : '}' + suffix);
+      for (var i = 1; i < keys.length; i++) {
+        this.appendElementsForType_(
+            keys[i] + ': ',
+            object[keys[i]],
+            indent + label.length + 1, depth + 1, maxDepth,
+            i < keys.length - 1 ? ',' : '}' + suffix);
+      }
+    },
+
+    appendElementWithLabel_: function(label, indent, dataElement, suffix) {
+      var row = document.createElement('div');
+
+      var indentSpan = document.createElement('span');
+      indentSpan.style.whiteSpace = 'pre';
+      for (var i = 0; i < indent; i++)
+        indentSpan.textContent += ' ';
+      row.appendChild(indentSpan);
+
+      var labelSpan = document.createElement('span');
+      labelSpan.textContent = label;
+      row.appendChild(labelSpan);
+
+      row.appendChild(dataElement);
+      var suffixSpan = document.createElement('span');
+      suffixSpan.textContent = suffix;
+      row.appendChild(suffixSpan);
+
+      row.dataElement = dataElement;
+      this.shadowRoot.appendChild(row);
+    },
+
+    appendSimpleText_: function(label, indent, text, suffix) {
+      var el = this.ownerDocument.createElement('span');
+      el.textContent = text;
+      this.appendElementWithLabel_(label, indent, el, suffix);
+      return el;
+    }
+  });
+  </script>
+</polymer-element>
+
+<polymer-element name="tv-c-analysis-generic-object-view-with-label"
+    is="HTMLUnknownElement">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+    </style>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.labelEl_ = document.createElement('div');
+      this.genericObjectView_ =
+          document.createElement('tv-c-analysis-generic-object-view');
+      this.shadowRoot.appendChild(this.labelEl_);
+      this.shadowRoot.appendChild(this.genericObjectView_);
+    },
+
+    get label() {
+      return this.labelEl_.textContent;
+    },
+
+    set label(label) {
+      this.labelEl_.textContent = label;
+    },
+
+    get object() {
+      return this.genericObjectView_.object;
+    },
+
+    set object(object) {
+      this.genericObjectView_.object = object;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/generic_object_view_test.html b/trace-viewer/trace_viewer/core/analysis/generic_object_view_test.html
new file mode 100644
index 0000000..3ab7be2
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/generic_object_view_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('undefinedValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = undefined;
+    assert.equal(view.shadowRoot.textContent, 'undefined');
+  });
+
+  test('nullValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = null;
+    assert.equal(view.shadowRoot.textContent, 'null');
+  });
+
+  test('stringValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = 'string value';
+    assert.equal(view.shadowRoot.textContent, '"string value"');
+  });
+
+  test('jsonObjectStringValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = '{"x": 1}';
+    assert.equal(view.shadowRoot.children.length, 1);
+    assert.equal(view.shadowRoot.children[0].children.length, 4);
+  });
+
+  test('jsonArraStringValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = '[1,2,3]';
+    assert.equal(view.shadowRoot.children.length, 3);
+  });
+
+  test('booleanValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = false;
+    assert.equal(view.shadowRoot.textContent, 'false');
+  });
+
+  test('numberValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = 3.14159;
+    assert.equal(view.shadowRoot.textContent, '3.14159');
+  });
+
+  test('objectSnapshotValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+
+    var i10 = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'name', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+
+    view.object = s10;
+    this.addHTMLOutput(view);
+    assert.strictEqual(view.shadowRoot.children[0].dataElement.tagName,
+        'TV-C-ANALYSIS-LINK');
+  });
+
+  test('objectInstanceValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+
+    var i10 = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'name', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+
+    view.object = i10;
+    assert.strictEqual(view.shadowRoot.children[0].dataElement.tagName,
+        'TV-C-ANALYSIS-LINK');
+  });
+
+  test('instantiate_emptyArrayValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = [];
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_twoValueArrayValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = [1, 2];
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_twoValueBArrayValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = [1, {x: 1}];
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_arrayValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = [1, 2, 'three'];
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_arrayWithSimpleObjectValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = [{simple: 'object'}];
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_arrayWithComplexObjectValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = [{complex: 'object', field: 'two'}];
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_objectValue', function() {
+    var view = document.createElement('tv-c-analysis-generic-object-view');
+    view.object = {
+      'entry_one': 'entry_one_value',
+      'entry_two': 2,
+      'entry_three': [3, 4, 5]
+    };
+    this.addHTMLOutput(view);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_alert_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_alert_sub_view.html
new file mode 100644
index 0000000..24d7c85
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_alert_sub_view.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/multi_slice_sub_view.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<polymer-element name="tv-c-multi-alert-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.currentSelection_ = selection;
+      this.textContent = '';
+      var realView = document.createElement('tv-c-multi-slice-sub-view');
+
+      this.appendChild(realView);
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.currentSelection_ = selection;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_details_table.html b/trace-viewer/trace_viewer/core/analysis/multi_event_details_table.html
new file mode 100644
index 0000000..d41212d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_details_table.html
@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+<link rel="import" href="/core/analysis/multi_event_summary.html">
+<link rel="import" href="/core/analysis/time_span.html">
+<link rel="import" href="/core/analysis/time_stamp.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+
+<polymer-element name='tv-c-a-multi-event-details-table'>
+  <template>
+    <style>
+    :host {
+      display: flex;
+      flex-direction: column;
+    }
+    #table {
+      flex: 1 1 auto;
+      align-self: stretch;
+    }
+
+    #titletable {
+      font-weight: bold;
+    }
+
+    #title-info {
+      font-size: 12px;
+    }
+    </style>
+    <tracing-analysis-nested-table id="titletable">
+    </tracing-analysis-nested-table>
+    <tracing-analysis-nested-table id="table">
+    </tracing-analysis-nested-table>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.selection_ = undefined;
+    },
+
+    ready: function() {
+      this.initTitleTable_();
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      this.selection_ = selection;
+
+      this.updateTitleTable_();
+
+      if (this.selection_ === undefined) {
+        this.$.table.tableRows = [];
+        this.$.table.tableFooterRows = [];
+        this.$.table.rebuild();
+        return;
+      }
+
+      var summary = new tv.c.analysis.MultiEventSummary(
+          'Totals', this.selection_);
+      this.updateColumns_(summary);
+      this.updateRows_(summary);
+      this.$.table.rebuild();
+    },
+
+    initTitleTable_: function() {
+      var table = this.$.titletable;
+
+      table.showHeader = false;
+      table.tableColumns = [
+        {
+          title: 'Title',
+          value: function(row) { return row.title; },
+          width: '350px'
+        },
+        {
+          title: 'Value',
+          width: '100%',
+          value: function(row) {
+            return row.value;
+          }
+        }
+      ];
+    },
+
+    updateTitleTable_: function() {
+      var title;
+      if (this.selection_ && this.selection_.length)
+        title = this.selection_[0].title;
+      else
+        title = '<No selection>';
+
+      var table = this.$.titletable;
+      table.tableRows = [{
+        title: 'Title',
+        value: title
+      }];
+    },
+
+    updateColumns_: function(summary) {
+      var hasCpuData;
+      if (summary.cpuDuration !== undefined)
+        hasCpuData = true;
+      if (summary.cpuSelfTime !== undefined)
+        hasCpuData = true;
+
+      var colWidthPercentage;
+      if (hasCpuData)
+        colWidthPercentage = '20%';
+      else
+        colWidthPercentage = '33.3333%';
+
+      var columns = [];
+
+      columns.push({
+        title: 'Start',
+        value: function(row) {
+          if (row.__proto__ === tv.c.analysis.MultiEventSummary.prototype) {
+            return row.title;
+          }
+
+          var linkEl = document.createElement('tv-c-analysis-link');
+          linkEl.setSelectionAndContent(function() {
+              return new tv.c.Selection(row);
+          });
+          linkEl.appendChild(tv.c.analysis.createTimeStamp(row.start));
+          return linkEl;
+        },
+        width: '350px',
+        cmp: function(rowA, rowB) {
+          return rowA.title.localeCompare(rowB.title);
+        }
+      });
+      columns.push({
+        title: 'Wall Duration (ms)',
+        value: function(row) {
+          return tv.c.analysis.createTimeSpan(row.duration);
+        },
+        width: '<upated further down>',
+        cmp: function(rowA, rowB) {
+          return rowA.duration - rowB.duration;
+        }
+      });
+
+      if (hasCpuData) {
+        columns.push({
+          title: 'CPU Duration (ms)',
+          value: function(row) {
+            return tv.c.analysis.createTimeSpan(row.cpuDuration);
+          },
+          width: '<upated further down>',
+          cmp: function(rowA, rowB) {
+            return rowA.cpuDuration - rowB.cpuDuration;
+          }
+        });
+      }
+
+      columns.push({
+        title: 'Self time (ms)',
+        value: function(row) {
+          return tv.c.analysis.createTimeSpan(row.selfTime);
+        },
+        width: '<upated further down>',
+        cmp: function(rowA, rowB) {
+          return rowA.selfTime - rowB.selfTime;
+        }
+      });
+      if (hasCpuData) {
+        columns.push({
+          title: 'CPU Self Time (ms)',
+          value: function(row) {
+            return tv.c.analysis.createTimeSpan(row.cpuSelfTime);
+          },
+          width: '<upated further down>',
+          cmp: function(rowA, rowB) {
+            return rowA.cpuSelfTime - rowB.cpuSelfTime;
+          }
+        });
+      }
+
+      var argKeys = tv.b.dictionaryKeys(summary.totalledArgs);
+      argKeys.sort();
+
+      var otherKeys = summary.untotallableArgs.slice(0);
+      otherKeys.sort();
+
+      argKeys.push.apply(argKeys, otherKeys);
+      var keysWithColumns = argKeys.slice(0, 4);
+      var keysInOtherColumn = argKeys.slice(4);
+
+      keysWithColumns.forEach(function(argKey) {
+
+        var hasTotal = summary.totalledArgs[argKey];
+        var colDesc = {
+          title: 'Arg: ' + argKey,
+          value: function(row) {
+            if (row.__proto__ !== tv.c.analysis.MultiEventSummary.prototype) {
+              var argView =
+                  document.createElement('tv-c-analysis-generic-object-view');
+              argView.object = row.args[argKey];
+              return argView;
+            }
+            if (hasTotal)
+              return row.totalledArgs[argKey];
+            return '';
+          },
+          width: '<upated further down>'
+        };
+        if (hasTotal) {
+          colDesc.cmp = function(rowA, rowB) {
+            return rowA.args[argKey] - rowB.args[argKey];
+          }
+        }
+        columns.push(colDesc);
+      });
+
+      if (keysInOtherColumn.length) {
+        columns.push({
+          title: 'Other Args',
+          value: function(row) {
+            if (row.__proto__ === tv.c.analysis.MultiEventSummary.prototype)
+              return '';
+            var argView =
+                document.createElement('tv-c-analysis-generic-object-view');
+            var obj = {};
+            for (var i = 0; i < keysInOtherColumn.length; i++)
+              obj[keysInOtherColumn[i]] = row.args[keysInOtherColumn[i]];
+            argView.object = obj;
+            return argView;
+          },
+          width: '<upated further down>'
+        });
+      }
+
+      var colWidthPercentage = (100 / (columns.length - 1)).toFixed(3) + '%';
+      for (var i = 1; i < columns.length; i++)
+        columns[i].width = colWidthPercentage;
+
+      this.$.table.tableColumns = columns;
+    },
+
+    updateRows_: function(summary) {
+      this.$.table.sortColumnIndex = 0;
+      this.$.table.tableRows = this.selection_.map(function(event) {
+        return event;
+      });
+      this.$.table.footerRows = [summary];
+    }
+  });
+</script>
+</polymer>
+
+
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_details_table_test.html b/trace-viewer/trace_viewer/core/analysis/multi_event_details_table_test.html
new file mode 100644
index 0000000..bb307a3
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_details_table_test.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/deep_utils.html">
+<link rel="import" href="/core/analysis/multi_event_details_table.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Thread = tv.c.trace_model.Thread;
+  var Selection = tv.c.Selection;
+  var newSliceEx = tv.c.test_utils.newSliceEx;
+
+  test('withCpuTime', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+                              cpuStart: 0, cpuEnd: 3}));
+    tsg.pushSlice(newSliceEx({title: 'a', start: 1, end: 2,
+                              cpuStart: 1, cpuEnd: 1.75}));
+    tsg.pushSlice(newSliceEx({title: 'a', start: 4, end: 5,
+                              cpuStart: 3, cpuEnd: 3.75}));
+    tsg.createSubSlices();
+
+    var threadTrack = {};
+    threadTrack.thread = thread;
+
+    var selection = new Selection(tsg.slices);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-details-table');
+    viewEl.selection = selection;
+    this.addHTMLOutput(viewEl);
+  });
+
+  test('withoutCpuTime', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+    tsg.pushSlice(newSliceEx({title: 'a', start: 1, end: 2}));
+    tsg.pushSlice(newSliceEx({title: 'a', start: 4, end: 5}));
+    tsg.createSubSlices();
+
+    var threadTrack = {};
+    threadTrack.thread = thread;
+
+    var selection = new Selection(tsg.slices);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-details-table');
+    viewEl.selection = selection;
+    this.addHTMLOutput(viewEl);
+  });
+
+
+  test('withFewerThanFourArgs', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+                              args: {value1: 3, value2: 'x', value3: 1}}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+                              args: {value1: 3.1, value2: 'y', value3: 2}}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+                              args: {value1: 3.2, value2: 'z', value3: 'x'}}));
+    tsg.createSubSlices();
+
+    var threadTrack = {};
+    threadTrack.thread = thread;
+
+    var selection = new Selection(tsg.slices);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-details-table');
+    viewEl.selection = selection;
+    this.addHTMLOutput(viewEl);
+  });
+
+  test('withExtraArgs', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+                              args: {value1: 3, value2: 'x', value3: 1,
+                                     value4: 4, value5: 5, value6: 6}}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+                              args: {value1: 3.1, value2: 'y', value3: 2,
+                                     value4: 4, value5: 5, value6: 6}}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+                              args: {value1: 3.2, value2: 'z', value3: 'x',
+                                     value4: 4, value5: 'whoops', value6: 6}}));
+    tsg.createSubSlices();
+
+    var threadTrack = {};
+    threadTrack.thread = thread;
+
+    var selection = new Selection(tsg.slices);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-details-table');
+    viewEl.selection = selection;
+    this.addHTMLOutput(viewEl);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_event_sub_view.html
new file mode 100644
index 0000000..32ec713
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_sub_view.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/selection_summary_table.html">
+<link rel="import" href="/core/analysis/multi_event_summary_table.html">
+<link rel="import" href="/core/analysis/multi_event_details_table.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+
+<polymer-element name="tv-c-a-multi-event-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: flex;
+      overflow: auto;
+    }
+    #content {
+      display: flex;
+      flex-direction: column;
+      flex: 0 1 auto;
+      align-self: stretch;
+    }
+    #content > * {
+      flex: 0 0 auto;
+      align-self: stretch;
+    }
+    tv-c-a-multi-event-summary-table {
+      border-bottom: 1px solid #aaa;
+    }
+
+    tv-c-a-selection-summary-table  {
+      margin-top: 1.25em;
+      border-top: 1px solid #aaa;
+      background-color: #eee;
+      font-weight: bold;
+      margin-bottom: 1.25em;
+      border-bottom: 1px solid #aaa;
+    }
+    </style>
+    <div id="content"></div>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+      this.requiresTallView_ = false;
+    },
+
+    set selection(selection) {
+      if (selection.length <= 1)
+        throw new Error('Only supports multiple items');
+      if (!selection.every(
+          function(x) { return x instanceof tv.c.trace_model.Slice; })) {
+        throw new Error('Only supports slices');
+      }
+      this.setSelectionWithoutErrorChecks(selection);
+    },
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    get requiresTallView() {
+      return this.requiresTallView_;
+    },
+
+    setSelectionWithoutErrorChecks: function(selection) {
+      this.currentSelection_ = selection;
+      this.requiresTallView_ = false;
+
+      // TODO(nduca): This is a gross hack for cc Frame Viewer, but its only
+      // the frame viewer that needs this feature, so ~shrug~.
+      if (window.RasterTaskView !== undefined) { // May not have been imported.
+        if (tv.e.cc.RasterTaskSelection.supports(selection)) {
+          var ltvSelection = new tv.e.cc.RasterTaskSelection(selection);
+
+          var ltv = new tv.e.cc.LayerTreeHostImplSnapshotView();
+          ltv.objectSnapshot = ltvSelection.containingSnapshot;
+          ltv.selection = ltvSelection;
+          ltv.extraHighlightsByLayerId = ltvSelection.extraHighlightsByLayerId;
+          this.appendChild(ltv);
+
+          this.style.display = 'flex';
+
+          this.requiresTallView_ = true;
+          return;
+        }
+      }
+      this.style.display = '';
+
+      var eventsByTitle = selection.getEventsOrganizedByTitle();
+      var numTitles = tv.b.dictionaryLength(eventsByTitle);
+
+      this.$.content.textContent = '';
+
+      var summaryTableEl = document.createElement(
+          'tv-c-a-multi-event-summary-table');
+      summaryTableEl.configure({
+        showTotals: numTitles > 1,
+        eventsByTitle: eventsByTitle
+      });
+      this.$.content.appendChild(summaryTableEl);
+
+      var selectionSummaryTableEl = document.createElement(
+          'tv-c-a-selection-summary-table');
+      selectionSummaryTableEl.selection = this.currentSelection_;
+      this.$.content.appendChild(selectionSummaryTableEl);
+
+      if (numTitles === 1) {
+        var detailsTableEl = document.createElement(
+            'tv-c-a-multi-event-details-table');
+        detailsTableEl.selection = selection;
+        this.$.content.appendChild(detailsTableEl);
+      }
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/multi_event_sub_view_test.html
new file mode 100644
index 0000000..d936e8f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_sub_view_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/deep_utils.html">
+<link rel="import" href="/core/analysis/multi_event_sub_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Thread = tv.c.trace_model.Thread;
+  var Selection = tv.c.Selection;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+  var newSliceCategory = tv.c.test_utils.newSliceCategory;
+  var Slice = tv.c.trace_model.Slice;
+
+  test('differentTitles', function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(newSliceNamed('a', 0.0, 0.04));
+    t53.sliceGroup.pushSlice(newSliceNamed('a', 0.12, 0.06));
+    t53.sliceGroup.pushSlice(newSliceNamed('aa', 0.5, 0.5));
+    t53.sliceGroup.createSubSlices();
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+    selection.push(t53.sliceGroup.slices[2]);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-sub-view');
+    viewEl.selection = selection;
+    this.addHTMLOutput(viewEl);
+
+    var summaryTableEl = tv.b.findDeepElementMatching(
+        viewEl, 'tv-c-a-multi-event-summary-table');
+    assert.isDefined(summaryTableEl);
+
+    assert.isTrue(summaryTableEl.showTotals);
+    assert.equal(tv.b.dictionaryLength(summaryTableEl.eventsByTitle), 2);
+
+    var selectionSummaryTableEl = tv.b.findDeepElementMatching(
+        viewEl, 'tv-c-a-selection-summary-table');
+    assert.isDefined(selectionSummaryTableEl);
+    assert.equal(selectionSummaryTableEl.selection, selection);
+
+    var detailsTableEl = tv.b.findDeepElementMatching(
+        viewEl, 'tv-c-a-multi-event-details-table');
+    assert.isUndefined(detailsTableEl);
+  });
+
+  test('sameTitles', function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(newSliceNamed('c', 0.0, 0.04));
+    t53.sliceGroup.pushSlice(newSliceNamed('c', 0.12, 0.06));
+    t53.sliceGroup.createSubSlices();
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-sub-view');
+    viewEl.selection = selection;
+    this.addHTMLOutput(viewEl);
+
+    var summaryTableEl = tv.b.findDeepElementMatching(
+        viewEl, 'tv-c-a-multi-event-summary-table');
+    assert.isDefined(summaryTableEl);
+
+    assert.isFalse(summaryTableEl.showTotals);
+    assert.equal(tv.b.dictionaryLength(summaryTableEl.eventsByTitle), 1);
+
+    var selectionSummaryTableEl = tv.b.findDeepElementMatching(
+        viewEl, 'tv-c-a-selection-summary-table');
+    assert.isDefined(selectionSummaryTableEl);
+    assert.equal(selectionSummaryTableEl.selection, selection);
+
+    var detailsTableEl = tv.b.findDeepElementMatching(
+        viewEl, 'tv-c-a-multi-event-details-table');
+        assert.isDefined(detailsTableEl);
+    assert.equal(detailsTableEl.selection, selection);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_summary.html b/trace-viewer/trace_viewer/core/analysis/multi_event_summary.html
new file mode 100644
index 0000000..cce0912
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_summary.html
@@ -0,0 +1,138 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/base/statistics.html">
+
+<script>
+'use strict';
+tv.exportTo('tv.c.analysis', function() {
+  function MultiEventSummary(title, events) {
+    this.title = title;
+    this.duration_ = undefined;
+    this.selfTime_ = undefined;
+    this.events_ = events;
+
+    this.cpuTimesComputed_ = false;
+    this.cpuSelfTime_ = undefined;
+    this.cpuDuration_ = undefined;
+
+    this.untotallableArgs_ = [];
+    this.totalledArgs_ = undefined;
+  };
+  MultiEventSummary.prototype = {
+    get duration() {
+      if (this.duration_ === undefined) {
+        this.duration_ = tv.b.Statistics.sum(
+            this.events_, function(event) {
+                return event.duration;
+            });
+      }
+      return this.duration_;
+    },
+
+    get cpuSelfTime() {
+      this.computeCpuTimesIfNeeded_();
+      return this.cpuSelfTime_;
+    },
+
+    get cpuDuration() {
+      this.computeCpuTimesIfNeeded_();
+      return this.cpuDuration_;
+    },
+
+    computeCpuTimesIfNeeded_: function() {
+      if (this.cpuTimesComputed_)
+        return;
+      this.cpuTimesComputed_ = true;
+
+      var cpuSelfTime = 0;
+      var cpuDuration = 0;
+      var hasCpuData = false;
+      for (var i = 0; i < this.events_.length; i++) {
+        var event = this.events_[i];
+        if (event.cpuDuration !== undefined) {
+          cpuDuration += event.cpuDuration;
+          hasCpuData = true;
+        }
+
+        if (event.cpuSelfTime !== undefined) {
+          cpuSelfTime += event.cpuSelfTime;
+          hasCpuData = true;
+        }
+      }
+      if (hasCpuData) {
+        this.cpuDuration_ = cpuDuration;
+        this.cpuSelfTime_ = cpuSelfTime;
+      }
+    },
+
+    get selfTime() {
+      if (this.selfTime_ === undefined) {
+        this.selfTime_ = 0;
+        for (var i = 0; i < this.events_.length; i++) {
+          if (this.events_[i].selfTime !== undefined)
+            this.selfTime_ += this.events[i].selfTime;
+        }
+      }
+      return this.selfTime_;
+    },
+
+    get events() {
+      return this.events_;
+    },
+
+    get numEvents() {
+      return this.events_.length;
+    },
+
+    get untotallableArgs() {
+      this.updateArgsIfNeeded_();
+      return this.untotallableArgs_;
+    },
+
+    get totalledArgs() {
+      this.updateArgsIfNeeded_();
+      return this.totalledArgs_;
+    },
+
+    updateArgsIfNeeded_: function() {
+      if (this.totalledArgs_ !== undefined)
+        return;
+
+      var untotallableArgs = {};
+      var totalledArgs = {};
+      for (var i = 0; i < this.events_.length; i++) {
+        var event = this.events_[i];
+        for (var argName in event.args) {
+          var argVal = event.args[argName];
+          var type = typeof argVal;
+          if (type !== 'number') {
+            untotallableArgs[argName] = true;
+            delete totalledArgs[argName];
+            continue;
+          }
+          if (untotallableArgs[argName]) {
+            continue;
+          }
+
+          if (totalledArgs[argName] === undefined)
+            totalledArgs[argName] = 0;
+          totalledArgs[argName] += argVal;
+        }
+      }
+      this.untotallableArgs_ = tv.b.dictionaryKeys(untotallableArgs);
+      this.totalledArgs_ = totalledArgs;
+    }
+  };
+
+  return {
+    MultiEventSummary: MultiEventSummary
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_summary_table.html b/trace-viewer/trace_viewer/core/analysis/multi_event_summary_table.html
new file mode 100644
index 0000000..8e75daa
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_summary_table.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/base/statistics.html">
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/analysis/multi_event_summary.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+<link rel="import" href="/core/analysis/time_span.html">
+
+</script>
+<polymer-element name='tv-c-a-multi-event-summary-table'>
+  <template>
+    <style>
+    :host {
+      display: flex;
+    }
+    #table {
+      flex: 1 1 auto;
+      align-self: stretch;
+    }
+    </style>
+    <tracing-analysis-nested-table id="table">
+    </tracing-analysis-nested-table>
+    </div>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.showTotals_ = false;
+      this.eventsByTitle_ = undefined;
+    },
+
+    updateTableColumns_: function(rows) {
+      var hasCpuData = false;
+      rows.forEach(function(row) {
+        if (row.cpuDuration !== undefined)
+          hasCpuData = true;
+        if (row.cpuSelfTime !== undefined)
+          hasCpuData = true;
+      });
+
+      var colWidthPercentage;
+      if (hasCpuData)
+        colWidthPercentage = '20%';
+      else
+        colWidthPercentage = '33.3333%';
+
+      var columns = [];
+
+      columns.push({
+        title: 'Name',
+        value: function(row) {
+          if (row.title === 'Totals')
+            return 'Totals';
+
+          var linkEl = document.createElement('tv-c-analysis-link');
+          linkEl.setSelectionAndContent(function() {
+            return new tv.c.Selection(row.events);
+          }, row.title);
+          return linkEl;
+        },
+        width: '350px',
+        cmp: function(rowA, rowB) {
+          return rowA.title.localeCompare(rowB.title);
+        }
+      });
+      columns.push({
+        title: 'Wall Duration (ms)',
+        value: function(row) {
+          return tv.c.analysis.createTimeSpan(row.duration);
+        },
+        width: colWidthPercentage,
+        cmp: function(rowA, rowB) {
+          return rowA.duration - rowB.duration;
+        }
+      });
+
+      if (hasCpuData) {
+        columns.push({
+          title: 'CPU Duration (ms)',
+          value: function(row) {
+            return tv.c.analysis.createTimeSpan(row.cpuDuration);
+          },
+          width: colWidthPercentage,
+          cmp: function(rowA, rowB) {
+            return rowA.cpuDuration - rowB.cpuDuration;
+          }
+        });
+      }
+
+      columns.push({
+        title: 'Self time (ms)',
+        value: function(row) {
+          return tv.c.analysis.createTimeSpan(row.selfTime);
+        },
+        width: colWidthPercentage,
+        cmp: function(rowA, rowB) {
+          return rowA.selfTime - rowB.selfTime;
+        }
+      });
+      if (hasCpuData) {
+        columns.push({
+          title: 'CPU Self Time (ms)',
+          value: function(row) {
+            return tv.c.analysis.createTimeSpan(row.cpuSelfTime);
+          },
+          width: colWidthPercentage,
+          cmp: function(rowA, rowB) {
+            return rowA.cpuSelfTime - rowB.cpuSelfTime;
+          }
+        });
+      }
+      columns.push({
+        title: 'Occurrences',
+        value: function(row) {
+          return row.numEvents;
+        },
+        width: colWidthPercentage,
+        cmp: function(rowA, rowB) {
+          return rowA.numEvents - rowB.numEvents;
+        }
+      });
+
+      this.$.table.tableColumns = columns;
+    },
+
+    configure: function(config) {
+      this.showTotals_ = config.showTotals;
+      this.eventsByTitle_ = config.eventsByTitle;
+      this.updateContents_();
+    },
+
+    get showTotals() {
+      return this.showTotals_;
+    },
+
+    set showTotals(showTotals) {
+      this.showTotals_ = showTotals;
+      this.updateContents_();
+    },
+
+    get eventsByTitle() {
+      return this.eventsByTitle_;
+    },
+
+    set eventsByTitle(eventsByTitle) {
+      this.eventsByTitle_ = eventsByTitle;
+      this.appendChild(this.updateContents_());
+    },
+
+    get selectionBounds() {
+      return this.selectionBounds_;
+    },
+
+    set selectionBounds(selectionBounds) {
+      this.selectionBounds_ = selectionBounds;
+      this.updateContents_();
+    },
+
+    updateContents_: function() {
+      var eventsByTitle;
+      if (this.eventsByTitle_ !== undefined)
+        eventsByTitle = this.eventsByTitle_;
+      else
+        eventsByTitle = [];
+
+      var allEvents = [];
+      var rows = [];
+      tv.b.iterItems(
+          eventsByTitle,
+          function(title, eventsOfSingleTitle) {
+            allEvents.push.apply(allEvents, eventsOfSingleTitle);
+            var row = new tv.c.analysis.MultiEventSummary(title,
+                                                          eventsOfSingleTitle);
+            rows.push(row);
+          });
+
+      this.updateTableColumns_(rows);
+      this.$.table.tableRows = rows;
+
+      var footerRows = [];
+
+      if (this.showTotals_) {
+        footerRows.push(
+            new tv.c.analysis.MultiEventSummary('Totals', allEvents));
+      }
+      // TODO(selection bounds).
+
+      // TODO(sorting)
+
+      this.$.table.footerRows = footerRows;
+      this.$.table.rebuild();
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_summary_table_test.html b/trace-viewer/trace_viewer/core/analysis/multi_event_summary_table_test.html
new file mode 100644
index 0000000..32c9ddf
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_summary_table_test.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/deep_utils.html">
+<link rel="import" href="/core/analysis/multi_event_summary_table.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var newSliceEx = tv.c.test_utils.newSliceEx;
+
+  test('basicNoCpu', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, duration: 0.5}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, duration: 0.5}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 2, duration: 0.5}));
+    tsg.createSubSlices();
+
+    var threadTrack = {};
+    threadTrack.thread = thread;
+
+    var selection = new Selection(tsg.slices);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-summary-table');
+    viewEl.configure({
+      showTotals: true,
+      eventsByTitle: selection.getEventsOrganizedByTitle()
+    });
+    this.addHTMLOutput(viewEl);
+  });
+
+  test('basicWithCpu', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+                              cpuStart: 0, cpuEnd: 3}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+                              cpuStart: 1, cpuEnd: 1.75}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+                              cpuStart: 3, cpuEnd: 3.75}));
+    tsg.createSubSlices();
+
+    var threadTrack = {};
+    threadTrack.thread = thread;
+
+    var selection = new Selection(tsg.slices);
+
+    var viewEl = document.createElement('tv-c-a-multi-event-summary-table');
+    viewEl.configure({
+      showTotals: true,
+      eventsByTitle: selection.getEventsOrganizedByTitle()
+    });
+    this.addHTMLOutput(viewEl);
+  });
+
+  // TODO(nduca): Tooltippish stuff.
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_event_summary_test.html b/trace-viewer/trace_viewer/core/analysis/multi_event_summary_test.html
new file mode 100644
index 0000000..e69c68a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_event_summary_test.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/multi_event_summary.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var newSliceEx = tv.c.test_utils.newSliceEx;
+
+  test('summaryRowNoCpu', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+    tsg.pushSlice(newSliceEx({title: 'bb', start: 1, end: 2}));
+    tsg.pushSlice(newSliceEx({title: 'bb', start: 4, end: 5}));
+    tsg.createSubSlices();
+
+    var row = new tv.c.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+    assert.equal(row.duration, 5);
+    assert.equal(row.selfTime, 4);
+    assert.isUndefined(row.cpuDuration);
+    assert.isUndefined(row.cpuSelfTime);
+  });
+
+  test('summaryRowWithCpu', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+                              cpuStart: 0, cpuEnd: 3}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+                              cpuStart: 1, cpuEnd: 1.75}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+                              cpuStart: 3, cpuEnd: 3.75}));
+    tsg.createSubSlices();
+
+    var row = new tv.c.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+    assert.equal(row.duration, 5);
+    assert.equal(row.selfTime, 4);
+    assert.equal(row.cpuDuration, 4.5);
+    assert.equal(row.cpuSelfTime, 3.75);
+  });
+
+  test('summaryRowNonSlice', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+
+    var fe1 = new tv.c.trace_model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+    var fe2 = new tv.c.trace_model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+    model.flowEvents.push(fe1);
+    model.flowEvents.push(fe2);
+
+    var row = new tv.c.analysis.MultiEventSummary('a', [fe1, fe2]);
+    assert.equal(row.duration, 0);
+    assert.equal(row.selfTime, 0);
+    assert.isUndefined(row.cpuDuration);
+    assert.isUndefined(row.cpuSelfTime);
+  });
+
+  test('argSummary', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3,
+                              args: {value1: 3, value2: 'x', value3: 1}}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2,
+                              args: {value1: 3, value2: 'y', value3: 2}}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 4, end: 5,
+                              args: {value1: 3, value2: 'z', value3: 'x'}}));
+    tsg.createSubSlices();
+
+    var row = new tv.c.analysis.MultiEventSummary('x', tsg.slices.slice(0));
+    assert.deepEqual(row.totalledArgs, {value1: 9});
+    assert.deepEqual(row.untotallableArgs, ['value2', 'value3']);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_flow_event_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_flow_event_sub_view.html
new file mode 100644
index 0000000..93dca2b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_flow_event_sub_view.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/multi_slice_sub_view.html">
+
+<polymer-element name="tv-c-multi-flow-event-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.$.content.textContent = '';
+      var realView = document.createElement('tv-c-multi-slice-sub-view');
+
+      this.$.content.appendChild(realView);
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.currentSelection_ = selection;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_flow_event_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/multi_flow_event_sub_view_test.html
new file mode 100644
index 0000000..f4ddca9
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_flow_event_sub_view_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var trace_model = tv.c.trace_model;
+
+  test('analyzeSelectionWithSingleEvent', function() {
+    var model = new Model();
+
+    var fe1 = new trace_model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+    var fe2 = new trace_model.FlowEvent('cat', 1234, 'title', 8, 20, {});
+    model.flowEvents.push(fe1);
+    model.flowEvents.push(fe2);
+
+    var selection = new Selection();
+    selection.push(fe1);
+    selection.push(fe2);
+    assert.equal(selection.length, 2);
+
+    var subView = document.createElement('tv-c-multi-flow-event-sub-view');
+    subView.selection = selection;
+
+    this.addHTMLOutput(subView);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_global_memory_dump_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_global_memory_dump_sub_view.html
new file mode 100644
index 0000000..8effd0f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_global_memory_dump_sub_view.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<polymer-element name="tv-c-multi-global-memory-dump-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.currentSelection_ = selection;
+      this.textContent = '';
+
+      selection = tv.b.asArray(selection).sort(
+          tv.b.Range.compareByMinTimes);
+
+      var results = new tv.c.analysis.AnalysisResults();
+      this.appendChild(results);
+
+      var table = results.appendTable('analysis-global-memory-dump-table', 1);
+
+      selection.forEach(function(dump) {
+        var row = results.appendBodyRow(table);
+        var linkContainer = results.appendTableCell(table, row, '');
+        var label = 'Dump at ' + tv.c.analysis.tsString(dump.start);
+        var selectionGenerator = function() {
+          var selection = new tv.c.Selection();
+          selection.push(dump);
+          return selection;
+        }
+        linkContainer.appendChild(results.createSelectionChangingLink(
+            label, selectionGenerator));
+      });
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_instant_event_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_instant_event_sub_view.html
new file mode 100644
index 0000000..40151d8
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_instant_event_sub_view.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/multi_slice_sub_view.html">
+
+<polymer-element name="tv-c-multi-instant-event-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.$.content.textContent = '';
+      var realView = document.createElement('tv-c-multi-slice-sub-view');
+
+      this.$.content.appendChild(realView);
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.currentSelection_ = selection;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_instant_event_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/multi_instant_event_sub_view_test.html
new file mode 100644
index 0000000..960ad64
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_instant_event_sub_view_test.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var trace_model = tv.c.trace_model;
+
+  test('analyzeSelectionWithSingleEvent', function() {
+    var model = new Model();
+    var p52 = model.getOrCreateProcess(52);
+    var t53 = p52.getOrCreateThread(53);
+
+    var ie1 = new trace_model.ProcessInstantEvent('cat', 'title', 7, 10, {});
+    ie1.duration = 20;
+    var ie2 = new trace_model.ProcessInstantEvent('cat', 'title', 7, 20, {});
+    ie2.duration = 30;
+    p52.instantEvents.push(ie1);
+    p52.instantEvents.push(ie2);
+
+
+    var selection = new Selection();
+    selection.push(ie1);
+    selection.push(ie2);
+    assert.equal(selection.length, 2);
+
+    var subView = document.createElement('tv-c-multi-instant-event-sub-view');
+    subView.selection = selection;
+
+    this.addHTMLOutput(subView);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_interaction_record_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_interaction_record_sub_view.html
new file mode 100644
index 0000000..bda70f7
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_interaction_record_sub_view.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/multi_slice_sub_view.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<polymer-element name="tv-c-multi-interaction-record-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.currentSelection_ = selection;
+      this.textContent = '';
+      var realView = document.createElement('tv-c-multi-slice-sub-view');
+
+      this.appendChild(realView);
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.currentSelection_ = selection;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_object_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_object_sub_view.html
new file mode 100644
index 0000000..be7d5a0
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_object_sub_view.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<polymer-element name="tv-c-multi-object-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    set selection(selection) {
+      this.currentSelection_ = selection;
+      this.textContent = '';
+
+      var results = new tv.c.analysis.AnalysisResults();
+      this.appendChild(results);
+
+      this.analyzeObjectEvents_(results, selection);
+    },
+
+    /**
+     * Extremely simplistic analysis of objects. Mainly exists to provide
+     * click-through to the main object's analysis view.
+     */
+    analyzeObjectEvents_: function(results, objectEvents) {
+      objectEvents = tv.b.asArray(objectEvents).sort(
+          tv.b.Range.compareByMinTimes);
+
+      var table = results.appendTable('analysis-object-sample-table', 2);
+
+      objectEvents.forEach(function(event) {
+        var row = results.appendBodyRow(table);
+        var ts;
+        var objectText;
+        var selectionGenerator;
+        if (event instanceof tv.c.trace_model.ObjectSnapshot) {
+          var objectSnapshot = event;
+          ts = tv.c.analysis.tsString(objectSnapshot.ts);
+          objectText = objectSnapshot.objectInstance.typeName + ' ' +
+              objectSnapshot.objectInstance.id;
+          selectionGenerator = function() {
+            var selection = new tv.c.Selection();
+            selection.push(objectSnapshot);
+            return selection;
+          };
+        } else {
+          var objectInstance = event;
+
+          var deletionTs = objectInstance.deletionTs == Number.MAX_VALUE ?
+              '' : tv.c.analysis.tsString(objectInstance.deletionTs);
+          ts = tv.c.analysis.tsString(objectInstance.creationTs) +
+              ' - ' + deletionTs;
+
+          objectText = objectInstance.typeName + ' ' +
+              objectInstance.id;
+
+          selectionGenerator = function() {
+            var selection = new tv.c.Selection();
+            selection.push(objectInstance);
+            return selection;
+          };
+        }
+
+        results.appendTableCell(table, row, ts);
+        var linkContainer = results.appendTableCell(table, row, '');
+        linkContainer.appendChild(
+            results.createSelectionChangingLink(
+                objectText,
+                selectionGenerator));
+      });
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_object_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/multi_object_sub_view_test.html
new file mode 100644
index 0000000..ebcacc3
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_object_sub_view_test.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var TraceModel = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var ObjectInstance = tv.c.trace_model.ObjectInstance;
+
+  test('instantiate_analysisWithObjects', function() {
+    var model = new TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var objects = p1.objects;
+    var i10 = objects.idWasCreated(
+        '0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 10);
+    var s10 = objects.addSnapshot('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 10,
+                                  'snapshot-1');
+    var s25 = objects.addSnapshot('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 25,
+                                  'snapshot-2');
+    var s40 = objects.addSnapshot('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 40,
+                                  'snapshot-3');
+    objects.idWasDeleted('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 45);
+
+    var track = {};
+    var selection = new Selection();
+    selection.push(i10);
+    selection.push(s10);
+    selection.push(s25);
+    selection.push(s40);
+
+    var analysisEl = document.createElement('tv-c-multi-object-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_sample_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_sample_sub_view.html
new file mode 100644
index 0000000..255b458
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_sample_sub_view.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+
+<polymer-element name="tv-c-multi-sample-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    get requiresTallView() {
+      return true;
+    },
+
+    set selection(selection) {
+      this.$.content.textContent = '';
+      this.currentSelection_ = selection;
+
+      if (tv.isDefined('tv.e.analysis.SamplingSummaryPanel')) {
+        var panel = new tv.e.analysis.SamplingSummaryPanel();
+        this.$.content.appendChild(panel);
+        panel.selection = selection;
+      } else {
+        this.$.content.textContent = 'SamplingSummaryPanel not installed. :(';
+      }
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_sample_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/multi_sample_sub_view_test.html
new file mode 100644
index 0000000..411864c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_sample_sub_view_test.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel='import' href='/extras/analysis/sampling_summary.html'>
+<link rel="import" href="/core/analysis/multi_sample_sub_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var newSampleNamed = tv.c.test_utils.newSampleNamed;
+
+  test('instantiate_withMultipleSamples', function() {
+    var model = new Model();
+    var t53;
+    model.importTraces([], false, false, function() {
+      t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['BBB'], 0));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['AAA'], 0.02));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['AAA'], 0.04));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['Sleeping'], 0.06));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['BBB'], 0.08));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['AAA'], 0.10));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['CCC'], 0.12));
+      model.samples.push(newSampleNamed(t53, 'X', 'cat', ['Sleeping'], 0.14));
+    });
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    for (var i = 0; i < t53.samples.length; i++)
+      selection.push(t53.samples[i]);
+
+    var view = document.createElement('tv-c-multi-sample-sub-view');
+    view.style.height = '500px';
+    this.addHTMLOutput(view);
+    view.selection = selection;
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_slice_sub_view.html b/trace-viewer/trace_viewer/core/analysis/multi_slice_sub_view.html
new file mode 100644
index 0000000..e010e02
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_slice_sub_view.html
@@ -0,0 +1,304 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/sortable_table.html">
+
+<polymer-element name="tv-c-multi-slice-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <link rel='stylesheet' href='/core/analysis/analysis_results.css'>
+    <style>
+    :host {
+      display: flex;
+    }
+    #content {
+      flex: 1 1 auto;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+      this.requiresTallView_ = false;
+    },
+
+    set selection(selection) {
+      if (selection.length <= 1)
+        throw new Error('Only supports multiple items');
+      if (!selection.every(
+          function(x) { return x instanceof tv.c.trace_model.Slice; })) {
+        throw new Error('Only supports slices');
+      }
+      this.setSelectionWithoutErrorChecks(selection);
+    },
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    get requiresTallView() {
+      return this.requiresTallView_;
+    },
+
+    setSelectionWithoutErrorChecks: function(selection) {
+      this.currentSelection_ = selection;
+      this.$.content.textContent = '';
+      this.requiresTallView_ = false;
+
+      // TODO(nduca): This is a gross hack for cc Frame Viewer, but its only
+      // the frame viewer that needs this feature, so ~shrug~.
+      if (window.RasterTaskView !== undefined) { // May not have been imported.
+        if (tv.e.cc.RasterTaskSelection.supports(selection)) {
+          var ltvSelection = new tv.e.cc.RasterTaskSelection(selection);
+
+          var ltv = new tv.e.cc.LayerTreeHostImplSnapshotView();
+          ltv.objectSnapshot = ltvSelection.containingSnapshot;
+          ltv.selection = ltvSelection;
+          ltv.extraHighlightsByLayerId = ltvSelection.extraHighlightsByLayerId;
+          this.$.content.appendChild(ltv);
+
+          this.style.display = 'flex';
+
+          this.requiresTallView_ = true;
+          return;
+        }
+      }
+      this.style.display = '';
+
+      var results = new tv.c.analysis.AnalysisResults();
+      this.$.content.appendChild(results);
+
+      this.analyze_(results, selection);
+    },
+
+    analyze_: function(results, selection) {
+      var info = this.buildSliceGroups_(selection);
+      var table = this.analyzeMultipleSlices_(results, info.sliceGroups,
+          info.hasCpuDuration);
+
+      // Only one row so we already know the totals.
+      var keys = Object.keys(info.sliceGroups);
+      if (keys.length === 1) {
+        // The whole selection is a single type so list out the information
+        // for each sub slice.
+        var sliceGroup = info.sliceGroups[keys[0]];
+
+        results.appendInfo('Title: ', sliceGroup.slices[0].title);
+        results.appendInfo('Category: ', sliceGroup.slices[0].category);
+
+        var single_type_info = this.analyzeSingleTypeSlices_(
+            results, sliceGroup,
+            info.hasCpuDuration);
+
+        if (info.sliceGroups[keys[0]].slices.length > 1) {
+          results.appendDetailsRow(single_type_info.table, undefined,
+              sliceGroup.duration,
+              sliceGroup.selfTime,
+              single_type_info.args, undefined,
+              info.hasCpuDuration ? sliceGroup.cpuDuration : undefined,
+              true);
+          tv.b.ui.SortableTable.decorate(table);
+        }
+
+      } else {
+        results.appendDataRow(table, 'Totals',
+            info.totals.duration,
+            info.hasCpuDuration ? info.totals.cpuDuration : null,
+            info.totals.selfTime,
+            info.hasCpuDuration ? info.totals.cpuSelfTime : null,
+            selection.length,
+            null, null, null, true);
+        results.appendSpacingRow(table, true);
+        tv.b.ui.SortableTable.decorate(table);
+      }
+
+      var tsLo = selection.bounds.min;
+      var tsHi = selection.bounds.max;
+      results.appendInfoRowTime(table,
+          'Selection start', tsLo, true);
+      results.appendInfoRowTime(table,
+          'Selection extent', tsHi - tsLo, true);
+    },
+
+    buildSliceGroups_: function(slices) {
+      var sliceGroups = {};
+      var hasCpuDuration = false;
+      var totals = {
+          duration: 0,
+          cpuDuration: 0,
+          cpuSelfTime: 0,
+          selfTime: 0
+      };
+
+      for (var i = 0; i < slices.length; i++) {
+        var slice = slices[i];
+        if (sliceGroups[slice.title] === undefined) {
+          sliceGroups[slice.title] = {
+            slices: [],
+            duration: 0,
+            cpuDuration: 0,
+            selfTime: 0,
+            cpuSelfTime: 0,
+            startOfFirstOccurrence: Number.MAX_VALUE,
+            startOfLastOccurrence: -Number.MAX_VALUE,
+            min: Number.MAX_VALUE,
+            max: -Number.MAX_VALUE
+          };
+        }
+
+        if (slice.cpuDuration)
+          hasCpuDuration = true;
+
+        var sliceGroup = sliceGroups[slice.title];
+
+        sliceGroup.duration += slice.duration;
+        totals.duration += slice.duration;
+
+        if (slice.cpuDuration) {
+          sliceGroup.cpuDuration += slice.cpuDuration;
+          totals.cpuDuration += slice.cpuDuration;
+
+          sliceGroup.cpuSelfTime +=
+              slice.cpuSelfTime ? slice.cpuSelfTime : slice.cpuDuration;
+          totals.cpuSelfTime +=
+              slice.cpuSelfTime ? slice.cpuSelfTime : slice.cpuDuration;
+        }
+
+        sliceGroup.selfTime += slice.selfTime ? slice.selfTime : slice.duration;
+        totals.selfTime += slice.selfTime ? slice.selfTime : slice.duration;
+
+        sliceGroup.startOfFirstOccurrence =
+            Math.min(slice.start, sliceGroup.startOfFirstOccurrence);
+        sliceGroup.startOfLastOccurrence =
+            Math.max(slice.start, sliceGroup.startOfLastOccurrence);
+        sliceGroup.min = Math.min(slice.duration, sliceGroup.min);
+        sliceGroup.max = Math.max(slice.duration, sliceGroup.max);
+
+        sliceGroup.slices.push(slices[i]);
+      }
+
+      return {
+          hasCpuDuration: hasCpuDuration,
+          sliceGroups: sliceGroups,
+          totals: totals
+      };
+    },
+
+    analyzeSingleTypeSlices_: function(results, sliceGroup, hasCpuDuration) {
+      var table = results.appendTable('analysis-slice-table',
+                                      4 + hasCpuDuration);
+      var row = results.appendHeadRow(table);
+      results.appendTableCell(table, row, 'Start');
+      results.appendTableCell(table, row, 'Wall Duration (ms)');
+      if (hasCpuDuration)
+        results.appendTableCell(table, row, 'CPU Duration (ms)');
+      results.appendTableCell(table, row, 'Self Time (ms)');
+      results.appendTableCell(table, row, 'Args');
+
+      var totalArg = {};
+      tv.b.iterItems(sliceGroup.slices, function(title, slice) {
+        results.appendDetailsRow(table, slice.start, slice.duration,
+            slice.selfTime ? slice.selfTime : slice.duration, slice.args,
+            function() {
+              return new tv.c.Selection([slice]);
+            }, slice.cpuDuration, false);
+
+        for (var argName in slice.args) {
+          var argVal = slice.args[argName];
+          var type = (typeof argVal);
+          if (type == 'number') {
+            if (totalArg[argName] == null)
+              totalArg[argName] = 0;
+            totalArg[argName] += argVal;
+          }
+        }
+      });
+      return {table: table, args: totalArg};
+    },
+
+    analyzeMultipleSlices_: function(results, sliceGroups, hasCpuDuration) {
+      var table = results.appendTable('analysis-slice-table',
+                                      4 + hasCpuDuration);
+      var row = results.appendHeadRow(table);
+      results.appendTableCell(table, row, 'Name');
+      results.appendTableCell(table, row, 'Wall Duration (ms)');
+      if (hasCpuDuration)
+        results.appendTableCell(table, row, 'CPU Duration (ms)');
+      results.appendTableCell(table, row, 'Self Time (ms)');
+      if (hasCpuDuration)
+        results.appendTableCell(table, row, 'CPU Self Time (ms)');
+      results.appendTableCell(table, row, 'Occurrences');
+
+      var thisComponent = this;
+
+      tv.b.iterItems(sliceGroups, function(sliceGroupTitle, sliceGroup) {
+        var slices = sliceGroup.slices;
+        var avg = sliceGroup.duration / slices.length;
+
+        var statistics = {
+          min: sliceGroup.min,
+          max: sliceGroup.max,
+          avg: avg,
+          avg_stddev: undefined,
+          frequency: undefined,
+          frequency_stddev: undefined
+        };
+
+        // Compute the stddev of the slice durations.
+        var sumOfSquaredDistancesToMean = 0;
+        for (var i = 0; i < slices.length; i++) {
+          var signedDistance = statistics.avg - slices[i].duration;
+          sumOfSquaredDistancesToMean += signedDistance * signedDistance;
+        }
+
+        statistics.avg_stddev =
+            Math.sqrt(sumOfSquaredDistancesToMean / (slices.length - 1));
+
+        // We require at least 3 samples to compute the stddev.
+        var elapsed = sliceGroup.startOfLastOccurrence -
+            sliceGroup.startOfFirstOccurrence;
+        if (slices.length > 2 && elapsed > 0) {
+          var numDistances = slices.length - 1;
+          statistics.frequency = (1000 * numDistances) / elapsed;
+
+          // Compute the stddev.
+          sumOfSquaredDistancesToMean = 0;
+          for (var i = 1; i < slices.length; i++) {
+            var currentFrequency =
+                1000 / (slices[i].start - slices[i - 1].start);
+            var signedDistance = statistics.frequency - currentFrequency;
+            sumOfSquaredDistancesToMean += signedDistance * signedDistance;
+          }
+
+          statistics.frequency_stddev =
+              Math.sqrt(sumOfSquaredDistancesToMean / (numDistances - 1));
+        }
+        results.appendDataRow(table, sliceGroupTitle, sliceGroup.duration,
+                              hasCpuDuration ? (sliceGroup.cpuDuration > 0 ?
+                                  sliceGroup.cpuDuration : '') : null,
+                              sliceGroup.selfTime,
+                              hasCpuDuration ? (sliceGroup.cpuSelfTime > 0 ?
+                                  sliceGroup.cpuSelfTime : '') : null,
+                              slices.length, null, statistics, function() {
+                                return new tv.c.Selection(slices);
+                              });
+      });
+
+      return table;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/multi_slice_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/multi_slice_sub_view_test.html
new file mode 100644
index 0000000..1abe27d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/multi_slice_sub_view_test.html
@@ -0,0 +1,330 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/analysis/stub_analysis_results.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Thread = tv.c.trace_model.Thread;
+  var Selection = tv.c.Selection;
+  var StubAnalysisResults = tv.c.analysis.StubAnalysisResults;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+  var newSliceCategory = tv.c.test_utils.newSliceCategory;
+  var Slice = tv.c.trace_model.Slice;
+
+  var createSelectionWithTwoSlices = function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(newSliceNamed('a', 0.0, 0.04));
+    t53.sliceGroup.pushSlice(newSliceNamed('aa', 0.120, 0.06));
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+
+    return selection;
+  };
+
+  var createSelectionWithTwoSlicesSameTitle = function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(newSliceNamed('c', 0.0, 0.04));
+    t53.sliceGroup.pushSlice(newSliceNamed('c', 0.12, 0.06));
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+
+    return selection;
+  };
+
+  var createSelectionWithOneArg = function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.0, {'arg1': 3.14}, 0.04));
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.12, {'arg1': 6.28}, 0.06));
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+
+    return selection;
+  };
+
+  var createSelectionWithOneNumberOneTextArg = function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.0,
+        {'arg1': 3.14, 'arg2': 'text1'}, 0.04));
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.12,
+        {'arg1': 6.28, 'arg2': 'text2'}, 0.06));
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+
+    return selection;
+  };
+
+  var createSelectionWithTwoNumberArgs = function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.0,
+        {'arg1': 3.14, 'arg2': 200.0}, 0.04));
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.12,
+        {'arg1': 6.28, 'arg2': 100.0}, 0.06));
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+
+    return selection;
+  };
+
+  var createSelectionWithMismatchedArgs = function() {
+    var model = new Model();
+    var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+    // Two numbers
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.0,
+        {'arg1': 3.14, 'arg2': 200.0}, 0.04));
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.12,
+        {'arg1': 6.28, 'arg2': 100.0}, 0.06));
+    // One number, missing arg1.
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.14,
+        {'arg2': 50.0}, 0.08));
+    // One number, arg2 is not numeric.
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.17,
+        {'arg1': 1.0, 'arg2': 'text'}, 0.1));
+    // Missing both args.
+    t53.sliceGroup.pushSlice(new Slice('', 'c', 0, 0.19,
+        {}, 0.12));
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    selection.push(t53.sliceGroup.slices[1]);
+    selection.push(t53.sliceGroup.slices[2]);
+    selection.push(t53.sliceGroup.slices[3]);
+    selection.push(t53.sliceGroup.slices[4]);
+
+    return selection;
+  };
+
+  test('instantiate_withMultipleSlices', function() {
+    var selection = createSelectionWithTwoSlices();
+
+    var analysisEl = new TracingAnalysisView();
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withMultipleSlicesSameTitle', function() {
+    var selection = createSelectionWithTwoSlicesSameTitle();
+
+    var analysisEl = new TracingAnalysisView();
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withMultipleSlicesOneArg', function() {
+    var selection = createSelectionWithOneArg();
+
+    var analysisEl = new TracingAnalysisView();
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withMultipleSlicesOneNumberOneTextArg', function() {
+    var selection = createSelectionWithOneNumberOneTextArg();
+
+    var analysisEl = new TracingAnalysisView();
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withMultipleSlicesTwoNumberArgs', function() {
+    var selection = createSelectionWithTwoNumberArgs();
+
+    var analysisEl = new TracingAnalysisView();
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withMultipleSlicesMismatchedArgs', function() {
+    var selection = createSelectionWithMismatchedArgs();
+
+    var analysisEl = new TracingAnalysisView();
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('analyzeSelectionWithTwoSlices', function() {
+    var selection = createSelectionWithTwoSlices();
+
+    var sliceView = document.createElement('tv-c-multi-slice-sub-view');
+    var results = new StubAnalysisResults();
+
+    sliceView.analyze_(results, selection);
+    assert.equal(results.tables.length, 1);
+    var table = results.tables[0];
+    assert.equal(table.rows.length, 6);
+
+    assert.equal(table.rows[0].label, 'a');
+    assert.equal(table.rows[0].occurrences, 1);
+    assert.closeTo(0.04, table.rows[0].duration, 1e-5);
+    assert.closeTo(0.04, table.rows[0].selfTime, 1e-5);
+    assert.isNull(table.rows[0].cpuDuration);
+    assert.equal(table.rows[1].label, 'aa');
+    assert.equal(table.rows[1].occurrences, 1);
+    assert.closeTo(0.06, table.rows[1].duration, 1e-5);
+    assert.closeTo(0.06, table.rows[1].selfTime, 1e-5);
+    assert.isNull(table.rows[1].cpuDuration);
+    assert.equal(table.rows[2].label, 'Totals');
+    assert.equal(table.rows[2].occurrences, 2);
+    assert.closeTo(0.1, table.rows[2].duration, 1e-5);
+    assert.closeTo(0.1, table.rows[2].selfTime, 1e-5);
+    assert.isNull(table.rows[2].cpuDuration);
+
+    assert.equal(table.rows[4].label, 'Selection start');
+    assert.closeTo(0, table.rows[4].time, 1e-5);
+
+    assert.equal(table.rows[5].label, 'Selection extent');
+    assert.closeTo(0.18, table.rows[5].time, 1e-5);
+  });
+
+  test('analyzeSelectionWithTwoSlicesSameTitle', function() {
+    var selection = createSelectionWithTwoSlicesSameTitle();
+
+    var sliceView = document.createElement('tv-c-multi-slice-sub-view');
+    var results = new StubAnalysisResults();
+    sliceView.analyze_(results, selection);
+    assert.equal(results.tables.length, 2);
+
+    var t;
+    // Table 1.
+    t = results.tables[0];
+    assert.equal(t.rows[0].label, 'c');
+    assert.equal(t.rows[0].duration, 0.1);
+    assert.isNull(t.rows[0].cpuDuration);
+    assert.equal(t.rows[0].selfTime, 0.1);
+    assert.isNull(t.rows[0].cpuSelfTime);
+    assert.equal(t.rows[0].occurrences, 2);
+    assert.isNull(t.rows[0].percentage);
+    assert.deepEqual({
+      min: 0.04, max: 0.06, avg: 0.05,
+      avg_stddev: 0.014142135623730947,
+      frequency: undefined, frequency_stddev: undefined
+    }, t.rows[0].details);
+
+    assert.deepEqual(t.rows[1], {label: 'Selection start', time: 0});
+    assert.deepEqual(t.rows[2], {label: 'Selection extent', time: 0.18});
+
+    assert.deepEqual(results.info[0], {label: 'Title: ', value: 'c'});
+    assert.deepEqual(results.info[1], {label: 'Category: ', value: ''});
+
+    // Table 2.
+    var t = results.tables[1];
+    console.log('table 2', t);
+    assert.equal(t.rows.length, 3);
+    assert.equal(t.rows[0].start, 0);
+    assert.equal(t.rows[0].duration, 0.04);
+    assert.equal(t.rows[0].selfTime, 0.04);
+    assert.deepEqual(t.rows[0].args, {});
+    assert.equal(t.rows[1].start, 0.12);
+    assert.equal(t.rows[1].duration, 0.06);
+    assert.equal(t.rows[1].selfTime, 0.06);
+    assert.deepEqual(t.rows[2].args, {});
+    assert.equal(t.rows[2].duration, 0.1);
+    assert.equal(t.rows[2].selfTime, 0.1);
+    assert.deepEqual(t.rows[2].args, {});
+  });
+
+  test('analyzeSelectionTotalWithOneArg', function() {
+    var selection = createSelectionWithOneArg();
+
+    var sliceView = document.createElement('tv-c-multi-slice-sub-view');
+    var results = new StubAnalysisResults();
+    sliceView.analyze_(results, selection);
+    assert.equal(results.tables.length, 2);
+
+    var t = results.tables[1];
+
+    assert.equal(t.rows[2].duration, 0.1);
+    assert.equal(t.rows[2].selfTime, 0.1);
+    assert.deepEqual(t.rows[2].args, { 'arg1': 9.42 });
+  });
+
+  test('analyzeSelectionTotalWithOneNumberOneTextArg', function() {
+    var selection = createSelectionWithOneNumberOneTextArg();
+
+    var sliceView = document.createElement('tv-c-multi-slice-sub-view');
+    var results = new StubAnalysisResults();
+    sliceView.analyze_(results, selection);
+    assert.equal(results.tables.length, 2);
+
+    var t = results.tables[1];
+
+    assert.equal(t.rows[2].duration, 0.1);
+    assert.equal(t.rows[2].selfTime, 0.1);
+    assert.deepEqual(t.rows[2].args, { 'arg1': 9.42 });
+  });
+
+  test('analyzeSelectionTotalWithTwoNumberArgs', function() {
+    var selection = createSelectionWithTwoNumberArgs();
+
+    var sliceView = document.createElement('tv-c-multi-slice-sub-view');
+    var results = new StubAnalysisResults();
+    sliceView.analyze_(results, selection);
+    assert.equal(results.tables.length, 2);
+
+    var t = results.tables[1];
+
+    assert.equal(t.rows[2].duration, 0.1);
+    assert.equal(t.rows[2].selfTime, 0.1);
+    assert.deepEqual(t.rows[2].args, { 'arg1': 9.42, 'arg2': 300.0 });
+  });
+
+  test('analyzeSelectionTotalWithMismatchedArgs', function() {
+    var selection = createSelectionWithMismatchedArgs();
+
+    var sliceView = document.createElement('tv-c-multi-slice-sub-view');
+    var results = new StubAnalysisResults();
+    sliceView.analyze_(results, selection);
+    assert.equal(results.tables.length, 2);
+
+    var t = results.tables[1];
+
+    assert.equal(t.rows[5].duration, 0.4);
+    assert.equal(t.rows[5].selfTime, 0.4);
+    assert.deepEqual(t.rows[5].args, { 'arg1': 10.42, 'arg2': 350.0 });
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/object_instance_view.html b/trace-viewer/trace_viewer/core/analysis/object_instance_view.html
new file mode 100644
index 0000000..6fda7aa
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/object_instance_view.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.analysis', function() {
+  var ObjectInstanceView = tv.b.ui.define('object-instance-view');
+
+  ObjectInstanceView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.objectInstance_ = undefined;
+    },
+
+    get requiresTallView() {
+      return true;
+    },
+
+    set modelEvent(obj) {
+      this.objectInstance = obj;
+    },
+
+    get modelEvent() {
+      return this.objectInstance;
+    },
+
+    get objectInstance() {
+      return this.objectInstance_;
+    },
+
+    set objectInstance(i) {
+      this.objectInstance_ = i;
+      this.updateContents();
+    },
+
+    updateContents: function() {
+      throw new Error('Not implemented');
+    }
+  };
+
+  var options = new tv.b.ExtensionRegistryOptions(
+      tv.b.TYPE_BASED_REGISTRY_MODE);
+  options.mandatoryBaseClass = ObjectInstanceView;
+  options.defaultMetadata = {
+    showInTrackView: true
+  };
+  tv.b.decorateExtensionRegistry(ObjectInstanceView, options);
+
+  return {
+    ObjectInstanceView: ObjectInstanceView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/object_snapshot_view.html b/trace-viewer/trace_viewer/core/analysis/object_snapshot_view.html
new file mode 100644
index 0000000..de5b0aa
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/object_snapshot_view.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.analysis', function() {
+  var ObjectSnapshotView = tv.b.ui.define('object-snapshot-view');
+
+  ObjectSnapshotView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.objectSnapshot_ = undefined;
+    },
+
+    get requiresTallView() {
+      return true;
+    },
+
+    set modelEvent(obj) {
+      this.objectSnapshot = obj;
+    },
+
+    get modelEvent() {
+      return this.objectSnapshot;
+    },
+
+    get objectSnapshot() {
+      return this.objectSnapshot_;
+    },
+
+    set objectSnapshot(i) {
+      this.objectSnapshot_ = i;
+      this.updateContents();
+    },
+
+    updateContents: function() {
+      throw new Error('Not implemented');
+    }
+  };
+
+  var options = new tv.b.ExtensionRegistryOptions(
+      tv.b.TYPE_BASED_REGISTRY_MODE);
+  options.mandatoryBaseClass = ObjectSnapshotView;
+  options.defaultMetadata = {
+    showInstances: true,
+    showInTrackView: true
+  };
+  tv.b.decorateExtensionRegistry(ObjectSnapshotView, options);
+
+  return {
+    ObjectSnapshotView: ObjectSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/selection_summary_table.html b/trace-viewer/trace_viewer/core/analysis/selection_summary_table.html
new file mode 100644
index 0000000..b18b342
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/selection_summary_table.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+<link rel="import" href="/core/analysis/time_span.html">
+<link rel="import" href="/core/analysis/time_stamp.html">
+
+<polymer-element name='tv-c-a-selection-summary-table'>
+  <template>
+    <style>
+    :host {
+      display: flex;
+    }
+    #table {
+      flex: 1 1 auto;
+      align-self: stretch;
+    }
+    </style>
+    <tracing-analysis-nested-table id="table">
+    </tracing-analysis-nested-table>
+    </div>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.selection_ = new tv.b.Range();
+    },
+
+    ready: function() {
+      this.$.table.showHeader = false;
+      this.$.table.tableColumns = [
+        {
+          title: 'Name',
+          value: function(row) { return row.title; },
+          width: '350px'
+        },
+        {
+          title: 'Value',
+          width: '100%',
+          value: function(row) {
+            return row.value;
+          }
+        }
+      ];
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      this.selection_ = selection;
+      this.updateContents_();
+    },
+
+    updateContents_: function() {
+      var selection = this.selection_;
+      var rows = [];
+      var hasRange;
+      if (this.selection_ && (!selection.bounds.isEmpty))
+        hasRange = true;
+      else
+        hasRange = false;
+
+      rows.push({
+        title: 'Selection start',
+        value: hasRange ?
+            tv.c.analysis.createTimeStamp(selection.bounds.min) : '<empty>'
+      });
+      rows.push({
+        title: 'Selection extent',
+        value: hasRange ?
+            tv.c.analysis.createTimeSpan(selection.bounds.range) : '<empty>'
+      });
+
+      this.$.table.tableRows = rows;
+      this.$.table.rebuild();
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/selection_summary_table_test.html b/trace-viewer/trace_viewer/core/analysis/selection_summary_table_test.html
new file mode 100644
index 0000000..89c81e5
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/selection_summary_table_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/deep_utils.html">
+<link rel="import" href="/core/analysis/selection_summary_table.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var newSliceEx = tv.c.test_utils.newSliceEx;
+
+  test('noSelection', function() {
+    var summaryTable = document.createElement('tv-c-a-selection-summary-table');
+    summaryTable.selection = undefined;
+    this.addHTMLOutput(summaryTable);
+
+    var tableEl = tv.b.findDeepElementMatching(
+        summaryTable, 'tracing-analysis-nested-table');
+    assert.equal(tableEl.tableRows[0].value, '<empty>');
+    assert.equal(tableEl.tableRows[1].value, '<empty>');
+  });
+
+  test('emptySelection', function() {
+    var summaryTable = document.createElement('tv-c-a-selection-summary-table');
+    var selection = new Selection();
+    summaryTable.selection = selection;
+    this.addHTMLOutput(summaryTable);
+
+    var tableEl = tv.b.findDeepElementMatching(
+        summaryTable, 'tracing-analysis-nested-table');
+    assert.equal(tableEl.tableRows[0].value, '<empty>');
+    assert.equal(tableEl.tableRows[1].value, '<empty>');
+  });
+
+  test('selection', function() {
+    var model = new Model();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    var tsg = thread.sliceGroup;
+
+    tsg.pushSlice(newSliceEx({title: 'a', start: 0, end: 3}));
+    tsg.pushSlice(newSliceEx({title: 'b', start: 1, end: 2}));
+
+    var selection = new Selection();
+    selection.push(tsg.slices[0]);
+    selection.push(tsg.slices[1]);
+
+    var summaryTable = document.createElement('tv-c-a-selection-summary-table');
+    summaryTable.selection = selection;
+    this.addHTMLOutput(summaryTable);
+
+    var tableEl = tv.b.findDeepElementMatching(
+        summaryTable, 'tracing-analysis-nested-table');
+    assert.equal(tableEl.tableRows[0].value.timestamp, 0);
+    assert.equal(tableEl.tableRows[1].value.duration, 3);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_alert_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_alert_sub_view.html
new file mode 100644
index 0000000..a2891c2
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_alert_sub_view.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+
+<polymer-element name="tv-c-single-alert-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.textContent = '';
+      var realView = document.createElement('tv-c-single-slice-sub-view');
+
+      this.appendChild(realView);
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.currentSelection_ = selection;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_cpu_slice_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_cpu_slice_sub_view.html
new file mode 100644
index 0000000..140b3c9
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_cpu_slice_sub_view.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/base/utils.html">
+<polymer-element name="tv-c-single-cpu-slice-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    table {
+      border-collapse: collapse;
+      border-width: 0;
+      margin-bottom: 25px;
+      width: 100%;
+    }
+
+    table tr > td:first-child {
+      padding-left: 2px;
+    }
+
+    table tr > td {
+      padding: 2px 4px 2px 4px;
+      vertical-align: text-top;
+      width: 150px;
+    }
+
+    table td td {
+      padding: 0 0 0 0;
+      width: auto;
+    }
+    tr {
+      vertical-align: top;
+    }
+
+    tr:nth-child(2n+0) {
+      background-color: #e2e2e2;
+    }
+    </style>
+    <table>
+      <tr>
+        <td>Running process:</td><td id="process-name"></td>
+      </tr>
+      <tr>
+        <td>Running thread:</td><td id="thread-name"></td>
+      </tr>
+      <tr>
+        <td>Start:</td><td id="start"></td>
+      </tr>
+      <tr>
+        <td>Duration:</td><td id="duration"></td>
+      </tr>
+      <tr>
+        <td>Active slices:</td><td id="running-thread"></td>
+      </tr>
+    </table>
+  </template>
+
+  <script>
+  'use strict';
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    set selection(selection) {
+      if (selection.length !== 1)
+        throw new Error('Only supports single slices');
+      if (!(selection[0] instanceof tv.c.trace_model.CpuSlice))
+        throw new Error('Only supports thread time slices');
+
+      this.currentSelection_ = selection;
+
+      var cpuSlice = selection[0];
+      var thread = cpuSlice.threadThatWasRunning;
+
+      var shadowRoot = this.shadowRoot;
+      if (thread) {
+        shadowRoot.querySelector('#process-name').textContent =
+            thread.parent.userFriendlyName;
+        shadowRoot.querySelector('#thread-name').textContent =
+            thread.userFriendlyName;
+      } else {
+        shadowRoot.querySelector('#process-name').parentElement.style.display =
+            'none';
+        shadowRoot.querySelector('#thread-name').textContent = cpuSlice.title;
+      }
+      shadowRoot.querySelector('#start').textContent =
+          tv.c.analysis.tsString(cpuSlice.start);
+
+      shadowRoot.querySelector('#duration').textContent =
+          tv.c.analysis.tsString(cpuSlice.duration);
+      var runningThreadEl = shadowRoot.querySelector('#running-thread');
+
+      var timeSlice = cpuSlice.getAssociatedTimeslice();
+      if (!timeSlice) {
+        runningThreadEl.parentElement.style.display = 'none';
+      } else {
+        var threadLink = document.createElement('tv-c-analysis-link');
+        threadLink.selection = new tv.c.Selection(timeSlice);
+        threadLink.textContent = 'Click to select';
+        runningThreadEl.parentElement.style.display = '';
+        runningThreadEl.textContent = '';
+        runningThreadEl.appendChild(threadLink);
+      }
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_cpu_slice_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_cpu_slice_sub_view_test.html
new file mode 100644
index 0000000..3270637
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_cpu_slice_sub_view_test.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/analysis/single_cpu_slice_sub_view.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function createBasicModel() {
+    var lines = [
+      'Android.launcher-584   [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+      '       Binder_1-217   [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+      'Android.launcher-584   [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+      'Android.launcher-584   [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+      '       Binder_1-217   [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+      '       Binder_1-217   [001] ...1 12622.507175: tracing_mark_write: E',
+      '       Binder_1-217   [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+    ];
+
+    return new tv.c.TraceModel(lines.join('\n'), false);
+  }
+
+  test('cpuSliceView_withCpuSliceOnExistingThread', function() {
+    var m = createBasicModel();
+
+    var cpu = m.kernel.cpus[1];
+    assert.isDefined(cpu);
+    var cpuSlice = cpu.slices[0];
+    assert.equal('Binder_1', cpuSlice.title);
+
+    var thread = m.findAllThreadsNamed('Binder_1')[0];
+    assert.isDefined(thread);
+    assert.equal(cpuSlice.threadThatWasRunning, thread);
+
+    var view = document.createElement('tv-c-single-cpu-slice-sub-view');
+    var selection = new tv.c.Selection();
+    selection.push(cpuSlice);
+    view.selection = selection;
+    this.addHTMLOutput(view);
+
+    // Clicking the analysis link should focus the Binder1's timeslice.
+    var didSelectionChangeHappen = false;
+    view.addEventListener('requestSelectionChange', function(e) {
+      assert.equal(1, e.selection.length);
+      assert.equal(thread.timeSlices[0], e.selection[0]);
+      didSelectionChangeHappen = true;
+    });
+    view.shadowRoot.querySelector('tv-c-analysis-link').click();
+    assert.isTrue(didSelectionChangeHappen);
+  });
+
+  test('cpuSliceViewWithCpuSliceOnMissingThread', function() {
+    var m = createBasicModel();
+
+    var cpu = m.kernel.cpus[1];
+    assert.isDefined(cpu);
+    var cpuSlice = cpu.slices[1];
+    assert.equal('Android.launcher', cpuSlice.title);
+    assert.isUndefined(cpuSlice.thread);
+
+    var selection = new tv.c.Selection();
+    selection.push(cpuSlice);
+
+    var view = document.createElement('tv-c-single-cpu-slice-sub-view');
+    view.selection = selection;
+    this.addHTMLOutput(view);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_flow_event_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_flow_event_sub_view.html
new file mode 100644
index 0000000..87f8ded
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_flow_event_sub_view.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+
+<polymer-element name="tv-c-single-flow-event-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.$.content.textContent = '';
+
+      var realView = document.createElement('tv-c-single-slice-sub-view');
+      realView.setSelectionWithoutErrorChecks(selection);
+      this.$.content.appendChild(realView);
+
+      this.currentSelection_ = selection;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_flow_event_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_flow_event_sub_view_test.html
new file mode 100644
index 0000000..30c9836
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_flow_event_sub_view_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var trace_model = tv.c.trace_model;
+
+  test('analyzeSelectionWithSingleEvent', function() {
+    var model = new Model();
+
+    var fe = new trace_model.FlowEvent('cat', 1234, 'title', 7, 10, {});
+    model.flowEvents.push(fe);
+
+    var selection = new Selection();
+    selection.push(fe);
+    assert.equal(selection.length, 1);
+
+    var subView = document.createElement('tv-c-single-flow-event-sub-view');
+    subView.selection = selection;
+    this.addHTMLOutput(subView);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_global_memory_dump_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_global_memory_dump_sub_view.html
new file mode 100644
index 0000000..fc2920c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_global_memory_dump_sub_view.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+
+<polymer-element name="tv-c-single-global-memory-dump-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      if (selection.length !== 1)
+        throw new Error('Only supports a single global memory dump');
+      if (!(selection[0] instanceof tv.c.trace_model.GlobalMemoryDump))
+        throw new Error('Only supports global memory dumps');
+      this.setSelectionWithoutErrorChecks(selection);
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    setSelectionWithoutErrorChecks: function(selection) {
+      this.currentSelection_ = selection;
+      this.textContent = '';
+
+      var pidToProcessMemoryMap = {};
+      var gd = this.currentSelection_[0];
+      for (var pid in gd.processMemoryDumps) {
+        var pd = gd.processMemoryDumps[pid];
+        pidToProcessMemoryMap[pid] = pd.args;
+      }
+
+      var objectView =
+          document.createElement('tv-c-analysis-generic-object-view');
+      objectView.object = pidToProcessMemoryMap;
+      this.appendChild(objectView);
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_instant_event_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_instant_event_sub_view.html
new file mode 100644
index 0000000..2babdee
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_instant_event_sub_view.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+
+<polymer-element name="tv-c-single-instant-event-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.$.content.textContent = '';
+      var realView = document.createElement('tv-c-single-slice-sub-view');
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.$.content.appendChild(realView);
+
+      this.currentSelection_ = selection;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_instant_event_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_instant_event_sub_view_test.html
new file mode 100644
index 0000000..653253a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_instant_event_sub_view_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Thread = tv.c.trace_model.Thread;
+  var Selection = tv.c.Selection;
+  var trace_model = tv.c.trace_model;
+
+  test('analyzeSelectionWithSingleEvent', function() {
+    var model = new Model();
+    var p52 = model.getOrCreateProcess(52);
+    var t53 = p52.getOrCreateThread(53);
+
+    var ie = new trace_model.ProcessInstantEvent('cat', 'title', 7, 10, {});
+    ie.duration = 20;
+    p52.instantEvents.push(ie);
+
+
+    var selection = new Selection();
+    selection.push(ie);
+    assert.equal(selection.length, 1);
+
+    var subView = document.createElement('tv-c-single-instant-event-sub-view');
+    subView.selection = selection;
+
+    this.addHTMLOutput(subView);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_interaction_record_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_interaction_record_sub_view.html
new file mode 100644
index 0000000..9e56696
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_interaction_record_sub_view.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+
+<polymer-element name="tv-c-single-interaction-record-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.textContent = '';
+      var realView = document.createElement('tv-c-single-slice-sub-view');
+
+      this.appendChild(realView);
+      realView.setSelectionWithoutErrorChecks(selection);
+
+      this.currentSelection_ = selection;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_object_instance_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_object_instance_sub_view.html
new file mode 100644
index 0000000..001f9b9
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_object_instance_sub_view.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/object_instance_view.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<polymer-element name="tv-c-single-object-instance-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: block;
+    }
+
+    #snapshots > * {
+      display: block;
+    }
+
+    :host {
+      overflow: auto;
+      display: block;
+    }
+
+    * {
+      -webkit-user-select: text;
+    }
+
+    .title {
+      border-bottom: 1px solid rgb(128, 128, 128);
+      font-size: 110%;
+      font-weight: bold;
+    }
+
+    td, th {
+      font-family: monospace;
+      vertical-align: top;
+    }
+    </style>
+    <div id='content'></div>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    get requiresTallView() {
+      if (this.$.content.children.length === 0)
+        return false;
+      if (this.$.content.children[0] instanceof
+          tv.c.analysis.ObjectInstanceView)
+        return this.$.content.children[0].requiresTallView;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    set selection(selection) {
+      if (selection.length !== 1)
+        throw new Error('Only supports single item selections');
+      if (!(selection[0] instanceof tv.c.trace_model.ObjectInstance))
+        throw new Error('Only supports object instances');
+
+      this.$.content.textContent = '';
+      this.currentSelection_ = selection;
+
+      var instance = selection[0];
+      var typeInfo = tv.c.analysis.ObjectInstanceView.getTypeInfo(
+          instance.category, instance.typeName);
+      if (typeInfo) {
+        var customView = new typeInfo.constructor();
+        this.$.content.appendChild(customView);
+        customView.modelEvent = instance;
+      } else {
+        this.appendGenericAnalysis_(instance);
+      }
+    },
+
+    appendGenericAnalysis_: function(instance) {
+      var html = '';
+      html += '<div class="title">' +
+          instance.typeName + ' ' +
+          instance.id + '</div>\n';
+      html += '<table>';
+      html += '<tr>';
+      html += '<tr><td>creationTs:</td><td>' +
+          instance.creationTs + '</td></tr>\n';
+      if (instance.deletionTs != Number.MAX_VALUE) {
+        html += '<tr><td>deletionTs:</td><td>' +
+            instance.deletionTs + '</td></tr>\n';
+      } else {
+        html += '<tr><td>deletionTs:</td><td>not deleted</td></tr>\n';
+      }
+      html += '<tr><td>snapshots:</td><td id="snapshots"></td></tr>\n';
+      html += '</table>';
+      this.$.content.innerHTML = html;
+      var snapshotsEl = this.$.content.querySelector('#snapshots');
+      instance.snapshots.forEach(function(snapshot) {
+        var snapshotLink = document.createElement('tv-c-analysis-link');
+        snapshotLink.selection = new tv.c.Selection(snapshot);
+        snapshotsEl.appendChild(snapshotLink);
+      });
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_object_instance_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_object_instance_sub_view_test.html
new file mode 100644
index 0000000..8066b65
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_object_instance_sub_view_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/single_object_instance_sub_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var TraceModel = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var ObjectInstance = tv.c.trace_model.ObjectInstance;
+
+  test('analyzeSelectionWithObjectInstanceUnknownType', function() {
+    var i10 = new ObjectInstance({}, '0x1000', 'cat', 'someUnhandledName', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+    var s20 = i10.addSnapshot(20, {foo: 2});
+
+    var selection = new Selection();
+    selection.push(i10);
+
+    var view = document.createElement('tv-c-single-object-instance-sub-view');
+    view.selection = selection;
+    this.addHTMLOutput(view);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_object_snapshot_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_object_snapshot_sub_view.html
new file mode 100644
index 0000000..731441b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_object_snapshot_sub_view.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/analysis/analysis_results.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/object_instance_view.html">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<polymer-element name="tv-c-single-object-snapshot-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    #args {
+      white-space: pre;
+    }
+
+    :host {
+      overflow: auto;
+      display: flex;
+    }
+
+    * {
+      -webkit-user-select: text;
+    }
+
+    .title {
+      border-bottom: 1px solid rgb(128, 128, 128);
+      font-size: 110%;
+      font-weight: bold;
+    }
+
+    td, th {
+      font-family: monospace;
+      vertical-align: top;
+    }
+    </style>
+    <content></content>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    get requiresTallView() {
+      if (this.children.length === 0)
+        return false;
+      if (this.children[0] instanceof tv.c.analysis.ObjectSnapshotView)
+        return this.children[0].requiresTallView;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    set selection(selection) {
+      if (selection.length !== 1)
+        throw new Error('Only supports single item selections');
+      if (!(selection[0] instanceof tv.c.trace_model.ObjectSnapshot))
+        throw new Error('Only supports object instances');
+
+      this.textContent = '';
+      this.currentSelection_ = selection;
+
+      var snapshot = selection[0];
+
+      var typeInfo = tv.c.analysis.ObjectSnapshotView.getTypeInfo(
+          snapshot.objectInstance.category, snapshot.objectInstance.typeName);
+      if (typeInfo) {
+        var customView = new typeInfo.constructor();
+        this.appendChild(customView);
+        customView.modelEvent = snapshot;
+      } else {
+        this.appendGenericAnalysis_(snapshot);
+      }
+    },
+
+    appendGenericAnalysis_: function(snapshot) {
+      var instance = snapshot.objectInstance;
+
+      var html = '';
+      html += '<div class="title">Snapshot of <a id="instance-link"></a> @ ' +
+          tv.c.analysis.tsString(snapshot.ts) + '</div>\n';
+      html += '<table>';
+      html += '<tr>';
+      html += '<tr><td>args:</td><td id="args"></td></tr>\n';
+      html += '</table>';
+      this.innerHTML = html;
+
+      var instanceLinkEl = document.createElement('tv-c-analysis-link');
+      instanceLinkEl.selection = new tv.c.Selection(instance);
+
+      // TODO(nduca): tv.ui.decoreate doesn't work when subclassed. So,
+      // replace the template element.
+      var tmp = this.querySelector('#instance-link');
+      tmp.parentElement.replaceChild(instanceLinkEl, tmp);
+
+      var argsEl = this.querySelector('#args');
+      argsEl.textContent = '';
+      var objectView =
+          document.createElement('tv-c-analysis-generic-object-view');
+      objectView.object = snapshot.args;
+      argsEl.appendChild(objectView);
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_object_snapshot_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_object_snapshot_sub_view_test.html
new file mode 100644
index 0000000..ce9166c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_object_snapshot_sub_view_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/single_object_snapshot_sub_view.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var TraceModel = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var ObjectInstance = tv.c.trace_model.ObjectInstance;
+
+  test('instantiate_snapshotView', function() {
+    var i10 = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'name', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+    i10.updateBounds();
+
+    var selection = new Selection();
+    selection.push(s10);
+
+    var view = document.createElement('tv-c-single-object-snapshot-sub-view');
+    view.selection = selection;
+    this.addHTMLOutput(view);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_sample_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_sample_sub_view.html
new file mode 100644
index 0000000..0558e4c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_sample_sub_view.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/analysis_results.html">
+
+<polymer-element name="tv-c-single-sample-sub-view"
+    extends="tracing-analysis-sub-view">
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    set selection(selection) {
+
+      this.textContent = '';
+      this.currentSelection_ = selection;
+
+      var results = new tv.c.analysis.AnalysisResults();
+      this.appendChild(results);
+
+      this.analyzeSingleSampleEvent_(
+          results, selection[0], 'Sample Event');
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    analyzeSingleSampleEvent_: function(results, sample, type) {
+      results.appendHeader('Selected ' + type + ':');
+      var table = results.appendTable('analysis-slice-table', 2);
+
+      results.appendInfoRow(table, 'Title', sample.title);
+      results.appendInfoRowTime(table, 'Sample Time', sample.start);
+      results.appendInfoRow(table,
+                            'Stack Trace',
+                            sample.getUserFriendlyStackTrace());
+    }
+  });
+  </script>
+</polymer>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/analysis/single_sample_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_sample_sub_view_test.html
new file mode 100644
index 0000000..09b2cee
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_sample_sub_view_test.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/single_sample_sub_view.html">
+<link rel="import" href="/core/analysis/stub_analysis_results.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Selection = tv.c.Selection;
+  var StubAnalysisResults = tv.c.analysis.StubAnalysisResults;
+  var newSampleNamed = tv.c.test_utils.newSampleNamed;
+
+  var createSelectionWithSingleSample = function() {
+    var model = new Model();
+    var t53;
+    model.importTraces([], false, false, function() {
+      t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+      model.samples.push(newSampleNamed(t53, 'X', 'my-category',
+                                        ['a', 'b', 'c'], 0.184));
+    });
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+
+    assert.equal(selection.length, 0);
+    selection.push(t53.samples[0]);
+    assert.equal(selection.length, 1);
+
+    return selection;
+  };
+
+  test('instantiate_withSingleSample', function() {
+    var selection = createSelectionWithSingleSample();
+
+    var view = document.createElement('tv-c-single-sample-sub-view');
+    view.selection = selection;
+    this.addHTMLOutput(view);
+  });
+
+  test('analyzeSelectionWithSingleSample', function() {
+    var selection = createSelectionWithSingleSample();
+
+    var view = document.createElement('tv-c-single-sample-sub-view');
+
+    var results = new StubAnalysisResults();
+    view.analyzeSingleSampleEvent_(results, selection[0], 'Sample Event');
+    assert.equal(results.tables.length, 1);
+    var table = results.tables[0];
+    var header = results.headers[0];
+    assert.equal(header.label, 'Selected Sample Event:');
+    assert.equal(table.rows.length, 3);
+
+    assert.equal(table.rows[0].text, 'X');
+    assert.equal(table.rows[1].time, 0.184);
+    assert.equal(table.rows[2].text[0], 'my-category: a');
+    assert.equal(table.rows[2].text[1], 'my-category: b');
+    assert.equal(table.rows[2].text[2], 'my-category: c');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_slice_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_slice_sub_view.html
new file mode 100644
index 0000000..b11ecc4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_slice_sub_view.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/time_span.html">
+<link rel="import" href="/core/analysis/time_stamp.html">
+<link rel="import" href="/core/analysis/stack_frame.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/sortable_table.html">
+
+<polymer-element name="tv-c-single-slice-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    :host {
+      display: flex;
+      flex-direction: column;
+    }
+    #table {
+      font-family: monospace;
+      flex: 1 1 auto;
+      align-self: stretch;
+    }
+    </style>
+    <tracing-analysis-nested-table id="table">
+    </tracing-analysis-nested-table>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.currentSelection_ = undefined;
+      this.$.table.tableColumns = [
+        {
+          title: 'Label',
+          value: function(row) { return row.name; },
+          width: '150px'
+        },
+        {
+          title: 'Value',
+          width: '100%',
+          value: function(row) { return row.value; }
+        }
+      ];
+      this.$.table.showHeader = false;
+    },
+
+    set selection(selection) {
+      if (selection.length !== 1)
+        throw new Error('Only supports single slices');
+      if (!(selection[0] instanceof tv.c.trace_model.Slice))
+        throw new Error('Only supports slices');
+      this.setSelectionWithoutErrorChecks(selection);
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    setSelectionWithoutErrorChecks: function(selection) {
+      this.currentSelection_ = selection;
+
+      if (this.currentSelection_ === undefined) {
+        this.$.table.rows = [];
+        this.$.table.rebuild();
+        return;
+      }
+
+      var slice = this.currentSelection_[0];
+
+      var rows = [];
+      if (slice.error)
+        rows.push({ name: 'Error', value: slice.error });
+
+      if (slice.title)
+        rows.push({ name: 'Title', value: slice.title });
+
+      if (slice.category)
+        rows.push({ name: 'Category', value: slice.category });
+
+      var startEl = document.createElement('tv-c-a-time-stamp');
+      startEl.timestamp = slice.start;
+      rows.push({ name: 'Start', value: startEl });
+
+      var wallDurationEl = document.createElement('tv-c-a-time-span');
+      wallDurationEl.duration = slice.duration;
+      rows.push({ name: 'Wall Duration', value: wallDurationEl });
+
+      if (slice.cpuDuration) {
+        var cpuDurationEl = document.createElement('tv-c-a-time-span');
+        cpuDurationEl.duration = slice.cpuDuration;
+        rows.push({ name: 'CPU Duration', value: cpuDurationEl });
+      }
+
+      if (slice.subSlices !== undefined && slice.subSlices.length !== 0) {
+        if (slice.selfTime) {
+          var selfTimeEl = document.createElement('tv-c-a-time-span');
+          selfTimeEl.duration = slice.selfTime;
+          rows.push({ name: 'Self Time', value: selfTimeEl });
+        }
+
+        if (slice.cpuSelfTime) {
+          var cpuSelfTimeEl = document.createElement('tv-c-a-time-span');
+          cpuSelfTimeEl.duration = slice.cpuSelfTime;
+          if (slice.cpuSelfTime > slice.selfTime) {
+            cpuSelfTimeEl.warning =
+                ' Note that CPU Self Time is larger than Self Time. ' +
+                'This is a known limitation of this system, which occurs ' +
+                'due to several subslices, rounding issues, and imprecise ' +
+                'time at which we get cpu- and real-time.';
+          }
+          rows.push({name: 'CPU Self Time',
+                     value: cpuSelfTimeEl});
+        }
+      }
+
+      if (slice.durationInUserTime) {
+        var durationInUserTimeEl = document.createElement('tv-c-a-time-span');
+        durationInUserTimeEl.duration = slice.durationInUserTime;
+        rows.push({ name: 'Duration (U)', value: durationInUserTimeEl });
+      }
+
+      function createStackFrameEl(sf) {
+        var sfEl = document.createElement('tv-c-a-stack-frame');
+        sfEl.stackFrame = sf;
+        return sfEl;
+      }
+      if (slice.startStackFrame && slice.endStackFrame) {
+        if (slice.startStackFrame === slice.endStackFrame) {
+          rows.push({name: 'Start+End Stack Trace',
+              value: createStackFrameEl(slice.startStackFrame)});
+
+        } else {
+          rows.push({ name: 'Start Stack Trace',
+              value: createStackFrameEl(slice.startStackFrame)});
+          rows.push({ name: 'End Stack Trace',
+              value: createStackFrameEl(slice.endStackFrame)});
+        }
+      } else if (slice.startStackFrame) {
+        rows.push({ name: 'Start Stack Trace',
+            value: createStackFrameEl(slice.startStackFrame)});
+
+      } else if (slice.endStackFrame) {
+        rows.push({ name: 'End Stack Trace',
+            value: createStackFrameEl(slice.endStackFrame)});
+      }
+
+      var n = 0;
+      for (var argName in slice.args) {
+        n += 1;
+      }
+      if (n > 0) {
+        var subRows = [];
+        for (var argName in slice.args) {
+          var argView =
+              document.createElement('tv-c-analysis-generic-object-view');
+          argView.object = slice.args[argName];
+          subRows.push({ name: argName,
+                      value: argView});
+        }
+        rows.push({
+          name: 'Args', value: '',
+          isExpanded: true, subRows: subRows
+        });
+      }
+
+      this.$.table.tableRows = rows;
+      this.$.table.rebuild();
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_slice_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_slice_sub_view_test.html
new file mode 100644
index 0000000..16d1a4a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_slice_sub_view_test.html
@@ -0,0 +1,228 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/deep_utils.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Model = tv.c.TraceModel;
+  var Thread = tv.c.trace_model.Thread;
+  var Selection = tv.c.Selection;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+  var newSliceCategory = tv.c.test_utils.newSliceCategory;
+
+  function createSelection(customizeThreadCallback) {
+    var model = new Model();
+    var importOptions = new tv.c.ImportOptions();
+    importOptions.customizeModelCallback = function() {
+      var t53 = model.getOrCreateProcess(52).getOrCreateThread(53);
+      customizeThreadCallback(t53, model);
+    }
+    model.importTraces([], importOptions);
+
+    var t53 = model.processes[52].threads[53];
+
+    var t53track = {};
+    t53track.thread = t53;
+
+    var selection = new Selection();
+    selection.push(t53.sliceGroup.slices[0]);
+    assert.equal(selection.length, 1);
+
+    return selection;
+  }
+
+  function createSelectionWithSingleSlice(opt_options) {
+    var options = opt_options || {};
+    return createSelection(function(t53, model) {
+      if (options.withStartStackFrame || options.withEndStackFrame) {
+        var fA = tv.c.test_utils.newStackTrace(model, 'cat', ['a1', 'a2']);
+        var fB = tv.c.test_utils.newStackTrace(model, 'cat', ['b1', 'b2']);
+      }
+
+      var slice;
+      if (options.withCategory)
+        slice = newSliceCategory('foo', 'b', 0, 0.002);
+      else
+        slice = newSliceNamed('b', 0, 0.002);
+
+      if (options.withStartStackFrame)
+        slice.startStackFrame = options.withStartStackFrame === 'a' ? fA : fB;
+
+      if (options.withEndStackFrame)
+        slice.endStackFrame = options.withEndStackFrame === 'a' ? fA : fB;
+
+      t53.sliceGroup.pushSlice(slice);
+    });
+  };
+
+  test('instantiate_withSingleSlice', function() {
+    var selection = createSelectionWithSingleSlice();
+
+    var analysisEl = document.createElement('tv-c-single-slice-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withSingleSliceWithArg', function() {
+    var selection = createSelection(function(t53) {
+      var slice = newSliceNamed('my_slice', 0, 1.0);
+      slice.args = {
+        'complex': {
+          'b': '2 as a string',
+          'c': [3, 4, 5]
+        }
+      };
+      t53.sliceGroup.pushSlice(slice);
+    });
+
+    var subView = document.createElement('tv-c-single-slice-sub-view');
+    subView.selection = selection;
+    this.addHTMLOutput(subView);
+
+    var gov = tv.b.findDeepElementMatching(subView,
+                                           'tv-c-analysis-generic-object-view');
+    assert.isDefined(gov);
+  });
+
+
+  test('instantiate_withSingleSliceCategory', function() {
+    var selection = createSelectionWithSingleSlice({withCategory: true});
+
+    var analysisEl = document.createElement('tv-c-single-slice-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+  });
+
+  test('instantiate_withSingleStartStackFrame', function() {
+    var selection = createSelectionWithSingleSlice(
+        {withStartStackFrame: 'a'});
+
+    var analysisEl = document.createElement('tv-c-single-slice-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+
+    var e = tv.b.findDeepElementWithTextContent(
+        analysisEl, /Start Stack Trace/);
+    assert.isDefined(e);
+    assert.isDefined(e.nextSibling.children[0].stackFrame);
+  });
+
+  test('instantiate_withSingleEndStackFrame', function() {
+    var selection = createSelectionWithSingleSlice(
+        {withEndStackFrame: 'b'});
+
+    var analysisEl = document.createElement('tv-c-single-slice-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+
+    var e = tv.b.findDeepElementWithTextContent(
+        analysisEl, /End Stack Trace/);
+    assert.isDefined(e);
+    assert.isDefined(e.nextSibling.children[0].stackFrame);
+    assert.equal(e.nextSibling.children[0].stackFrame.title, 'b2');
+  });
+
+  test('instantiate_withDifferentStartAndEndStackFrames', function() {
+    var selection = createSelectionWithSingleSlice(
+        {withStartStackFrame: 'a',
+         withEndStackFrame: 'b'});
+
+    var analysisEl = document.createElement('tv-c-single-slice-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+
+    var eA = tv.b.findDeepElementWithTextContent(
+        analysisEl, /Start Stack Trace/);
+    assert.isDefined(eA);
+    assert.isDefined(eA.nextSibling.children[0].stackFrame);
+    assert.equal(eA.nextSibling.children[0].stackFrame.title, 'a2');
+
+    var eB = tv.b.findDeepElementWithTextContent(
+        analysisEl, /End Stack Trace/);
+    assert.isDefined(eB);
+    assert.isDefined(eB.nextSibling.children[0].stackFrame);
+    assert.equal(eB.nextSibling.children[0].stackFrame.title, 'b2');
+  });
+
+  test('instantiate_withSameStartAndEndStackFrames', function() {
+    var selection = createSelectionWithSingleSlice(
+        {withStartStackFrame: 'a',
+         withEndStackFrame: 'a'});
+
+    var analysisEl = document.createElement('tv-c-single-slice-sub-view');
+    analysisEl.selection = selection;
+    this.addHTMLOutput(analysisEl);
+
+    var e = tv.b.findDeepElementWithTextContent(
+        analysisEl, /Start\+End Stack Trace/);
+    assert.isDefined(e);
+    assert.isDefined(e.nextSibling.children[0].stackFrame);
+    assert.equal(e.nextSibling.children[0].stackFrame.title, 'a2');
+  });
+
+  test('analyzeSelectionWithSingleSlice', function() {
+    var selection = createSelectionWithSingleSlice();
+    var subView = document.createElement('tv-c-single-slice-sub-view');
+    subView.selection = selection;
+
+    var table = tv.b.findDeepElementMatching(
+        subView, 'tracing-analysis-nested-table');
+    assert.equal(table.tableRows.length, 3);
+    assert.equal(table.tableRows[0].value, 'b');
+    assert.equal(table.tableRows[1].value.timestamp, 0);
+    assert.equal(table.tableRows[2].value.duration, 0.002);
+  });
+
+  test('analyzeSelectionWithSingleSliceCategory', function() {
+    var selection = createSelectionWithSingleSlice({withCategory: true});
+
+    var subView = document.createElement('tv-c-single-slice-sub-view');
+    subView.selection = selection;
+
+    var table = tv.b.findDeepElementMatching(
+        subView, 'tracing-analysis-nested-table');
+    assert.equal(table.tableRows.length, 4);
+    assert.equal(table.tableRows[0].value, 'b');
+    assert.equal(table.tableRows[1].value, 'foo');
+    assert.equal(table.tableRows[2].value.timestamp, 0);
+    assert.equal(table.tableRows[3].value.duration, 0.002);
+  });
+
+  test('instantiate_withSingleSliceContainingIDRef', function() {
+    var model = new Model();
+    var p1 = model.getOrCreateProcess(1);
+    var myObjectSlice = p1.objects.addSnapshot(
+        '0x1000', 'cat', 'my_object', 0);
+
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(newSliceCategory('cat', 'b', 0, 2));
+    t1.sliceGroup.slices[0].args.my_object = myObjectSlice;
+
+    var t1track = {};
+    t1track.thread = t1;
+
+    var selection = new Selection();
+    selection.push(t1.sliceGroup.slices[0]);
+    assert.equal(selection.length, 1);
+
+    var subView = document.createElement('tv-c-single-slice-sub-view');
+    subView.selection = selection;
+    this.addHTMLOutput(subView);
+
+    var analysisLink = tv.b.findDeepElementMatching(subView,
+                                                    'tv-c-analysis-link');
+    assert.isDefined(analysisLink);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_thread_time_slice_sub_view.html b/trace-viewer/trace_viewer/core/analysis/single_thread_time_slice_sub_view.html
new file mode 100644
index 0000000..721152b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_thread_time_slice_sub_view.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/ui/color_scheme.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/analysis/analysis_sub_view.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<polymer-element name="tv-c-single-thread-time-slice-sub-view"
+    extends="tracing-analysis-sub-view">
+  <template>
+    <style>
+    table {
+      border-collapse: collapse;
+      border-width: 0;
+      margin-bottom: 25px;
+      width: 100%;
+    }
+
+    table tr > td:first-child {
+      padding-left: 2px;
+    }
+
+    table tr > td {
+      padding: 2px 4px 2px 4px;
+      vertical-align: text-top;
+      width: 150px;
+    }
+
+    table td td {
+      padding: 0 0 0 0;
+      width: auto;
+    }
+    tr {
+      vertical-align: top;
+    }
+
+    tr:nth-child(2n+0) {
+      background-color: #e2e2e2;
+    }
+    </style>
+    <table>
+      <tr>
+        <td>Running process:</td><td id="process-name"></td>
+      </tr>
+      <tr>
+        <td>Running thread:</td><td id="thread-name"></td>
+      </tr>
+      <tr>
+        <td>State:</td>
+        <td><b><span id="state"></span></b></td>
+      </tr>
+      <tr>
+        <td>Start:</td><td id="start"></td>
+      </tr>
+      <tr>
+        <td>Duration:</td><td id="duration"></td>
+      </tr>
+
+      <tr>
+        <td>On CPU:</td><td id="on-cpu"></td>
+      </tr>
+
+      <tr>
+        <td>Running instead:</td><td id="running-instead"></td>
+      </tr>
+
+      <tr>
+        <td>Args:</td><td id="args"></td>
+      </tr>
+    </table>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.currentSelection_ = undefined;
+    },
+
+    get selection() {
+      return this.currentSelection_;
+    },
+
+    set selection(selection) {
+      if (selection.length !== 1)
+        throw new Error('Only supports single slices');
+      if (!(selection[0] instanceof tv.c.trace_model.ThreadTimeSlice))
+        throw new Error('Only supports thread time slices');
+
+      this.currentSelection_ = selection;
+
+      var timeSlice = selection[0];
+      var thread = timeSlice.thread;
+
+      var shadowRoot = this.shadowRoot;
+      shadowRoot.querySelector('#state').textContent = timeSlice.title;
+      var stateColor = tv.b.ui.getColorPalette()[timeSlice.colorId];
+      shadowRoot.querySelector('#state').style.backgroundColor = stateColor;
+
+      shadowRoot.querySelector('#process-name').textContent =
+          thread.parent.userFriendlyName;
+      shadowRoot.querySelector('#thread-name').textContent =
+          thread.userFriendlyName;
+
+      shadowRoot.querySelector('#start').textContent =
+          tv.c.analysis.tsString(timeSlice.start);
+      shadowRoot.querySelector('#duration').textContent =
+          tv.c.analysis.tsString(timeSlice.duration);
+      var onCpuEl = shadowRoot.querySelector('#on-cpu');
+      onCpuEl.textContent = '';
+      var runningInsteadEl = shadowRoot.querySelector('#running-instead');
+      if (timeSlice.cpuOnWhichThreadWasRunning) {
+        runningInsteadEl.parentElement.removeChild(runningInsteadEl);
+
+        var cpuLink = document.createElement('tv-c-analysis-link');
+        cpuLink.selection = new tv.c.Selection(
+            timeSlice.getAssociatedCpuSlice());
+        cpuLink.textContent =
+            timeSlice.cpuOnWhichThreadWasRunning.userFriendlyName;
+        onCpuEl.appendChild(cpuLink);
+      } else {
+        onCpuEl.parentElement.removeChild(onCpuEl);
+
+        var cpuSliceThatTookCpu = timeSlice.getCpuSliceThatTookCpu();
+        if (cpuSliceThatTookCpu) {
+          var cpuLink = document.createElement('tv-c-analysis-link');
+          cpuLink.selection = new tv.c.Selection(cpuSliceThatTookCpu);
+          if (cpuSliceThatTookCpu.thread)
+            cpuLink.textContent = cpuSliceThatTookCpu.thread.userFriendlyName;
+          else
+            cpuLink.textContent = cpuSliceThatTookCpu.title;
+          runningInsteadEl.appendChild(cpuLink);
+        } else {
+          runningInsteadEl.parentElement.removeChild(runningInsteadEl);
+        }
+      }
+
+      var argsEl = shadowRoot.querySelector('#args');
+      if (tv.b.dictionaryKeys(timeSlice.args).length > 0) {
+        var argsView =
+            document.createElement('tv-c-analysis-generic-object-view');
+        argsView.object = timeSlice.args;
+
+        argsEl.parentElement.style.display = '';
+        argsEl.textContent = '';
+        argsEl.appendChild(argsView);
+      } else {
+        argsEl.parentElement.style.display = 'none';
+      }
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/single_thread_time_slice_sub_view_test.html b/trace-viewer/trace_viewer/core/analysis/single_thread_time_slice_sub_view_test.html
new file mode 100644
index 0000000..e6c3f4f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/single_thread_time_slice_sub_view_test.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/analysis/single_thread_time_slice_sub_view.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  function createBasicModel() {
+    var lines = [
+      'Android.launcher-584   [001] d..3 12622.506890: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+      '       Binder_1-217   [001] d..3 12622.506918: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D ==> next_comm=Android.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+      'Android.launcher-584   [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+      'Android.launcher-584   [001] d..3 12622.506950: sched_switch: prev_comm=Android.launcher prev_pid=584 prev_prio=120 prev_state=R+ ==> next_comm=Binder_1 next_pid=217 next_prio=120', // @suppress longLineCheck
+      '       Binder_1-217   [001] ...1 12622.507057: tracing_mark_write: B|128|queueBuffer', // @suppress longLineCheck
+      '       Binder_1-217   [001] ...1 12622.507175: tracing_mark_write: E',
+      '       Binder_1-217   [001] d..3 12622.507253: sched_switch: prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=S ==> next_comm=Android.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+    ];
+
+    return new tv.c.TraceModel(lines.join('\n'), false);
+  }
+
+  test('runningSlice', function() {
+    var m = createBasicModel();
+
+    var cpu = m.kernel.cpus[1];
+    var binderSlice = cpu.slices[0];
+    assert.equal(binderSlice.title, 'Binder_1');
+    var launcherSlice = cpu.slices[1];
+    assert.equal(launcherSlice.title, 'Android.launcher');
+
+
+    var thread = m.findAllThreadsNamed('Binder_1')[0];
+
+    var view = document.createElement('tv-c-single-thread-time-slice-sub-view');
+    var selection = new tv.c.Selection();
+    selection.push(thread.timeSlices[0]);
+    view.selection = selection;
+    this.addHTMLOutput(view);
+
+    // Clicking the analysis link should focus the Binder1's timeslice.
+    var didSelectionChangeHappen = false;
+    view.addEventListener('requestSelectionChange', function(e) {
+      assert.equal(e.selection.length, 1);
+      assert.equal(e.selection[0], binderSlice);
+      didSelectionChangeHappen = true;
+    });
+    view.shadowRoot.querySelector('tv-c-analysis-link').click();
+    assert.isTrue(didSelectionChangeHappen);
+  });
+
+  test('sleepingSlice', function() {
+    var m = createBasicModel();
+
+    var cpu = m.kernel.cpus[1];
+    var binderSlice = cpu.slices[0];
+    assert.equal(binderSlice.title, 'Binder_1');
+    var launcherSlice = cpu.slices[1];
+    assert.equal(launcherSlice.title, 'Android.launcher');
+
+
+    var thread = m.findAllThreadsNamed('Binder_1')[0];
+
+    var view = document.createElement('tv-c-single-thread-time-slice-sub-view');
+    var selection = new tv.c.Selection();
+    selection.push(thread.timeSlices[1]);
+    view.selection = selection;
+    this.addHTMLOutput(view);
+
+    // Clicking the analysis link should focus the Android.launcher slice
+    var didSelectionChangeHappen = false;
+    view.addEventListener('requestSelectionChange', function(e) {
+      assert.equal(e.selection.length, 1);
+      assert.equal(e.selection[0], launcherSlice);
+      didSelectionChangeHappen = true;
+    });
+    view.shadowRoot.querySelector('tv-c-analysis-link').click();
+    assert.isTrue(didSelectionChangeHappen);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/size_span.html b/trace-viewer/trace_viewer/core/analysis/size_span.html
new file mode 100644
index 0000000..95f4b24
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/size_span.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<polymer-element name="tv-c-a-size-span">
+  <template>
+    <style>
+    :host {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
+    </style>
+    <span id="content"></span>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.$.content.textContent = String.fromCharCode(9888);
+      this.numBytes_ = undefined;
+    },
+
+    get numBytes() {
+      return this.numBytes_;
+    },
+
+    set numBytes(numBytes) {
+      this.numBytes_ = numBytes;
+
+      var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti'];
+      var i = 0;
+      while (numBytes >= 1024 && i < prefixes.length - 1) {
+        numBytes /= 1024;
+        i++;
+      }
+      var sizeString = numBytes.toFixed(1) + ' ' + prefixes[i] + 'B';
+
+      this.$.content.textContent = sizeString;
+    },
+
+    get stringContent() {
+      return this.$.content.textContent;
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/size_span_test.html b/trace-viewer/trace_viewer/core/analysis/size_span_test.html
new file mode 100644
index 0000000..a030cab
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/size_span_test.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/size_span.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+  test('instantiate', function() {
+    var timeSpan = document.createElement('tv-c-a-size-span');
+    timeSpan.numBytes = 5 * 1024;
+    assert.equal(timeSpan.numBytes, 5 * 1024);
+    this.addHTMLOutput(timeSpan);
+  });
+
+  test('sizeStrings', function() {
+    var el = document.createElement('tv-c-a-size-span');
+
+    el.numBytes = 0;
+    assert.equal(el.stringContent, '0.0 B');
+
+    el.numBytes = 1;
+    assert.equal(el.stringContent, '1.0 B');
+
+    el.numBytes = 1536;
+    assert.equal(el.stringContent, '1.5 KiB');
+
+    el.numBytes = 424.2 * 1024 * 1024;
+    assert.equal(el.stringContent, '424.2 MiB');
+
+    el.numBytes = 5 * 1024 * 1024 * 1024;
+    assert.equal(el.stringContent, '5.0 GiB');
+
+    el.numBytes = 1025 * 1024 * 1024 * 1024 * 1024;
+    assert.equal(el.stringContent, '1025.0 TiB');
+    this.addHTMLOutput(el);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/stack_frame.html b/trace-viewer/trace_viewer/core/analysis/stack_frame.html
new file mode 100644
index 0000000..82913b4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/stack_frame.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/generic_object_view.html">
+
+<polymer-element name="tv-c-a-stack-frame">
+  <template>
+    <style>
+    :host {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
+    </style>
+    <tv-c-analysis-generic-object-view id="ov">
+    </tv-c-analysis-generic-object-view>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.stackFrame_ = undefined;
+    },
+
+    get stackFrame() {
+      return this.stackFrame_;
+    },
+
+    set stackFrame(stackFrame) {
+      this.stackFrame_ = stackFrame;
+      this.$.ov.object = stackFrame.getUserFriendlyStackTrace();
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/stack_frame_test.html b/trace-viewer/trace_viewer/core/analysis/stack_frame_test.html
new file mode 100644
index 0000000..d13b232
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/stack_frame_test.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/stack_frame.html">
+<link rel="import" href="/core/test_utils.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var model = new tv.c.TraceModel();
+    var fA = tv.c.test_utils.newStackTrace(model, 'cat', ['a1', 'a2', 'a3']);
+
+    var stackFrameView = document.createElement('tv-c-a-stack-frame');
+    stackFrameView.stackFrame = fA;
+    this.addHTMLOutput(stackFrameView);
+  });
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/analysis/stub_analysis_results.html b/trace-viewer/trace_viewer/core/analysis/stub_analysis_results.html
new file mode 100644
index 0000000..0666794
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/stub_analysis_results.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c.analysis', function() {
+  function StubAnalysisResults() {
+    this.headers = [];
+    this.info = [];
+    this.tables = [];
+  }
+  StubAnalysisResults.prototype = {
+    __proto__: Object.protoype,
+
+    appendTable: function(parent, className) {
+      var table = {
+        className: className,
+        rows: []
+      };
+      table.tHead = undefined;
+      table.className = className;
+      table.classList = [];
+      table.classList.push(className);
+      table.classList.add = function(className) {
+        table.classList.push(className);
+      };
+      this.tables.push(table);
+      return table;
+    },
+
+    appendHeader: function(label) {
+      var header = {
+        label: label
+      };
+      this.headers.push(header);
+      return header;
+    },
+
+    appendInfo: function(label, value) {
+      this.info.push({label: label, value: value});
+    },
+
+    appendDetailsRow: function(table, start, duration, selfTime, args,
+                               selectionGenerator, cpuDuration) {
+      table.rows.push({
+        start: start,
+        duration: duration,
+        selfTime: selfTime,
+        args: args,
+        selectionGenerator: selectionGenerator,
+        cpuDuration: cpuDuration});
+    },
+
+    appendHeadRow: function(table) {
+      if (table.headerRow)
+        throw new Error('Only one header row allowed.');
+      table.headerRow = [];
+      return table.headerRow;
+    },
+
+    appendTableCell: function(table, row, text) {
+      row.push(text);
+    },
+
+    appendSpacingRow: function(table) {
+      var row = {spacing: true};
+      table.rows.push(row);
+      return row;
+    },
+
+    appendInfoRow: function(table, label, opt_text) {
+      var row = {label: label, text: opt_text};
+      table.rows.push(row);
+      return row;
+    },
+
+    appendInfoRowTime: function(table, label, time) {
+      var row = {label: label, time: time};
+      table.rows.push(row);
+      return row;
+    },
+
+    appendDataRow: function(table, label, duration, cpuDuration, selfTime,
+                            cpuSelfTime, occurrences, percentage, details,
+                            selectionGenerator) {
+      var row = {
+        label: label,
+        duration: duration,
+        cpuDuration: cpuDuration,
+        selfTime: selfTime,
+        cpuSelfTime: cpuSelfTime,
+        occurrences: occurrences,
+        percentage: percentage,
+        details: details,
+        selectionGenerator: selectionGenerator
+      };
+      table.rows.push(row);
+      return row;
+    }
+  };
+
+  return {
+    StubAnalysisResults: StubAnalysisResults
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/stub_analysis_table.html b/trace-viewer/trace_viewer/core/analysis/stub_analysis_table.html
new file mode 100644
index 0000000..1bb17db
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/stub_analysis_table.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c.analysis', function() {
+  function StubAnalysisTable() {
+    this.ownerDocument_ = document;
+    this.nodes_ = [];
+  }
+
+  StubAnalysisTable.prototype = {
+    __proto__: Object.protoype,
+
+    get ownerDocument() {
+      return this.ownerDocument_;
+    },
+
+    appendChild: function(node) {
+      if (node.tagName == 'TFOOT' || node.tagName == 'THEAD' ||
+              node.tagName == 'TBODY') {
+        node.__proto__ = StubAnalysisTable.prototype;
+        node.nodes_ = [];
+        node.ownerDocument_ = document;
+      }
+      this.nodes_.push(node);
+    },
+
+    get lastNode() {
+      return this.nodes_.pop();
+    },
+
+    get nodeCount() {
+      return this.nodes_.length;
+    }
+  };
+
+  return {
+    StubAnalysisTable: StubAnalysisTable
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/tab_view.html b/trace-viewer/trace_viewer/core/analysis/tab_view.html
new file mode 100644
index 0000000..82b5170
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/tab_view.html
@@ -0,0 +1,386 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<polymer-element name="tracing-analysis-tab-view"
+    constructor="TracingAnalysisTabView">
+  <template>
+    <style>
+      :host {
+        display: flex;
+        flex-flow: column nowrap;
+        overflow: hidden;
+        box-sizing: border-box;
+      }
+
+      tab-strip[tabs-hidden] {
+        display: none;
+      }
+
+      tab-strip {
+        background-color: rgb(236, 236, 236);
+        border-bottom: 1px solid #8e8e8e;
+        display: flex;
+        flex: 0 0 auto;
+        flex-flow: row;
+        overflow-x: auto;
+        padding: 0 10px 0 10px;
+        font-size: 12px;
+      }
+
+      tab-button {
+        display: block;
+        flex: 0 0 auto;
+        padding: 4px 15px 1px 15px;
+        margin-top: 2px;
+      }
+
+      tab-button[selected=true] {
+        background-color: white;
+        border: 1px solid rgb(163, 163, 163);
+        border-bottom: none;
+        padding: 3px 14px 1px 14px;
+      }
+
+      tabs-content-container {
+        display: flex;
+        flex: 1 1 auto;
+        overflow: auto;
+        width: 100%;
+      }
+
+      ::content > * {
+        flex: 1 1 auto;
+      }
+
+      ::content > *:not([selected]) {
+        display: none;
+      }
+
+      button-label {
+        display: inline;
+      }
+    </style>
+
+    <tab-strip>
+      <template repeat="{{tab in tabs_}}">
+        <tab-button
+            button-id="{{ tab.id }}"
+            on-click="{{ tabButtonSelectHandler_ }}"
+            selected="{{ selectedTab_.id === tab.id }}">
+          <button-label>{{ tab.label ? tab.label : 'No Label'}}</button-label>
+        </tab-button>
+      </template>
+    </tab-strip>
+
+    <tabs-content-container id='content-container'>
+        <content></content>
+    </tabs-content-container>
+
+  </template>
+
+  <script>
+  'use strict';
+  Polymer({
+
+    get selectedTab() {
+      // Do not give access to the user to the inner data structure.
+      // A user should only be able to mutate the added tab content.
+      if (this.selectedTab_)
+        return this.selectedTab_.content;
+      return undefined;
+    },
+
+    set selectedTab(content) {
+      // Make sure we process any pending children additions / removals, before
+      // trying to select a tab. Otherwise, we might not find some children.
+      this.childrenUpdated_(
+        this.childrenObserver_.takeRecords(), this.childrenObserver_);
+
+      if (content === undefined || content === null) {
+        this.changeSelectedTabById_(undefined);
+        return;
+      }
+
+      // Search for the specific node in our tabs list.
+      // If it is not there print a warning.
+      var contentTabId = undefined;
+      for (var i = 0; i < this.tabs_.length; i++)
+        if (this.tabs_[i].content === content) {
+          contentTabId = this.tabs_[i].id;
+          break;
+        }
+
+      if (contentTabId === undefined) {
+        console.warn('Tab not in tabs list. Ignoring changed selection.');
+        return;
+      }
+
+      this.changeSelectedTabById_(contentTabId);
+    },
+
+    get tabsHidden() {
+      var ts = this.shadowRoot.querySelector('tab-strip');
+      return ts.hasAttribute('tabs-hidden');
+    },
+
+    set tabsHidden(tabsHidden) {
+      tabsHidden = !!tabsHidden;
+      var ts = this.shadowRoot.querySelector('tab-strip');
+      if (tabsHidden)
+        ts.setAttribute('tabs-hidden', true);
+      else
+        ts.removeAttribute('tabs-hidden');
+    },
+
+    ready: function() {
+      // A tab is represented by the following tuple:
+      // (id, label, content, observer, savedScrollTop, savedScrollLeft).
+      // The properties are used in the following way:
+      // id: Uniquely identifies a tab. It is the same number as the index
+      //     in the tabs array. Used primarily by the on-click event attached
+      //     to buttons.
+      // label: A string, representing the label printed on the tab button.
+      // content: The light-dom child representing the contents of the tab.
+      //     The content is appended to this tab-view by the user.
+      // observers: The observers attached to the content node to watch for
+      //     attribute changes. The attributes of interest are: 'selected',
+      //     and 'tab-label'.
+      // savedScrollTop/Left: Used to return the scroll position upon switching
+      //     tabs. The values are generally saved when a tab switch occurs.
+      //
+      // The order of the tabs is relevant for the tab ordering.
+      this.tabs_ = [];
+      this.selectedTab_ = undefined;
+
+      // Register any already existing children.
+      for (var i = 0; i < this.children.length; i++)
+        this.processAddedChild_(this.children[i]);
+
+      // In case the user decides to add more tabs, make sure we watch for
+      // any child mutations.
+      this.childrenObserver_ = new MutationObserver(
+          this.childrenUpdated_.bind(this));
+      this.childrenObserver_.observe(this, { childList: 'true' });
+    },
+
+
+    /**
+     * Function called on light-dom child addition.
+     */
+    processAddedChild_: function(child) {
+      var observerAttributeSelected = new MutationObserver(
+          this.childAttributesChanged_.bind(this));
+      var observerAttributeTabLabel = new MutationObserver(
+          this.childAttributesChanged_.bind(this));
+      var tabObject = {
+        id: this.tabs_.length,
+        content: child,
+        label: child.getAttribute('tab-label'),
+        observers: {
+          forAttributeSelected: observerAttributeSelected,
+          forAttributeTabLabel: observerAttributeTabLabel
+        },
+        savedScrollTop: 0,
+        savedScrollLeft: 0
+      };
+
+      this.tabs_.push(tabObject);
+      if (child.hasAttribute('selected')) {
+        // When receiving a child with the selected attribute, if we have no
+        // selected tab, mark the child as the selected tab, otherwise keep
+        // the previous selection.
+        if (this.selectedTab_)
+          child.removeAttribute('selected');
+        else
+          this.setSelectedTabById_(tabObject.id);
+      }
+
+      // This is required because the user might have set the selected
+      // property before we got to process the child.
+      var previousSelected = child.selected;
+
+      var tabView = this;
+
+      Object.defineProperty(
+          child,
+          'selected', {
+            configurable: true,
+            set: function(value) {
+              if (value) {
+                tabView.changeSelectedTabById_(tabObject.id);
+                return;
+              }
+
+              var wasSelected = tabView.selectedTab_ === tabObject;
+              if (wasSelected)
+                tabView.changeSelectedTabById_(undefined);
+            },
+            get: function() {
+              return this.hasAttribute('selected');
+            }
+          });
+
+      if (previousSelected)
+        child.selected = previousSelected;
+
+      observerAttributeSelected.observe(child,
+          { attributeFilter: ['selected'] });
+      observerAttributeTabLabel.observe(child,
+          { attributeFilter: ['tab-label'] });
+
+    },
+
+    /**
+     * Function called on light-dom child removal.
+     */
+    processRemovedChild_: function(child) {
+      for (var i = 0; i < this.tabs_.length; i++) {
+        // Make sure ids are the same as the tab position after removal.
+        this.tabs_[i].id = i;
+        if (this.tabs_[i].content === child) {
+          this.tabs_[i].observers.forAttributeSelected.disconnect();
+          this.tabs_[i].observers.forAttributeTabLabel.disconnect();
+          // The user has removed the currently selected tab.
+          if (this.tabs_[i] === this.selectedTab_)
+            this.clearSelectedTab_();
+          child.removeAttribute('selected');
+          delete child.selected;
+          // Remove the observer since we no longer care about this child.
+          this.tabs_.splice(i, 1);
+          i--;
+        }
+      }
+    },
+
+
+    /**
+     * This function handles child attribute changes. The only relevant
+     * attributes for the tab-view are 'tab-label' and 'selected'.
+     */
+    childAttributesChanged_: function(mutations, observer) {
+      var tabObject = undefined;
+      // First figure out which child has been changed.
+      for (var i = 0; i < this.tabs_.length; i++) {
+        var observers = this.tabs_[i].observers;
+        if (observers.forAttributeSelected === observer ||
+            observers.forAttributeTabLabel === observer) {
+            tabObject = this.tabs_[i];
+            break;
+        }
+      }
+
+      // This should not happen, unless the user has messed with our internal
+      // data structure.
+      if (!tabObject)
+        return;
+
+      // Next handle the attribute changes.
+      for (var i = 0; i < mutations.length; i++) {
+        var node = tabObject.content;
+        // 'tab-label' attribute has been changed.
+        if (mutations[i].attributeName === 'tab-label')
+          tabObject.label = node.getAttribute('tab-label');
+        // 'selected' attribute has been changed.
+        if (mutations[i].attributeName === 'selected') {
+          // The attribute has been set.
+          var nodeIsSelected = node.hasAttribute('selected');
+          if (nodeIsSelected)
+            this.changeSelectedTabById_(tabObject.id);
+          else
+            this.changeSelectedTabById_(undefined);
+        }
+      }
+    },
+
+    /**
+     * This function handles light-dom additions and removals from the
+     * tab-view component.
+     */
+    childrenUpdated_: function(mutations, observer) {
+      mutations.forEach(function(mutation) {
+        for (var i = 0; i < mutation.removedNodes.length; i++)
+          this.processRemovedChild_(mutation.removedNodes[i]);
+        for (var i = 0; i < mutation.addedNodes.length; i++)
+          this.processAddedChild_(mutation.addedNodes[i]);
+      }, this);
+    },
+
+    /**
+     * Handler called when a click event happens on any of the tab buttons.
+     */
+    tabButtonSelectHandler_: function(event, detail, sender) {
+      this.changeSelectedTabById_(sender.getAttribute('button-id'));
+    },
+
+    /**
+     * This does the actual work. :)
+     */
+    changeSelectedTabById_: function(id) {
+      var newTab = id !== undefined ? this.tabs_[id] : undefined;
+      var changed = this.selectedTab_ !== newTab;
+      this.saveCurrentTabScrollPosition_();
+      this.clearSelectedTab_();
+      if (id !== undefined) {
+        this.setSelectedTabById_(id);
+        this.restoreCurrentTabScrollPosition_();
+      }
+
+      if (changed)
+        this.fire('selected-tab-change');
+    },
+
+    /**
+     * This function updates the currently selected tab based on its internal
+     * id. The corresponding light-dom element receives the selected attribute.
+     */
+    setSelectedTabById_: function(id) {
+      this.selectedTab_ = this.tabs_[id];
+      // Disconnect observer while we mutate the child.
+      this.selectedTab_.observers.forAttributeSelected.disconnect();
+      this.selectedTab_.content.setAttribute('selected', 'selected');
+      // Reconnect the observer to watch for changes in the future.
+      this.selectedTab_.observers.forAttributeSelected.observe(
+          this.selectedTab_.content, { attributeFilter: ['selected'] });
+
+    },
+
+    saveCurrentTabScrollPosition_: function() {
+      if (this.selectedTab_) {
+        this.selectedTab_.savedScrollTop =
+            this.$['content-container'].scrollTop;
+        this.selectedTab_.savedScrollLeft =
+            this.$['content-container'].scrollLeft;
+      }
+    },
+
+    restoreCurrentTabScrollPosition_: function() {
+      if (this.selectedTab_) {
+        this.$['content-container'].scrollTop =
+            this.selectedTab_.savedScrollTop;
+        this.$['content-container'].scrollLeft =
+            this.selectedTab_.savedScrollLeft;
+      }
+    },
+
+    /**
+     * This function clears the currently selected tab. This handles removal
+     * of the selected attribute from the light-dom element.
+     */
+    clearSelectedTab_: function() {
+      if (this.selectedTab_) {
+        // Disconnect observer while we mutate the child.
+        this.selectedTab_.observers.forAttributeSelected.disconnect();
+        this.selectedTab_.content.removeAttribute('selected');
+        // Reconnect the observer to watch for changes in the future.
+        this.selectedTab_.observers.forAttributeSelected.observe(
+            this.selectedTab_.content, { attributeFilter: ['selected'] });
+        this.selectedTab_ = undefined;
+      }
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/tab_view_test.html b/trace-viewer/trace_viewer/core/analysis/tab_view_test.html
new file mode 100644
index 0000000..c9e4998
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/tab_view_test.html
@@ -0,0 +1,278 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/tab_view.html">
+
+<template id="tab-view-test-template">
+  <tracing-analysis-tab-view>
+    <p tab-label="Existing Label"> Tab with label already set </p>
+    <p> Tab Content with no label </p>
+    <p selected="selected" tab-label="Should be selected">
+      Already selected tab
+    </p>
+    <p selected="selected" tab-label="Should not be selected">
+      Second already selected tab
+    </p>
+  </tracing-analysis-tab-view>
+</template>
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var THIS_DOC = document._currentScript.ownerDocument;
+
+  test('instantiate', function() {
+
+    var TAB_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' +
+        ' Cras eleifend elit nec erat tristique pellentesque. Cras placerat ' +
+        'lectus, sed semper tortor ornare quis. Maecenas vitae hendrerit. ' +
+        'Cras mattis interdum nisi, eget egestas dui iaculis ultricies. Proi' +
+        'n magna at nibh fringilla tincidunt id vitae ante. Fusce nec urna n' +
+        'on porttitor tincidunt. Pellentesque habitant morbi tristique senec' +
+        'tus netus et malesuada fames ac turpis egestas. Suspendisse sed vel' +
+        'it mollis ornare sit amet vel augue. Nullam rhoncus in tellus id. ' +
+        'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices ' +
+        'cubilia Curae; Nunc at velit consectetur ipsum tempus tempus. Nunc ' +
+        'mattis sapien, a placerat erat. Vivamus ac enim ultricies, gravida ' +
+        'nulla ut, scelerisque magna. Sed a volutpat enim. Morbi vulputate, ' +
+        'sed egestas mollis, urna nisl varius sem, sed venenatis turpis null' +
+        'a ipsum. Suspendisse potenti.';
+
+    var tabViewContainer = document.createElement('div');
+    tabViewContainer.style.width = '500px';
+    tabViewContainer.style.height = '200px';
+
+    var tabView = new TracingAnalysisTabView();
+
+    var firstTab = document.createElement('div');
+    firstTab.setAttribute('tab-label', 'First Tab Label');
+    firstTab.innerHTML = '<p>' + TAB_TEXT + '<p>';
+
+    var secondTab = document.createElement('div');
+    secondTab.setAttribute('tab-label', 'Second Tab Label');
+    secondTab.innerHTML = '<b>' + 'Second Tab Text' + '</b>';
+
+    var thirdTab = document.createElement('div');
+    thirdTab.setAttribute('tab-label', 'Third Tab Label');
+    thirdTab.innerHTML = '<b>' + 'Third Tab Text' + '</b>';
+
+    tabView.appendChild(firstTab);
+    tabView.appendChild(secondTab);
+    tabView.appendChild(thirdTab);
+    tabViewContainer.appendChild(tabView);
+
+    this.addHTMLOutput(tabViewContainer);
+
+    thirdTab.setAttribute('tab-label', 'Something Different');
+
+    var button = document.createElement('button');
+    button.textContent = 'Change label';
+
+    button.addEventListener('click', function() {
+      thirdTab.setAttribute('tab-label', 'Label Changed');
+    });
+
+    tabView.selectedTab = secondTab;
+    this.addHTMLOutput(button);
+  });
+
+  test('instantiateChildrenAlreadyInside', function() {
+    var tabViewTemplate = THIS_DOC.querySelector('#tab-view-test-template');
+    var tabView = tabViewTemplate.createInstance();
+
+    var tabViewContainer = document.createElement('div');
+    tabViewContainer.style.width = '400px';
+    tabViewContainer.style.height = '200px';
+
+    tabViewContainer.appendChild(tabView);
+
+    this.addHTMLOutput(tabViewContainer);
+
+  });
+
+  test('programaticallySetSelectedTab', function() {
+    var tabViewContainer = document.createElement('div');
+    tabViewContainer.style.width = '500px';
+    tabViewContainer.style.height = '200px';
+
+    var tabView = new TracingAnalysisTabView();
+
+    var t1 = document.createElement('div');
+    var t2 = document.createElement('div');
+    var t3 = document.createElement('div');
+
+    tabView.appendChild(t1);
+    tabView.appendChild(t2);
+    tabView.appendChild(t3);
+
+    assert.isUndefined(tabView.selectedTab);
+    tabView.selectedTab = t1;
+
+    assert.isTrue(t1.hasAttribute('selected'));
+    assert.isFalse(t2.hasAttribute('selected'));
+    assert.isFalse(t3.hasAttribute('selected'));
+    assert.isTrue(Object.is(t1, tabView.selectedTab));
+
+    tabView.selectedTab = t2;
+    assert.isFalse(t1.hasAttribute('selected'));
+    assert.isTrue(t2.hasAttribute('selected'));
+    assert.isFalse(t3.hasAttribute('selected'));
+    assert.isTrue(Object.is(t2, tabView.selectedTab));
+
+    tabView.selectedTab = t3;
+    assert.isFalse(t1.hasAttribute('selected'));
+    assert.isFalse(t2.hasAttribute('selected'));
+    assert.isTrue(t3.hasAttribute('selected'));
+    assert.isTrue(Object.is(t3, tabView.selectedTab));
+
+    t1.selected = true;
+    assert.isTrue(t1.hasAttribute('selected'));
+    assert.isFalse(t2.hasAttribute('selected'));
+    assert.isFalse(t3.hasAttribute('selected'));
+    assert.isTrue(Object.is(t1, tabView.selectedTab));
+
+    // Make sure just randomly setting a tab as not selected does not
+    // break the existing selection.
+    t2.selected = false;
+    t3.selected = false;
+    assert.isTrue(t1.hasAttribute('selected'));
+    assert.isFalse(t2.hasAttribute('selected'));
+    assert.isFalse(t3.hasAttribute('selected'));
+    assert.isTrue(Object.is(t1, tabView.selectedTab));
+
+    t3.selected = true;
+    assert.isFalse(t1.hasAttribute('selected'));
+    assert.isFalse(t2.hasAttribute('selected'));
+    assert.isTrue(t3.hasAttribute('selected'));
+    assert.isTrue(Object.is(t3, tabView.selectedTab));
+
+    tabViewContainer.appendChild(tabView);
+
+    this.addHTMLOutput(tabViewContainer);
+  });
+
+  /**
+   * This test checks that if an element has a selected property already set,
+   * before being attached to the tabView, it still gets selected if the
+   * property is true, after it gets attached.
+   */
+  test('instantiateSetSelectedTabAlreadySet', function() {
+    var tabViewContainer = document.createElement('div');
+    tabViewContainer.style.width = '500px';
+    tabViewContainer.style.height = '200px';
+
+    var tabView = new TracingAnalysisTabView();
+
+    var t1 = document.createElement('div');
+    t1.textContent = 'This text should BE visible.';
+    var t2 = document.createElement('div');
+    t2.textContent = 'This text should NOT be visible.';
+    var t3 = document.createElement('div');
+    t3.textContent = 'This text should NOT be visible, also.';
+
+    t1.selected = true;
+    t2.selected = false;
+    t3.selected = false;
+
+    tabView.appendChild(t1);
+    tabView.appendChild(t2);
+    tabView.appendChild(t3);
+
+    t1.setAttribute('tab-label', 'This should be selected');
+    t2.setAttribute('tab-label', 'Not selected');
+    t3.setAttribute('tab-label', 'Not selected');
+
+    tabViewContainer.appendChild(tabView);
+
+    this.addHTMLOutput(tabViewContainer);
+  });
+
+  test('selectingInvalidTabWorks', function() {
+    var tabView = new TracingAnalysisTabView();
+    var t1 = document.createElement('div');
+    var t2 = document.createElement('div');
+    var t3 = document.createElement('div');
+    var invalidChild = document.createElement('div');
+
+    tabView.appendChild(t1);
+    tabView.appendChild(t2);
+    tabView.appendChild(t3);
+
+    tabView.selectedTab = t1;
+
+    assert.equal(tabView.selectedTab, t1);
+
+    // Make sure that selecting an invalid tab does not break the current
+    // selection.
+    tabView.selectedTab = invalidChild;
+    assert.equal(t1, tabView.selectedTab);
+
+    // Also make sure the invalidChild does not influence the tab view when
+    // it has a selected property set.
+    invalidChild.selected = true;
+    tabView.selectedTab = invalidChild;
+    assert.equal(t1, tabView.selectedTab);
+  });
+
+  test('changeTabCausesEvent', function() {
+    var tabView = new TracingAnalysisTabView();
+    var t1 = document.createElement('div');
+    var t2 = document.createElement('div');
+    var invalidChild = document.createElement('div');
+
+    tabView.appendChild(t1);
+    tabView.appendChild(t2);
+
+    var numChangeEvents = 0;
+    tabView.addEventListener('selected-tab-change', function() {
+        numChangeEvents++;
+    });
+    tabView.selectedTab = t1;
+    assert.equal(numChangeEvents, 1);
+    tabView.selectedTab = t1;
+    assert.equal(numChangeEvents, 1);
+    tabView.selectedTab = t2;
+    assert.equal(numChangeEvents, 2);
+    tabView.selectedTab = undefined;
+    assert.equal(numChangeEvents, 3);
+  });
+
+  /**
+   * This test makes sure that removing the selected tab does not select
+   * any other tab.
+   */
+  test('instantiateRemovingSelectedTab', function() {
+    var tabViewContainer = document.createElement('div');
+    tabViewContainer.style.width = '500px';
+    tabViewContainer.style.height = '200px';
+
+    var tabView = new TracingAnalysisTabView();
+
+    var t1 = document.createElement('div');
+    t1.textContent = 'This text should BE visible.';
+    var t2 = document.createElement('div');
+    t2.textContent = 'This text should NOT be visible.';
+    var t3 = document.createElement('div');
+    t3.textContent = 'This text should NOT be visible, also.';
+
+    tabView.appendChild(t1);
+    tabView.appendChild(t2);
+    tabView.appendChild(t3);
+
+    t1.setAttribute('tab-label', 'This should not exist');
+    t2.setAttribute('tab-label', 'Not selected');
+    t3.setAttribute('tab-label', 'Not selected');
+
+    tabView.selectedTab = t1;
+    tabView.removeChild(t1);
+
+    tabViewContainer.appendChild(tabView);
+
+    this.addHTMLOutput(tabViewContainer);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/table_builder.html b/trace-viewer/trace_viewer/core/analysis/table_builder.html
new file mode 100644
index 0000000..28553f4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/table_builder.html
@@ -0,0 +1,662 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/toggle_container.html">
+
+<!--
+@fileoverview A container that constructs a table-like container.
+-->
+<polymer-element name="tracing-analysis-nested-table">
+  <template>
+    <style>
+      :host {
+        display: flex;
+        flex-direction: column;
+      }
+
+      table {
+        font-size: 12px;
+
+        flex: 1 1 auto;
+        align-self: stretch;
+        border-collapse: separate;
+        border-spacing: 0;
+        border-width: 0;
+        -webkit-user-select: initial;
+      }
+
+      tr > td {
+        padding: 2px 4px 2px 4px;
+        vertical-align: text-top;
+      }
+
+      button.toggle-button {
+        height: 15px;
+        line-height: 60%;
+        vertical-align: middle;
+        width: 100%;
+      }
+
+      button > * {
+        height: 15px;
+        vertical-align: middle;
+      }
+
+      td.button-column {
+        width: 30px;
+      }
+
+
+      table > thead > tr > td.sensitive:hover {
+        background-color: #fcfcfc;
+      }
+
+      table > thead > tr > td {
+        font-weight: bold;
+        text-align: left;
+
+        background-color: #eee;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+
+        border-top: 1px solid #ffffff;
+        border-bottom: 1px solid #aaa;
+      }
+
+      table > tfoot {
+        background-color: #eee;
+        font-weight: bold;
+      }
+
+      table > tbody > tr:hover,
+      table > tfoot > tr:hover {
+        background-color: #e6e6e6
+      }
+
+      table > tbody.has-footer > tr:last-child > td {
+        border-bottom: 1px solid #aaa;
+      }
+
+      table > tfoot > tr:first-child > td {
+        border-top: 1px solid #ffffff;
+      }
+
+      expand-button {
+        -webkit-user-select: none;
+        display: inline-block;
+        cursor: pointer;
+        font-size: 9px;
+        min-width: 8px;
+        max-width: 8px;
+      }
+
+      .button-expanded {
+        transform: rotate(90deg);
+      }
+    </style>
+    <table>
+      <thead id="head">
+      </thead>
+      <tbody id="body">
+      </tbody>
+      <tfoot id="foot">
+      </tfoot>
+    </table>
+  </template>
+  <script>
+  'use strict';
+  (function() {
+    var RIGHT_ARROW = String.fromCharCode(0x25b6);
+    var UNSORTED_ARROW = String.fromCharCode(0x25BF);
+    var ASCENDING_ARROW = String.fromCharCode(0x25BE);
+    var DESCENDING_ARROW = String.fromCharCode(0x25B4);
+    var BASIC_INDENTATION = 8;
+
+    Polymer({
+      created: function() {
+        this.tableColumns_ = [];
+        this.tableRows_ = [];
+        this.tableRowsInfo_ = [];
+        this.tableFooterRows_ = [];
+        this.sortColumnIndex_ = undefined;
+        this.sortDescending_ = false;
+        this.columnsWithExpandButtons_ = [];
+        this.headerCells_ = [];
+        this.showHeader_ = true;
+      },
+
+      clear: function() {
+        this.textContent = '';
+        this.tableColumns_ = [];
+        this.tableRows_ = [];
+        this.tableRowsInfo_ = [];
+        this.tableFooterRows_ = [];
+        this.sortColumnIndex_ = undefined;
+        this.sortDescending_ = false;
+        this.columnsWithExpandButtons_ = [];
+        this.headerCells_ = [];
+        this.rowClickCallback_ = undefined;
+      },
+
+      get showHeader() {
+        return this.showHeader_;
+      },
+
+      set showHeader(showHeader) {
+        this.showHeader_ = showHeader;
+        this.scheduleRebuildHeaders_();
+      },
+
+      /**
+       * Data objects should have the following fields:
+       *   mandatory: title, value
+       *   optional: width {string}, cmp {function}, colSpan {number},
+       *             showExpandButtons {boolean}
+       *
+       * @param {Array} columns An array of data objects.
+       */
+      set tableColumns(columns) {
+        // Figure out the columsn with expand buttons...
+        var columnsWithExpandButtons = [];
+        for (var i = 0; i < columns.length; i++) {
+          if (columns[i].showExpandButtons)
+            columnsWithExpandButtons.push(i);
+        }
+        if (columnsWithExpandButtons.length === 0) {
+          // First column if none have specified.
+          columnsWithExpandButtons = [0];
+        }
+
+        // Sanity check columns.
+        for (var i = 0; i < columns.length; i++) {
+          var colInfo = columns[i];
+          if (colInfo.width === undefined)
+            continue;
+
+          var hasExpandButton = columnsWithExpandButtons.indexOf(i) !== -1;
+
+          var w = colInfo.width;
+          if (w) {
+            if (/\d+px/.test(w)) {
+              continue;
+            } else if (/\d+%/.test(w)) {
+              if (hasExpandButton) {
+                throw new Error('Columns cannot be %-sized and host ' +
+                                ' an expand button');
+              }
+            } else {
+              throw new Error('Unrecognized width string');
+            }
+          }
+        }
+
+        // Commit the change.
+        this.tableColumns_ = columns;
+        this.columnsWithExpandButtons_ = columnsWithExpandButtons;
+        this.sortColumnIndex = undefined;
+        this.scheduleRebuildHeaders_();
+      },
+
+      get tableColumns() {
+        return this.tableColumns_;
+      },
+
+      /**
+       * @param {Array} rows An array of 'row' objects with the following
+       * fields:
+       *   optional: subRows An array of objects that have the same 'row'
+       *                     structure.
+       */
+      set tableRows(rows) {
+        this.tableRows_ = rows;
+        this.tableRowsInfo_ = [];
+        this.createTableRowsInfo_(rows, this.tableRowsInfo_);
+        if (this.sortColumnIndex_ !== undefined)
+          this.sortTable_();
+        this.scheduleRebuildBody_();
+      },
+
+      get tableRows() {
+        return this.tableRows_;
+      },
+
+      set footerRows(rows) {
+        this.tableFooterRows_ = rows;
+        this.tableFooterRowsInfo_ = [];
+        this.createTableRowsInfo_(rows, this.tableFooterRowsInfo_);
+        this.scheduleRebuildFooter_();
+      },
+
+      get footerRows() {
+        return this.tableFooterRows_;
+      },
+
+      set sortColumnIndex(number) {
+        if (number === undefined) {
+          this.sortColumnIndex_ = undefined;
+          this.updateHeaderArrows_();
+          return;
+        }
+
+        if (this.tableColumns_.length <= number)
+          throw new Error('Column number ' + number + ' is out of bounds.');
+        if (!this.tableColumns_[number].cmp)
+          throw new Error('Column ' + number + ' does not have a comparator.');
+
+        this.sortColumnIndex_ = number;
+        this.updateHeaderArrows_();
+        this.sortTable_();
+      },
+
+      get sortColumnIndex() {
+        return this.sortColumnIndex_;
+      },
+
+      set sortDescending(value) {
+        var newValue = !!value;
+
+        if (newValue !== this.sortDescending_) {
+          this.sortDescending_ = newValue;
+          this.updateHeaderArrows_();
+          if (this.sortColumnIndex_ !== undefined)
+            this.sortTable_();
+        }
+      },
+
+      get sortDescending() {
+        return this.sortDescending_;
+      },
+
+      set rowClickCallback(callback) {
+        this.rowClickCallback_ = callback;
+      },
+
+      get rowClickCallback() {
+        return this.rowClickCallback_;
+      },
+
+      updateHeaderArrows_: function() {
+        for (var i = 0; i < this.headerCells_.length; i++) {
+          if (!this.tableColumns_[i].cmp) {
+            this.headerCells_[i].sideContent = '';
+            continue;
+          }
+          if (i !== this.sortColumnIndex_) {
+            this.headerCells_[i].sideContent = UNSORTED_ARROW;
+            continue;
+          }
+          this.headerCells_[i].sideContent = this.sortDescending_ ?
+            DESCENDING_ARROW : ASCENDING_ARROW;
+        }
+      },
+
+      sortTable_: function() {
+        this.sortRows_(this.tableRowsInfo_);
+        this.scheduleRebuildBody_();
+      },
+
+      sortRows_: function(rows) {
+        rows.sort(function(rowA, rowB) {
+          if (this.sortDescending_)
+            return this.tableColumns_[this.sortColumnIndex_].cmp(
+                rowB.userRow, rowA.userRow);
+          return this.tableColumns_[this.sortColumnIndex_].cmp(
+                rowA.userRow, rowB.userRow);
+        }.bind(this));
+        // Sort expanded sub rows recursively.
+        for (var i = 0; i < rows.length; i++) {
+          if (rows[i].isExpanded)
+            this.sortRows_(rows[i].subRows);
+        }
+      },
+
+      generateHeaderColumns_: function() {
+        this.headerCells_ = [];
+        this.$.head.textContent = '';
+        if (!this.showHeader_)
+          return;
+
+        var tr = this.appendNewElementAfter_(this.$.head, 'tr');
+        for (var i = 0; i < this.tableColumns_.length; i++) {
+          var td = this.appendNewElementAfter_(tr, 'td');
+
+          var headerCell = new TracingAnalysisHeaderCell();
+
+          if (this.showHeader)
+            headerCell.cellTitle = this.tableColumns_[i].title;
+          else
+            headerCell.cellTitle = '';
+
+          // If the table can be sorted by this column, attach a tap callback
+          // to the column.
+          if (this.tableColumns_[i].cmp) {
+            td.classList.add('sensitive');
+            headerCell.tapCallback = this.createSortCallback_(i);
+            // Set arrow position, depending on the sortColumnIndex.
+            if (this.sortColumnIndex_ === i)
+              headerCell.sideContent = this.sortDescending_ ?
+                DESCENDING_ARROW : ASCENDING_ARROW;
+            else
+              headerCell.sideContent = UNSORTED_ARROW;
+          }
+
+          td.appendChild(headerCell);
+          this.headerCells_.push(headerCell);
+        }
+      },
+
+      applySizes_: function() {
+        var rowToRemoveSizing;
+        var rowToSize;
+        if (this.showHeader) {
+          rowToSize = this.$.head.children[0];
+          rowToRemoveSizing = this.$.body.children[0];
+        } else {
+          rowToSize = this.$.body.children[0];
+          rowToRemoveSizing = this.$.head.children[0];
+        }
+        for (var i = 0; i < this.tableColumns_.length; i++) {
+          if (rowToRemoveSizing && rowToRemoveSizing.children[i]) {
+            var tdToRemoveSizing = rowToRemoveSizing.children[i];
+            tdToRemoveSizing.style.minWidth = '';
+            tdToRemoveSizing.style.width = '';
+          }
+
+          // Apply sizing.
+          var td = rowToSize.children[i];
+
+          var delta;
+          if (this.columnsWithExpandButtons_.indexOf(i) !== -1) {
+            td.style.paddingLeft = BASIC_INDENTATION + 'px';
+            delta = BASIC_INDENTATION + 'px';
+          } else {
+            delta = undefined;
+          }
+
+          function calc(base, delta) {
+            if (delta)
+              return 'calc(' + base + ' - ' + delta + ')';
+            else
+              return base;
+          }
+
+          var w = this.tableColumns_[i].width;
+          if (w) {
+            if (/\d+px/.test(w)) {
+              td.style.minWidth = calc(w, delta);
+            } else if (/\d+%/.test(w)) {
+              td.style.width = w;
+            } else {
+              throw new Error('Unrecognized width string: ' + w);
+            }
+          }
+        }
+      },
+
+      createSortCallback_: function(columnNumber) {
+        return function() {
+          var previousIndex = this.sortColumnIndex;
+          this.sortColumnIndex = columnNumber;
+          if (previousIndex !== columnNumber)
+            this.sortDescending = false;
+          else
+            this.sortDescending = !this.sortDescending;
+        }.bind(this);
+      },
+
+      generateTableRowNodes_: function(tableSection, sectionRows, indentation,
+                                       opt_prevSibling) {
+        var sibling = opt_prevSibling;
+        for (var i = 0; i < sectionRows.length; i++) {
+          var row = sectionRows[i];
+          this.generateRowNode_(tableSection, row, indentation);
+          this.appendElementAfter_(tableSection, row.htmlNode, sibling);
+          if (row.isExpanded) {
+            sibling = this.generateTableRowNodes_(tableSection, row.subRows,
+                          indentation + 1, row.htmlNode);
+          } else {
+            sibling = row.htmlNode;
+          }
+        }
+        return sibling;
+      },
+
+      generateRowNode_: function(tableSection, row, indentation) {
+        if (row.htmlNode)
+          return row.htmlNode;
+
+        var INDENT_SPACE = indentation * 16;
+        var INDENT_SPACE_NO_BUTTON = indentation * 16 + BASIC_INDENTATION;
+        var tr = this.ownerDocument.createElement('tr');
+        row.htmlNode = tr;
+        row.indentation = indentation;
+
+        for (var i = 0; i < this.tableColumns_.length;) {
+          var td = this.appendNewElementAfter_(tr, 'td');
+          var column = this.tableColumns_[i];
+          var value = column.value(row.userRow);
+          var colSpan = column.colSpan ? column.colSpan : 1;
+          td.style.colSpan = colSpan;
+
+          if (this.columnsWithExpandButtons_.indexOf(i) != -1) {
+            if (row.subRows.length > 0) {
+              td.style.paddingLeft = INDENT_SPACE + 'px';
+              var expandButton = this.appendNewElementAfter_(td,
+                  'expand-button');
+              expandButton.textContent = RIGHT_ARROW;
+              if (row.isExpanded)
+                expandButton.classList.add('button-expanded');
+              this.addToggleListenerForRowToButton_(tableSection, row,
+                  expandButton);
+            } else {
+              td.style.paddingLeft = INDENT_SPACE_NO_BUTTON + 'px';
+            }
+          }
+
+          if (value instanceof HTMLElement)
+            td.appendChild(value);
+          else
+            td.appendChild(this.ownerDocument.createTextNode(value));
+
+          i += colSpan;
+        }
+
+        var self = this;
+        tr.addEventListener('click', function(e) {
+          if (self.rowClickCallback_) {
+            self.rowClickCallback_(e);
+          }
+        });
+      },
+
+      addToggleListenerForRowToButton_: function(tableSection, row, button) {
+        button.parentElement.addEventListener('click', function() {
+          row.isExpanded = !row.isExpanded;
+
+          if (row.isExpanded) {
+            button.classList.add('button-expanded');
+            // Before adding the expanded nodes, sort them if we can.
+            if (this.sortColumnIndex_ !== undefined)
+              this.sortRows_(row.subRows);
+            var sibling = row.htmlNode;
+            this.generateTableRowNodes_(tableSection,
+                row.subRows, row.indentation + 1, sibling);
+          } else {
+            button.classList.remove('button-expanded');
+            this.removeSubNodes_(tableSection, row);
+          }
+        }.bind(this));
+      },
+
+      removeSubNodes_: function(tableSection, row) {
+        for (var i = 0; i < row.subRows.length; i++) {
+          var subNode = row.subRows[i].htmlNode;
+          if (subNode && subNode.parentNode === tableSection) {
+            tableSection.removeChild(row.subRows[i].htmlNode);
+            this.removeSubNodes_(tableSection, row.subRows[i]);
+          }
+        }
+      },
+
+      scheduleRebuildHeaders_: function() {
+        this.headerDirty_ = true;
+        this.scheduleRebuild_();
+      },
+
+      scheduleRebuildBody_: function() {
+        this.bodyDirty_ = true;
+        this.scheduleRebuild_();
+      },
+
+      scheduleRebuildFooter_: function() {
+        this.footerDirty_ = true;
+        this.scheduleRebuild_();
+      },
+
+      scheduleRebuild_: function() {
+        if (this.rebuildPending_)
+          return;
+        this.rebuildPending_ = true;
+        setTimeout(function() {
+          this.rebuildPending_ = false;
+          this.rebuild();
+        }.bind(this), 0);
+      },
+
+      rebuild: function() {
+        var wasBodyOrHeaderDirty = this.headerDirty_ || this.bodyDirty_;
+
+        if (this.headerDirty_) {
+          this.generateHeaderColumns_();
+          this.headerDirty_ = false;
+        }
+        if (this.bodyDirty_) {
+          this.generateTableRowNodes_(this.$.body, this.tableRowsInfo_, 0);
+          this.bodyDirty_ = false;
+        }
+
+        if (wasBodyOrHeaderDirty)
+          this.applySizes_();
+
+        if (this.footerDirty_) {
+          this.generateTableRowNodes_(this.$.foot, this.tableFooterRowsInfo_,
+                                      0);
+          if (this.tableFooterRowsInfo_.length) {
+            this.$.body.classList.add('has-footer');
+          } else {
+            this.$.body.classList.remove('has-footer');
+          }
+          this.footerDirty_ = false;
+        }
+      },
+
+      createTableRowsInfo_: function(rows, containerForResults) {
+        for (var i = 0; i < rows.length; i++) {
+          var subRowsArray = [];
+          if (rows[i].subRows)
+            this.createTableRowsInfo_(rows[i].subRows, subRowsArray);
+
+          containerForResults.push({
+            userRow: rows[i],
+            htmlNode: undefined,
+            subRows: subRowsArray,
+            isExpanded: rows[i].isExpanded || false
+          });
+        }
+      },
+
+      appendElementAfter_: function(parent, element, opt_prevSibling) {
+        var nodeAfter = undefined;
+        if (opt_prevSibling)
+          nodeAfter = opt_prevSibling.nextSibling;
+        parent.insertBefore(element, nodeAfter);
+      },
+
+      appendNewElementAfter_: function(parent, tagName, opt_prevSibling) {
+        var element = parent.ownerDocument.createElement(tagName);
+        this.appendElementAfter_(parent, element, opt_prevSibling);
+        return element;
+      }
+    });
+  })();
+  </script>
+</polymer-element>
+<polymer-element name="tracing-analysis-header-cell"
+    constructor="TracingAnalysisHeaderCell"
+    on-tap="onTap_">
+  <template>
+  <style>
+    :host {
+      -webkit-user-select: none;
+      display: flex;
+    }
+
+    span {
+      flex: 0 1 auto;
+    }
+
+    side-element {
+      -webkit-user-select: none;
+      flex: 1 0 auto;
+      padding-left: 4px;
+      vertical-align: top;
+      font-size: 15px;
+      font-family: sans-serif;
+      display: inline;
+      line-height: 85%;
+    }
+  </style>
+
+    <span>{{ cellTitle_ }}</span><side-element id="side"></side-element>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    created: function() {
+      this.tapCallback_ = undefined;
+      this.cellTitle_ = '';
+    },
+
+    set cellTitle(value) {
+      this.cellTitle_ = value;
+    },
+
+    get cellTitle() {
+      return this.cellTitle_;
+    },
+
+    clearSideContent: function() {
+      this.$.side.textContent = '';
+    },
+
+    set sideContent(content) {
+      this.$.side.textContent = content;
+    },
+
+    get sideContent() {
+      return this.$.side.textContent;
+    },
+
+    set tapCallback(callback) {
+      this.style.cursor = 'pointer';
+      this.tapCallback_ = callback;
+    },
+
+    get tapCallback() {
+      return this.tapCallback_;
+    },
+
+    onTap_: function() {
+      if (this.tapCallback_)
+        this.tapCallback_();
+    }
+  });
+</script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/table_builder_test.html b/trace-viewer/trace_viewer/core/analysis/table_builder_test.html
new file mode 100644
index 0000000..969ad83
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/table_builder_test.html
@@ -0,0 +1,374 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/deep_utils.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var THIS_DOC = document._currentScript.ownerDocument;
+
+  test('instantiateNestedTableNoNests', function() {
+    var columns = [
+      {
+        title: 'First Column',
+        value: function(row) { return row.firstData; },
+        width: '200px'
+      },
+      {
+        title: 'Second Column',
+        value: function(row) { return row.secondData; }
+      }
+    ];
+
+    var rows = [
+      {
+        firstData: 'A1',
+        secondData: 'A2'
+      },
+      {
+        firstData: 'B1',
+        secondData: 'B2'
+      }
+    ];
+
+    var table = document.createElement('tracing-analysis-nested-table');
+    table.tableColumns = columns;
+    table.tableRows = rows;
+    table.rebuild();
+
+    this.addHTMLOutput(table);
+  });
+
+  test('instantiateNestedTableWithNests', function() {
+    var columns = [
+      {
+        title: 'First Column',
+        value: function(row) { return row.firstData; },
+        width: '250px'
+      },
+      {
+        title: 'Second Column',
+        value: function(row) { return row.secondData; },
+        width: '50%'
+      }
+    ];
+
+    var rows = [
+      {
+        firstData: 'A1',
+        secondData: 'A2',
+        subRows: [
+          {
+            firstData: 'Sub1 A1',
+            secondData: 'Sub1 A2'
+          },
+          {
+            firstData: 'Sub2 A1',
+            secondData: 'Sub2 A2',
+            subRows: [
+              {
+                firstData: 'SubSub1 A1',
+                secondData: 'SubSub1 A2'
+              },
+              {
+                firstData: 'SubSub2 A1',
+                secondData: 'SubSub2 A2'
+              }
+            ]
+          },
+          {
+            firstData: 'Sub3 A1',
+            secondData: 'Sub3 A2'
+          }
+        ]
+      },
+      {
+        firstData: 'B1',
+        secondData: 'B2'
+      }
+    ];
+
+    var table = document.createElement('tracing-analysis-nested-table');
+    table.tableColumns = columns;
+    table.tableRows = rows;
+    table.rebuild();
+
+    this.addHTMLOutput(table);
+  });
+
+  test('instantiateSortingCallbacksWithNests', function() {
+    var table = document.createElement('tracing-analysis-nested-table');
+
+    var columns = [
+      {
+        title: 'First Column',
+        value: function(row) { return row.firstData; },
+        width: '50%'
+      },
+      {
+        title: 'Second Column',
+        value: function(row) { return row.secondData; },
+        width: '250px',
+        cmp: function(rowA, rowB) {
+          return rowA.secondData.toString().localeCompare(
+              rowB.secondData.toString());
+        },
+        showExpandButtons: true
+      }
+    ];
+
+    var rows = [
+      {
+        firstData: 'A1',
+        secondData: 'A2',
+        subRows: [
+          {
+            firstData: 'Sub1 A1',
+            secondData: 'Sub1 A2'
+          },
+          {
+            firstData: 'Sub2 A1',
+            secondData: 'Sub2 A2',
+            subRows: [
+              {
+                firstData: 'SubSub1 A1',
+                secondData: 'SubSub1 A2'
+              },
+              {
+                firstData: 'SubSub2 A1',
+                secondData: 'SubSub2 A2'
+              }
+            ]
+          },
+          {
+            firstData: 'Sub3 A1',
+            secondData: 'Sub3 A2'
+          }
+        ]
+      },
+      {
+        firstData: 'B1',
+        secondData: 'B2'
+      }
+    ];
+
+    var footerRows = [
+      {
+        firstData: 'F1',
+        secondData: 'F2',
+        subRows: [
+          {
+            firstData: 'Sub1F1',
+            secondData: 'Sub1F2'
+          },
+          {
+            firstData: 'Sub2F1',
+            secondData: 'Sub2F2',
+            subRows: [
+              {
+                firstData: 'SubSub1F1',
+                secondData: 'SubSub1F2'
+              },
+              {
+                firstData: 'SubSub2F1',
+                secondData: 'SubSub2F2'
+              }
+            ]
+          },
+          {
+            firstData: 'Sub3F1',
+            secondData: 'Sub3F2'
+          }
+        ]
+      },
+      {
+        firstData: 'F\'1',
+        secondData: 'F\'2'
+      }
+
+    ];
+
+    table.tableColumns = columns;
+    table.tableRows = rows;
+    table.footerRows = footerRows;
+    table.rebuild();
+
+    this.addHTMLOutput(table);
+
+    var button = THIS_DOC.createElement('button');
+    button.textContent = 'Sort By Col 0';
+    button.addEventListener('click', function() {
+      table.sortDescending = !table.sortDescending;
+      table.sortColumnIndex = 0;
+    });
+    table.rebuild();
+
+    this.addHTMLOutput(button);
+  });
+
+
+  test('instantiateNestedTableAlreadyExpanded', function() {
+    var columns = [
+      {
+        title: 'a',
+        value: function(row) { return row.a; },
+        width: '150px'
+      },
+      {
+        title: 'a',
+        value: function(row) { return row.b; },
+        width: '50%'
+      }
+    ];
+
+    var rows = [
+      {
+        a: 'aToplevel',
+        b: 'bToplevel',
+        isExpanded: true,
+        subRows: [
+          {
+            a: 'a1',
+            b: 'b1'
+          }
+        ]
+      }
+    ];
+
+    var table = document.createElement('tracing-analysis-nested-table');
+    table.tableColumns = columns;
+    table.tableRows = rows;
+    table.rebuild();
+    this.addHTMLOutput(table);
+
+    var a1El = tv.b.findDeepElementMatchingPredicate(table, function(element) {
+      return element.textContent == 'a1';
+    });
+    assert.isDefined(a1El);
+
+    var bToplevelEl = tv.b.findDeepElementMatchingPredicate(
+        table,
+        function(element) {
+          return element.textContent == 'bToplevel';
+        });
+    assert.isDefined(bToplevelEl);
+    var expandButton = bToplevelEl.parentElement.querySelector('expand-button');
+    assert.isTrue(expandButton.classList.contains('button-expanded'));
+  });
+
+
+  test('instantiateTableWithHiddenHeader', function() {
+    var columns = [
+      {
+        title: 'a',
+        value: function(row) { return row.a; },
+        width: '150px'
+      },
+      {
+        title: 'a',
+        value: function(row) { return row.b; },
+        width: '50%'
+      }
+    ];
+
+    var rows = [
+      {
+        a: 'aToplevel',
+        b: 'bToplevel'
+      }
+    ];
+
+    var table = document.createElement('tracing-analysis-nested-table');
+    table.showHeader = false;
+    table.tableColumns = columns;
+    table.tableRows = rows;
+    table.rebuild();
+    this.addHTMLOutput(table);
+
+    var tHead = table.$.head;
+    assert.equal(table.$.head.children.length, 0);
+    assert.equal(0, tHead.getBoundingClientRect().height);
+
+    table.showHeader = true;
+    table.rebuild();
+    table.showHeader = false;
+    table.rebuild();
+    assert.equal(table.$.head.children.length, 0);
+  });
+
+
+  test('sortColumnsNotPossibleOnPercentSizedColumns', function() {
+    var columns = [
+      {
+        title: 'Title',
+        value: function(row) { return row.a; },
+        width: '150px'
+      },
+      {
+        title: 'Value',
+        value: function(row) { return row.b; },
+        width: '100%',
+        showExpandButtons: true
+      }
+    ];
+
+    var table1 = document.createElement('tracing-analysis-nested-table');
+    table1.showHeader = true;
+
+    assert.throws(function() {
+      table1.tableColumns = columns;
+    });
+  });
+
+  test('twoTablesFirstColumnMatching', function() {
+    var columns = [
+      {
+        title: 'Title',
+        value: function(row) { return row.a; },
+        width: '150px'
+      },
+      {
+        title: 'Value',
+        value: function(row) { return row.b; },
+        width: '100%'
+      }
+    ];
+
+    var table1 = document.createElement('tracing-analysis-nested-table');
+    table1.showHeader = true;
+    table1.tableColumns = columns;
+    table1.tableRows = [
+      {
+        a: 'first',
+        b: 'row'
+      }
+    ];
+    table1.rebuild();
+    this.addHTMLOutput(table1);
+
+    var table2 = document.createElement('tracing-analysis-nested-table');
+    table2.showHeader = false;
+    table2.tableColumns = columns;
+    table2.tableRows = [
+      {
+        a: 'second',
+        b: 'row'
+      }
+    ];
+    table2.rebuild();
+    this.addHTMLOutput(table2);
+
+    var h1FirstCol = table1.$.head.children[0].children[0];
+    var h2FirstCol = table2.$.body.children[0].children[0];
+    assert.equal(h1FirstCol.getBoundingClientRect().width,
+                 h2FirstCol.getBoundingClientRect().width);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/time_span.html b/trace-viewer/trace_viewer/core/analysis/time_span.html
new file mode 100644
index 0000000..065daec
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/time_span.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+tv.exportTo('tv.c.analysis', function() {
+  function createTimeSpan(duration) {
+    if (duration === undefined)
+      return '';
+    var span = document.createElement('tv-c-a-time-span');
+    span.duration = duration;
+    return span;
+  }
+  return {
+    createTimeSpan: createTimeSpan
+  };
+});
+</script>
+
+<polymer-element name="tv-c-a-time-span">
+  <template>
+    <style>
+    :host {
+      display: flex;
+      flex-direction: row;
+      align-items: center;
+    }
+    #warning {
+      margin-left: 4px;
+      font-size: 66%;
+    }
+    </style>
+    <span id="content"></span>
+    <span id="warning" style="display:none">&#9888;</span>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.warning_ = undefined;
+      this.duration_ = undefined;
+    },
+
+    get duration() {
+      return this.duration_;
+    },
+
+    set duration(duration) {
+      this.duration_ = duration;
+      this.$.content.textContent = tv.c.analysis.tsString(duration);
+    },
+
+    get warning() {
+      return this.warning_;
+    },
+
+    set warning(warning) {
+      this.warning_ = warning;
+      var warningEl = this.$.warning;
+      if (this.warning_) {
+        warningEl.title = warning;
+        warningEl.style.display = '';
+      } else {
+        warningEl.title = '';
+        warningEl.style.display = 'none';
+      }
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/time_span_test.html b/trace-viewer/trace_viewer/core/analysis/time_span_test.html
new file mode 100644
index 0000000..231204b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/time_span_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/time_span.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var timeSpan = document.createElement('tv-c-a-time-span');
+    timeSpan.duration = 73;
+    this.addHTMLOutput(timeSpan);
+  });
+  test('instantiateWithWarning', function() {
+    var timeSpan = document.createElement('tv-c-a-time-span');
+    timeSpan.duration = 400;
+    timeSpan.warning = 'there is a problem with this time';
+    this.addHTMLOutput(timeSpan);
+  });
+
+  test('warningAndNonWarningHaveSimilarHeights', function() {
+    var spanA = document.createElement('tv-c-a-time-span');
+    spanA.duration = 400;
+
+    var spanB = document.createElement('tv-c-a-time-span');
+    spanB.duration = 400;
+    spanB.warning = 'there is a problem with this time';
+
+    var overall = document.createElement('div');
+    overall.style.display = 'flex';
+    overall.appendChild(spanA);
+    spanB.style.marginLeft = '4px';
+    overall.appendChild(spanB);
+    this.addHTMLOutput(overall);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/time_stamp.html b/trace-viewer/trace_viewer/core/analysis/time_stamp.html
new file mode 100644
index 0000000..fdfbe4c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/time_stamp.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+tv.exportTo('tv.c.analysis', function() {
+  function createTimeStamp(timestamp) {
+    if (timestamp === undefined)
+      return '';
+    var span = document.createElement('tv-c-a-time-stamp');
+    span.timestamp = timestamp;
+    return span;
+  }
+  return {
+    createTimeStamp: createTimeStamp
+  };
+});
+</script>
+
+<polymer-element name="tv-c-a-time-stamp">
+  <template>
+  </template>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.timestamp_ = undefined;
+    },
+
+    get timestamp() {
+      return this.timestamp_;
+    },
+
+    set timestamp(timestamp) {
+      this.timestamp_ = timestamp;
+      this.shadowRoot.textContent = tv.c.analysis.tsString(timestamp);
+    }
+  });
+  </script>
+</polymer>
diff --git a/trace-viewer/trace_viewer/core/analysis/time_stamp_test.html b/trace-viewer/trace_viewer/core/analysis/time_stamp_test.html
new file mode 100644
index 0000000..aa9b074
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/time_stamp_test.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/analysis/time_stamp.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var timeStamp = document.createElement('tv-c-a-time-stamp');
+    timeStamp.timestamp = 73;
+    this.addHTMLOutput(timeStamp);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/toggle_container.html b/trace-viewer/trace_viewer/core/analysis/toggle_container.html
new file mode 100644
index 0000000..bf212bf
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/toggle_container.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<!--
+@fileoverview A basic container that shows and hides itself based on an
+event fired by a specified target.
+-->
+
+<polymer-element name="tracing-analysis-toggle-container"
+    constructor="TracingAnalysisToggleContainer">
+  <template>
+    <style>
+      :host(:[visible]) {
+        display: flex;
+      }
+
+      :host(:not([visible])) {
+        display: none;
+      }
+
+      ::content > * {
+        flex: 0 1 auto;
+      }
+
+    </style>
+    <content></content>
+  </template>
+  <script>
+  'use strict';
+  Polymer({
+    /**
+     * The visible property governs wether the component displays
+     * it's contents or not.
+     */
+    publish: {
+      visible: {
+        value: false,
+        reflect: true
+      }
+    },
+
+    created: function() {
+      this.toggleListeners_ = [];
+    },
+
+    toggleVisible: function() {
+      this.visible = !this.visible;
+    },
+
+    setToggleListener: function(target, eventType) {
+      var listenerFunction = this.toggleVisible.bind(this);
+      target.addEventListener(eventType, listenerFunction, false);
+      this.toggleListeners_.push({
+        target: target,
+        eventType: eventType,
+        listenerFunction: listenerFunction
+      });
+    },
+
+    clearToggleListener: function(target, eventType) {
+      for (var i = 0; i < this.toggleListeners_.length; i++) {
+        var listener = this.toggleListeners_[i];
+        if (listener.target === target && listener.eventType === eventType) {
+          target.removeEventListener(listener.eventType,
+                                     listener.listenerFunction, false);
+          this.toggleListeners_.splice(i, 1);
+          return;
+        }
+      }
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/analysis/toggle_container_test.html b/trace-viewer/trace_viewer/core/analysis/toggle_container_test.html
new file mode 100644
index 0000000..616ccd7
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/toggle_container_test.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/toggle_container.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var THIS_DOC = document._currentScript.ownerDocument;
+
+  test('instantiateVisibleProperty', function() {
+    var container = THIS_DOC.createElement('div');
+    var firstToggleContainer = new TracingAnalysisToggleContainer();
+    var secondToggleContainer = new TracingAnalysisToggleContainer();
+    var defaultSettingsToggleContainer = new TracingAnalysisToggleContainer();
+
+    var visibleChild = THIS_DOC.createElement('div');
+    var hiddenChild = THIS_DOC.createElement('div');
+    var defaultHiddenChild = THIS_DOC.createElement('div');
+
+    var button1 = THIS_DOC.createElement('button');
+    button1.textContent = '1: Toggle Visibility';
+
+    var button2 = THIS_DOC.createElement('button');
+    button2.textContent = '2: Toggle Visibility';
+
+    var button3 = THIS_DOC.createElement('button');
+    button3.textContent = '3: Toggle Visibility';
+
+    var buttonToggleAll = THIS_DOC.createElement('button');
+    buttonToggleAll.textContent = 'Toggle All';
+
+    visibleChild.textContent = '1: Should be visible';
+    hiddenChild.textContent = '2: Should not be visible';
+    defaultHiddenChild.textContent = '3: Should also not be visible';
+
+    firstToggleContainer.visible = true;
+    secondToggleContainer.visible = false;
+    // Default toggle container should have default visibility set to false.
+
+    firstToggleContainer.appendChild(visibleChild);
+    secondToggleContainer.appendChild(hiddenChild);
+    defaultSettingsToggleContainer.appendChild(defaultHiddenChild);
+
+    container.appendChild(button1);
+    container.appendChild(button2);
+    container.appendChild(button3);
+    container.appendChild(buttonToggleAll);
+    container.appendChild(firstToggleContainer);
+    container.appendChild(secondToggleContainer);
+    container.appendChild(defaultSettingsToggleContainer);
+
+    firstToggleContainer.setToggleListener(button1, 'click');
+    secondToggleContainer.setToggleListener(button2, 'click');
+    defaultSettingsToggleContainer.setToggleListener(button3, 'click');
+
+    firstToggleContainer.setToggleListener(buttonToggleAll, 'click');
+    secondToggleContainer.setToggleListener(buttonToggleAll, 'click');
+    defaultSettingsToggleContainer.setToggleListener(buttonToggleAll, 'click');
+
+    this.addHTMLOutput(container);
+  });
+
+  test('visiblePropertyReflection', function() {
+    var toggleContainer = new TracingAnalysisToggleContainer();
+
+    assert.isFalse(toggleContainer.hasAttribute('visible'));
+    toggleContainer.visible = true;
+    assert.isTrue(toggleContainer.hasAttribute('visible'));
+    toggleContainer.visible = false;
+    assert.isFalse(toggleContainer.hasAttribute('visible'));
+    toggleContainer.setAttribute('visible', 'true');
+    assert.isTrue(toggleContainer.visible);
+    toggleContainer.removeAttribute('visible');
+    assert.isFalse(toggleContainer.visible);
+  });
+
+  test('setAndClearToggleListener', function() {
+    var toggleContainer = new TracingAnalysisToggleContainer();
+
+    var firstTarget = THIS_DOC.createElement('div');
+    var secondTarget = THIS_DOC.createElement('div');
+    var invalidTarget = THIS_DOC.createElement('div');
+    toggleContainer.setToggleListener(firstTarget, 'click');
+    toggleContainer.setToggleListener(secondTarget, 'click');
+
+    var clickEvent = new MouseEvent('click', {
+      'view': window,
+      'bubbles': true,
+      'cancelable': true
+    });
+
+    firstTarget.dispatchEvent(clickEvent);
+    assert.isTrue(toggleContainer.visible);
+
+    firstTarget.dispatchEvent(clickEvent);
+    assert.isFalse(toggleContainer.visible);
+
+    secondTarget.dispatchEvent(clickEvent);
+    assert.isTrue(toggleContainer.visible);
+
+    secondTarget.dispatchEvent(clickEvent);
+    assert.isFalse(toggleContainer.visible);
+
+    toggleContainer.clearToggleListener(firstTarget, 'click');
+    firstTarget.dispatchEvent(clickEvent);
+    // This event should not toggle the state.
+    assert.isFalse(toggleContainer.visible);
+
+    secondTarget.dispatchEvent(clickEvent);
+    // This event should toggle the state.
+    assert.isTrue(toggleContainer.visible);
+
+    toggleContainer.clearToggleListener(invalidTarget, 'click');
+    secondTarget.dispatchEvent(clickEvent);
+    // This event should toggle the state.
+    assert.isFalse(toggleContainer.visible);
+
+    toggleContainer.clearToggleListener(secondTarget, 'invalidEventName');
+    secondTarget.dispatchEvent(clickEvent);
+    // This event should toggle the state.
+    assert.isTrue(toggleContainer.visible);
+
+    toggleContainer.clearToggleListener(secondTarget, 'click');
+    secondTarget.dispatchEvent(clickEvent);
+    // This event should not toggle the state as we've removed the listener.
+    assert.isTrue(toggleContainer.visible);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/analysis/util.html b/trace-viewer/trace_viewer/core/analysis/util.html
new file mode 100644
index 0000000..8fb952a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/analysis/util.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for use in selection_analysis files.
+ */
+tv.exportTo('tv.c.analysis', function() {
+  function tsString(ts) {
+    return Number(parseFloat(tsRound(ts)).toFixed(3)).toLocaleString() + ' ms';
+  }
+
+  function tsRound(ts) {
+    return Math.round(ts * 1000.0) / 1000.0;
+  }
+
+  return {
+    tsString: tsString,
+    tsRound: tsRound
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/auditor.html b/trace-viewer/trace_viewer/core/auditor.html
new file mode 100644
index 0000000..590f5dd
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/auditor.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for auditors.
+ */
+tv.exportTo('tv.c', function() {
+  function Auditor(model) {
+  }
+
+  Auditor.prototype = {
+    __proto__: Object.prototype,
+
+    /**
+     * Called by the Model after baking slices. May modify model.
+     */
+    runAnnotate: function() {
+    },
+
+    /**
+     * Called by the Model after importing. Should not modify model, except
+     * for adding interaction ranges and audits.
+     */
+    runAudit: function() {
+    }
+  };
+
+  var options = new tv.b.ExtensionRegistryOptions(tv.b.BASIC_REGISTRY_MODE);
+  options.defaultMetadata = {};
+  options.mandatoryBaseClass = Auditor;
+  tv.b.decorateExtensionRegistry(Auditor, options);
+
+  return {
+    Auditor: Auditor
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/constants.html b/trace-viewer/trace_viewer/core/constants.html
new file mode 100644
index 0000000..2fb03c4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/constants.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  var constants = {
+    HEADING_WIDTH: 250
+  };
+
+  return {
+    constants: constants
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/draw_helpers.html b/trace-viewer/trace_viewer/core/draw_helpers.html
new file mode 100644
index 0000000..e15af3a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/draw_helpers.html
@@ -0,0 +1,391 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/event_presenter.html">
+<link rel="import" href="/core/elided_cache.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides various helper methods for drawing to a provided
+ * canvas.
+ */
+tv.exportTo('tv.c', function() {
+  var elidedTitleCache = new tv.c.ElidedTitleCache();
+  var palette = tv.b.ui.getColorPalette();
+  var EventPresenter = tv.c.EventPresenter;
+
+  /**
+   * This value is used to allow for consistent style UI elements.
+   * Thread time visualisation uses a smaller rectangle that has this height.
+   * @const
+   */
+  var THIN_SLICE_HEIGHT = 4;
+
+  /**
+   * This value is used to for performance considerations when drawing large
+   * zoomed out traces that feature cpu time in the slices. If the waiting
+   * width is less than the threshold, we only draw the rectangle as a solid.
+   * @const
+   */
+  var SLICE_WAITING_WIDTH_DRAW_THRESHOLD = 3;
+
+  /**
+   * If the slice has mostly been waiting to be scheduled on the cpu, the
+   * wall clock will be far greater than the cpu clock. Draw the slice
+   * only as an idle slice, if the active width is not thicker than the
+   * threshold.
+   * @const
+   */
+  var SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD = 1;
+
+  /**
+   * Should we elide text on trace labels?
+   * Without eliding, text that is too wide isn't drawn at all.
+   * Disable if you feel this causes a performance problem.
+   * This is a default value that can be overridden in tracks for testing.
+   * @const
+   */
+  var SHOULD_ELIDE_TEXT = true;
+
+  /**
+   * Draw the define line into |ctx|.
+   *
+   * @param {Context} ctx The context to draw into.
+   * @param {float} x1 The start x position of the line.
+   * @param {float} y1 The start y position of the line.
+   * @param {float} x2 The end x position of the line.
+   * @param {float} y2 The end y position of the line.
+   */
+  function drawLine(ctx, x1, y1, x2, y2) {
+    ctx.moveTo(x1, y1);
+    ctx.lineTo(x2, y2);
+  }
+
+  /**
+   * Draw the defined triangle into |ctx|.
+   *
+   * @param {Context} ctx The context to draw into.
+   * @param {float} x1 The first corner x.
+   * @param {float} y1 The first corner y.
+   * @param {float} x2 The second corner x.
+   * @param {float} y2 The second corner y.
+   * @param {float} x3 The third corner x.
+   * @param {float} y3 The third corner y.
+   */
+  function drawTriangle(ctx, x1, y1, x2, y2, x3, y3) {
+    ctx.beginPath();
+    ctx.moveTo(x1, y1);
+    ctx.lineTo(x2, y2);
+    ctx.lineTo(x3, y3);
+    ctx.closePath();
+  }
+
+  /**
+   * Draw an arrow into |ctx|.
+   *
+   * @param {Context} ctx The context to draw into.
+   * @param {float} x1 The shaft x.
+   * @param {float} y1 The shaft y.
+   * @param {float} x2 The head x.
+   * @param {float} y2 The head y.
+   * @param {float} arrowLength The length of the head.
+   * @param {float} arrowWidth The width of the head.
+   */
+  function drawArrow(ctx, x1, y1, x2, y2, arrowLength, arrowWidth) {
+    var dx = x2 - x1;
+    var dy = y2 - y1;
+    var len = Math.sqrt(dx * dx + dy * dy);
+    var perc = (len - arrowLength) / len;
+    var bx = x1 + perc * dx;
+    var by = y1 + perc * dy;
+    var ux = dx / len;
+    var uy = dy / len;
+    var ax = uy * arrowWidth;
+    var ay = -ux * arrowWidth;
+
+    ctx.beginPath();
+    drawLine(ctx, x1, y1, x2, y2);
+    ctx.stroke();
+
+    drawTriangle(ctx,
+        bx + ax, by + ay,
+        x2, y2,
+        bx - ax, by - ay);
+    ctx.fill();
+  }
+
+  /**
+   * Draw the provided slices to the screen.
+   *
+   * Each of the elements in |slices| must provide the follow methods:
+   *   * start
+   *   * duration
+   *   * colorId
+   *   * selected
+   *
+   * @param {Context} ctx The canvas context.
+   * @param {TimelineDrawTransform} dt The draw transform.
+   * @param {float} viewLWorld The left most point of the world viewport.
+   * @param {float} viewRWorld The right most point of the world viewport.
+   * @param {float} viewHeight The height of the viewport.
+   * @param {Array} slices The slices to draw.
+   * @param {bool} async Whether the slices are drawn with async style.
+   */
+  function drawSlices(ctx, dt, viewLWorld, viewRWorld, viewHeight, slices,
+                      async) {
+    var pixelRatio = window.devicePixelRatio || 1;
+    var pixWidth = dt.xViewVectorToWorld(1);
+    var height = viewHeight * pixelRatio;
+
+    var darkRectHeight = THIN_SLICE_HEIGHT * pixelRatio;
+
+    // Not enough space for both colors, use light color only.
+    if (height < darkRectHeight)
+      darkRectHeight = 0;
+
+    var lightRectHeight = height - darkRectHeight;
+
+    // Begin rendering in world space.
+    ctx.save();
+    dt.applyTransformToCanvas(ctx);
+
+    var tr = new tv.c.FastRectRenderer(
+        ctx, 2 * pixWidth, 2 * pixWidth, palette);
+    tr.setYandH(0, height);
+
+    var lowSlice = tv.b.findLowIndexInSortedArray(
+        slices,
+        function(slice) { return slice.start + slice.duration; },
+        viewLWorld);
+
+    for (var i = lowSlice; i < slices.length; ++i) {
+      var slice = slices[i];
+      var x = slice.start;
+      if (x > viewRWorld)
+        break;
+
+      var w = pixWidth;
+      if (slice.duration > 0) {
+        w = Math.max(slice.duration, 0.001);
+        if (w < pixWidth)
+          w = pixWidth;
+      }
+
+      var colorId = EventPresenter.getSliceColorId(slice);
+      var alpha = EventPresenter.getSliceAlpha(slice, async);
+      var lightAlpha = alpha * 0.70;
+
+      // Shift the top level slice down, make it shorter, and draw a top border
+      // in order to visually separate the slice from events above it.
+      // See https://github.com/google/trace-viewer/issues/725.
+      if (slice.isTopLevel) {
+        ctx.beginPath();
+        drawLine(ctx, x, 2, w + x, 2);
+        ctx.lineWidth = 2;
+        ctx.stroke();
+        tr.setYandH(3, height - 3);
+      }
+
+      // If cpuDuration is available, draw rectangles proportional to the
+      // amount of cpu time taken.
+      if (!slice.cpuDuration) {
+        // No cpuDuration available, draw using only one alpha.
+        tr.fillRect(x, w, colorId, alpha);
+        continue;
+      }
+
+      var activeWidth = w * (slice.cpuDuration / slice.duration);
+      var waitingWidth = w - activeWidth;
+
+      // Check if we have enough screen space to draw the whole slice, with
+      // both color tones.
+      //
+      // Truncate the activeWidth to 0 if it is less than 'threshold' pixels.
+      if (activeWidth < SLICE_ACTIVE_WIDTH_DRAW_THRESHOLD * pixWidth) {
+        activeWidth = 0;
+        waitingWidth = w;
+      }
+
+      // Truncate the waitingWidth to 0 if it is less than 'threshold' pixels.
+      if (waitingWidth < SLICE_WAITING_WIDTH_DRAW_THRESHOLD * pixWidth) {
+        activeWidth = w;
+        waitingWidth = 0;
+      }
+
+      // We now draw the two rectangles making up the event slice.
+      // NOTE: The if statements are necessary for performance considerations.
+      // We do not want to force draws, if the width of the rectangle is 0.
+      //
+      // First draw the solid color, representing the 'active' part.
+      if (activeWidth > 0) {
+        tr.fillRect(x, activeWidth, colorId, alpha);
+      }
+
+      // Next draw the two toned 'idle' part.
+      // NOTE: Substracting pixWidth and drawing one extra pixel is done to
+      // prevent drawing artifacts. Without it, the two parts of the slice,
+      // ('active' and 'idle') may appear split apart.
+      if (waitingWidth > 0) {
+        // First draw the light toned top part.
+        tr.setYandH(0, lightRectHeight);
+        tr.fillRect(x + activeWidth - pixWidth,
+            waitingWidth + pixWidth, colorId, lightAlpha);
+        // Then the solid bottom half.
+        tr.setYandH(lightRectHeight, darkRectHeight);
+        tr.fillRect(x + activeWidth - pixWidth,
+            waitingWidth + pixWidth, colorId, alpha);
+        // Reset for the next slice.
+        tr.setYandH(0, height);
+      }
+    }
+    tr.flush();
+    ctx.restore();
+  }
+
+  /**
+   * Draw the provided instant slices as lines to the screen.
+   *
+   * Each of the elements in |slices| must provide the follow methods:
+   *   * start
+   *   * duration with value of 0.
+   *   * colorId
+   *   * selected
+   *
+   * @param {Context} ctx The canvas context.
+   * @param {TimelineDrawTransform} dt The draw transform.
+   * @param {float} viewLWorld The left most point of the world viewport.
+   * @param {float} viewRWorld The right most point of the world viewport.
+   * @param {float} viewHeight The height of the viewport.
+   * @param {Array} slices The slices to draw.
+   * @param {Numer} lineWidthInPixels The width of the lines.
+   */
+  function drawInstantSlicesAsLines(
+      ctx, dt, viewLWorld, viewRWorld, viewHeight, slices, lineWidthInPixels) {
+    var pixelRatio = window.devicePixelRatio || 1;
+    var height = viewHeight * pixelRatio;
+
+    var pixWidth = dt.xViewVectorToWorld(1);
+
+    // Begin rendering in world space.
+    ctx.save();
+    ctx.lineWidth = pixWidth * lineWidthInPixels * pixelRatio;
+    dt.applyTransformToCanvas(ctx);
+    ctx.beginPath();
+
+    var lowSlice = tv.b.findLowIndexInSortedArray(
+        slices,
+        function(slice) { return slice.start; },
+        viewLWorld);
+
+    for (var i = lowSlice; i < slices.length; ++i) {
+      var slice = slices[i];
+      var x = slice.start;
+      if (x > viewRWorld)
+        break;
+
+      ctx.strokeStyle = EventPresenter.getInstantSliceColor(slice);
+
+      ctx.beginPath();
+      ctx.moveTo(x, 0);
+      ctx.lineTo(x, height);
+      ctx.stroke();
+    }
+    ctx.restore();
+  }
+
+  /**
+   * Draws the labels for the given slices.
+   *
+   * The |slices| array must contain objects with the following API:
+   *   * start
+   *   * duration
+   *   * title
+   *   * didNotFinish (optional)
+   *
+   * @param {Context} ctx The graphics context.
+   * @param {TimelineDrawTransform} dt The draw transform.
+   * @param {float} viewLWorld The left most point of the world viewport.
+   * @param {float} viewRWorld The right most point of the world viewport.
+   * @param {Array} slices The slices to label.
+   * @param {bool} async Whether the slice labels are drawn with async style.
+   * @param {float} fontSize The font size.
+   * @param {float} yOffset The font offset.
+   */
+  function drawLabels(ctx, dt, viewLWorld, viewRWorld, slices, async,
+                      fontSize, yOffset) {
+    var pixelRatio = window.devicePixelRatio || 1;
+    var pixWidth = dt.xViewVectorToWorld(1);
+
+    ctx.save();
+
+    ctx.textAlign = 'center';
+    ctx.textBaseline = 'top';
+    ctx.font = (fontSize * pixelRatio) + 'px sans-serif';
+
+    if (async)
+      ctx.font = 'italic ' + ctx.font;
+
+    var cY = yOffset * pixelRatio;
+
+    var lowSlice = tv.b.findLowIndexInSortedArray(
+        slices,
+        function(slice) { return slice.start + slice.duration; },
+        viewLWorld);
+
+    // Don't render text until until it is 20px wide
+    var quickDiscardThresshold = pixWidth * 20;
+    for (var i = lowSlice; i < slices.length; ++i) {
+      var slice = slices[i];
+      if (slice.start > viewRWorld)
+        break;
+
+      if (slice.duration <= quickDiscardThresshold)
+        continue;
+
+      var title = slice.title +
+          (slice.didNotFinish ? ' (Did Not Finish)' : '');
+
+      var drawnTitle = title;
+      var drawnWidth = elidedTitleCache.labelWidth(ctx, drawnTitle);
+      var fullLabelWidth = elidedTitleCache.labelWidthWorld(
+          ctx, drawnTitle, pixWidth);
+      if (SHOULD_ELIDE_TEXT && fullLabelWidth > slice.duration) {
+        var elidedValues = elidedTitleCache.get(
+            ctx, pixWidth,
+            drawnTitle, drawnWidth,
+            slice.duration);
+        drawnTitle = elidedValues.string;
+        drawnWidth = elidedValues.width;
+      }
+
+      if (drawnWidth * pixWidth < slice.duration) {
+        ctx.fillStyle = EventPresenter.getTextColor(slice);
+        var cX = dt.xWorldToView(slice.start + 0.5 * slice.duration);
+        ctx.fillText(drawnTitle, cX, cY, drawnWidth);
+      }
+    }
+    ctx.restore();
+  }
+
+  return {
+    drawSlices: drawSlices,
+    drawInstantSlicesAsLines: drawInstantSlicesAsLines,
+    drawLabels: drawLabels,
+
+    drawLine: drawLine,
+    drawTriangle: drawTriangle,
+    drawArrow: drawArrow,
+
+    elidedTitleCache_: elidedTitleCache,
+
+    THIN_SLICE_HEIGHT: THIN_SLICE_HEIGHT
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/elided_cache.html b/trace-viewer/trace_viewer/core/elided_cache.html
new file mode 100644
index 0000000..ec9cf6f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/elided_cache.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides a caching layer for elided text values.
+ */
+tv.exportTo('tv.c', function() {
+  /**
+   * Cache for elided strings.
+   * Moved from the ElidedTitleCache protoype to a "global" for speed
+   * (variable reference is 100x faster).
+   *   key: String we wish to elide.
+   *   value: Another dict whose key is width
+   *     and value is an ElidedStringWidthPair.
+   */
+  var elidedTitleCacheDict = {};
+  var elidedTitleCache = new ElidedTitleCache();
+
+  /**
+   * A cache for elided strings.
+   * @constructor
+   */
+  function ElidedTitleCache() {
+    // TODO(jrg): possibly obsoleted with the elided string cache.
+    // Consider removing.
+    this.textWidthMap = {};
+  }
+
+  ElidedTitleCache.prototype = {
+    /**
+     * Return elided text.
+     *
+     * @param {ctx} Context The graphics context.
+     * @param {pixWidth} Pixel width.
+     * @param {title} Original title text.
+     * @param {width} Drawn width in world coords.
+     * @param {sliceDuration} Where the title must fit (in world coords).
+     * @return {ElidedStringWidthPair} Elided string and width.
+     */
+    get: function(ctx, pixWidth, title, width, sliceDuration) {
+      var elidedDict = elidedTitleCacheDict[title];
+      if (!elidedDict) {
+        elidedDict = {};
+        elidedTitleCacheDict[title] = elidedDict;
+      }
+
+      var elidedDictForPixWidth = elidedDict[pixWidth];
+      if (!elidedDictForPixWidth) {
+        elidedDict[pixWidth] = {};
+        elidedDictForPixWidth = elidedDict[pixWidth];
+      }
+
+      var stringWidthPair = elidedDictForPixWidth[sliceDuration];
+      if (stringWidthPair === undefined) {
+        var newtitle = title;
+        var elided = false;
+        while (this.labelWidthWorld(ctx, newtitle, pixWidth) > sliceDuration) {
+          if (newtitle.length * 0.75 < 1)
+            break;
+          newtitle = newtitle.substring(0, newtitle.length * 0.75);
+          elided = true;
+        }
+
+        if (elided && newtitle.length > 3)
+          newtitle = newtitle.substring(0, newtitle.length - 3) + '...';
+
+        stringWidthPair = new ElidedStringWidthPair(
+            newtitle, this.labelWidth(ctx, newtitle));
+        elidedDictForPixWidth[sliceDuration] = stringWidthPair;
+      }
+      return stringWidthPair;
+    },
+
+    quickMeasureText_: function(ctx, text) {
+      var w = this.textWidthMap[text];
+      if (!w) {
+        w = ctx.measureText(text).width;
+        this.textWidthMap[text] = w;
+      }
+      return w;
+    },
+
+    labelWidth: function(ctx, title) {
+      return this.quickMeasureText_(ctx, title) + 2;
+    },
+
+    labelWidthWorld: function(ctx, title, pixWidth) {
+      return this.labelWidth(ctx, title) * pixWidth;
+    }
+  };
+
+  /**
+   * A pair representing an elided string and world-coordinate width
+   * to draw it.
+   * @constructor
+   */
+  function ElidedStringWidthPair(string, width) {
+    this.string = string;
+    this.width = width;
+  }
+
+  return {
+    ElidedTitleCache: ElidedTitleCache
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/event_presenter.html b/trace-viewer/trace_viewer/core/event_presenter.html
new file mode 100644
index 0000000..104b9a6
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/event_presenter.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/base/ui/color_scheme.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides color scheme related functions.
+ */
+tv.exportTo('tv.c', function() {
+  var paletteRaw = tv.b.ui.getRawColorPalette();
+  var palette = tv.b.ui.getColorPalette();
+
+  var SelectionState = tv.c.trace_model.SelectionState;
+
+  /**
+   * Provides methods to get view values for events.
+   */
+  var EventPresenter = {
+    getAlpha_: function(event) {
+      if (event.selectionState === SelectionState.DIMMED)
+        return 0.3;
+      return 1.0;
+    },
+
+    getColorIdOffset_: function(event) {
+      if (event.selectionState === SelectionState.SELECTED)
+        return tv.b.ui.paletteProperties.highlightIdBoost;
+      return 0;
+    },
+
+    getTextColor: function(event) {
+      if (event.selectionState === SelectionState.DIMMED)
+        return 'rgb(60,60,60)';
+      return 'rgb(0,0,0)';
+    },
+
+    getSliceColorId: function(slice) {
+      return slice.colorId + this.getColorIdOffset_(slice);
+    },
+
+    getSliceAlpha: function(slice, async) {
+      var alpha = this.getAlpha_(slice);
+      if (async)
+        alpha *= 0.3;
+      return alpha;
+    },
+
+    getInstantSliceColor: function(instant) {
+      var colorId = instant.colorId + this.getColorIdOffset_(instant);
+      return tv.b.ui.colorToRGBAString(paletteRaw[colorId],
+                                       this.getAlpha_(instant));
+    },
+
+    getObjectInstanceColor: function(instance) {
+      var colorId = instance.colorId + this.getColorIdOffset_(instance);
+      return tv.b.ui.colorToRGBAString(paletteRaw[colorId], 0.25);
+    },
+
+    getObjectSnapshotColor: function(snapshot) {
+      var colorId =
+          snapshot.objectInstance.colorId + this.getColorIdOffset_(snapshot);
+      return palette[colorId];
+    },
+
+    getCounterSeriesColor: function(colorId, selectionState,
+                                    opt_alphaMultiplier) {
+      var event = {selectionState: selectionState};
+      return tv.b.ui.colorToRGBAString(
+          paletteRaw[colorId + this.getColorIdOffset_(event)],
+          this.getAlpha_(event) *
+              (opt_alphaMultiplier !== undefined ? opt_alphaMultiplier : 1.0));
+    },
+
+    getBarSnapshotColor: function(snapshot, offset) {
+      var colorId =
+          (snapshot.objectInstance.colorId + offset) %
+          tv.b.ui.paletteProperties.numGeneralPurposeColorIds;
+      colorId += this.getColorIdOffset_(snapshot);
+      return tv.b.ui.colorToRGBAString(paletteRaw[colorId],
+                                       this.getAlpha_(snapshot));
+    }
+  };
+
+  return {
+    EventPresenter: EventPresenter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/fast_rect_renderer.html b/trace-viewer/trace_viewer/core/fast_rect_renderer.html
new file mode 100644
index 0000000..6a6603e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/fast_rect_renderer.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides a mechanism for drawing massive numbers of
+ * colored rectangles into a canvas in an efficient manner, provided
+ * they are drawn left to right with fixed y and height throughout.
+ *
+ * The basic idea used here is to fuse subpixel rectangles together so that
+ * we never issue a canvas fillRect for them. It turns out Javascript can
+ * do this quite efficiently, compared to asking Canvas2D to do the same.
+ *
+ * A few extra things are done by this class in the name of speed:
+ * - Viewport culling: off-viewport rectangles are discarded.
+ *
+ * - The actual discarding operation is done in world space,
+ *   e.g. pre-transform.
+ *
+ * - Rather than expending compute cycles trying to figure out an average
+ *   color for fused rectangles from css strings, you instead draw using
+ *   palletized colors. The fused rect color is choosen from the rectangle with
+ *   the higher alpha value, if equal the max pallete index encountered.
+ *
+ * Make sure to flush the trackRenderer before finishing drawing in order
+ * to commit any queued drawing operations.
+ */
+tv.exportTo('tv.c', function() {
+
+  /**
+   * Creates a fast rect renderer with a specific set of culling rules
+   * and color pallette.
+   * @param {GraphicsContext2D} ctx Canvas2D drawing context.
+   * @param {number} minRectSize Only rectangles with width < minRectSize are
+   *    considered for merging.
+   * @param {number} maxMergeDist Controls how many successive small rectangles
+   *    can be merged together before issuing a rectangle.
+   * @param {Array} pallette The color pallete for drawing. Pallette slots
+   *    should map to valid Canvas fillStyle strings.
+   *
+   * @constructor
+   */
+  function FastRectRenderer(ctx, minRectSize, maxMergeDist, pallette) {
+    this.ctx_ = ctx;
+    this.minRectSize_ = minRectSize;
+    this.maxMergeDist_ = maxMergeDist;
+    this.pallette_ = pallette;
+  }
+
+  FastRectRenderer.prototype = {
+    y_: 0,
+    h_: 0,
+    merging_: false,
+    mergeStartX_: 0,
+    mergeCurRight_: 0,
+    mergedColorId_: 0,
+    mergedAlpha_: 0,
+
+    /**
+     * Changes the y position and height for subsequent fillRect
+     * calls. x and width are specifieid on the fillRect calls.
+     */
+    setYandH: function(y, h) {
+      this.flush();
+      this.y_ = y;
+      this.h_ = h;
+    },
+
+    /**
+     * Fills rectangle at the specified location, if visible. If the
+     * rectangle is subpixel, it will be merged with adjacent rectangles.
+     * The drawing operation may not take effect until flush is called.
+     * @param {number} colorId The color of this rectangle, as an index
+     *     in the renderer's color pallete.
+     * @param {number} alpha The opacity of the rectangle as 0.0-1.0 number.
+     */
+    fillRect: function(x, w, colorId, alpha) {
+      var r = x + w;
+      if (w < this.minRectSize_) {
+        if (r - this.mergeStartX_ > this.maxMergeDist_)
+          this.flush();
+        if (!this.merging_) {
+          this.merging_ = true;
+          this.mergeStartX_ = x;
+          this.mergeCurRight_ = r;
+          this.mergedColorId_ = colorId;
+          this.mergedAlpha_ = alpha;
+        } else {
+          this.mergeCurRight_ = r;
+
+          if (this.mergedAlpha_ < alpha ||
+              (this.mergedAlpha_ === alpha && this.mergedColorId_ < colorId)) {
+            this.mergedAlpha_ = alpha;
+            this.mergedColorId_ = colorId;
+          }
+        }
+      } else {
+        if (this.merging_)
+          this.flush();
+        this.ctx_.fillStyle = this.pallette_[colorId];
+        this.ctx_.globalAlpha = alpha;
+        this.ctx_.fillRect(x, this.y_, w, this.h_);
+      }
+    },
+
+    /**
+     * Commits any pending fillRect operations to the underlying graphics
+     * context.
+     */
+    flush: function() {
+      if (this.merging_) {
+        this.ctx_.fillStyle = this.pallette_[this.mergedColorId_];
+        this.ctx_.globalAlpha = this.mergedAlpha_;
+        this.ctx_.fillRect(this.mergeStartX_, this.y_,
+                           this.mergeCurRight_ - this.mergeStartX_, this.h_);
+        this.merging_ = false;
+      }
+    }
+  };
+
+  return {
+    FastRectRenderer: FastRectRenderer
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/favicons.html b/trace-viewer/trace_viewer/core/favicons.html
new file mode 100644
index 0000000..d72d165
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/favicons.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+tv.exportTo('tv.c', function() {
+  var FaviconsByHue = {
+    blue: 'data:image/vndmicrosofticon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjj8xAGArIgqOPzE8nUY3dqJJOJeiSTiXnUY3do4/MTxhKyIKjkAxAAAAAAAAAAAAAAAAAAAAAABQJBwAAAAAAZJBMzSoSzqlsU8+6bRQP/21UT//tVE//7RQP/2wTz3ppko6pY9AMjQAAAABTyMbAAAAAAB7e3sAAP//AKFSRE+wTz3dtVE//7VRP/+1UT//tVE//7VRP/+zUD7/sE89/7BOPf+qTDvdl0M0TwAAAABWJx4A+fn5ANjd3TnIiX7ftVA9/7VRP/+1UT//tVE//7VRP/+xTz3/rE08/6xMO/+sTDv/rE08/6dKOt+SQTM5q0w7ALO0tA3v8fGu05uR/7NMOf+0Tzz/tE88/7RPPv+uTT3/p0o7/6ZJOv+mSTr/pkk6/6ZJOv+mSjr/n0Y4rnIwKg3h4eFK9/j48N2zrP/FeGr/xnps/8Z6bP/AaUv/tlw1/7RbNf+1WzX/tFs1/7RbNf+0WzX/tFs1/7NbNPCqWy1K7e3tjPn5+f/49vX/9vLy//by8v/28vH/8bZv/+6RH//ukyP/7pMj/+6SI//ukiP/7pMj/+2SIv/qjyL/34kfjPHx8bL5+fn/+fn5//n5+f/5+fr/+fn5//W7cP/zlB3/85Yh//OWIf/zliH/85Yh//GVIf/rkR//6ZAf/+KLHrLz8/O2+fn5//n5+f/5+fn/+fn5//n5+f/1unD/85Qd//OWIf/zliH/85Yh//CUIP/mjh//44we/+OMHv/diR628vLymfn5+f/5+fn/+fn5//n5+f/5+fn/9bx0//OXI//zmCb/85gm/++VIv/hjB//3Yoe/92KHv/dih7/2IYdmfHx8Vz4+Pj3+fn5//n5+f/5+fn/+fn5//jo0//33bv/9929//bbtf/euDX/06oJ/9OrC//Tqwv/06oM98yfD1zr6+sY9/f3xvn5+f/5+fn/+fn5//n5+f/5+vv/+fv8//n7/f/3+PH/3Ms6/9O8AP/UvQD/1L0A/9K8AMbItAAY////APT09Fb4+Pjy+fn5//n5+f/5+fn/+fn5//n5+f/5+fr/9/bu/9zKOf/TuwD/1LwA/9S8APLQuABW3cQAAOzs7ADm5uYF9vb2ePn5+fT5+fn/+fn5//n5+f/5+fn/+fn6//f27v/cyTn/07sA/9S8APTRugB4w60ABcmyAAAAAAAA8PDwAOzs7Ab29vZd+Pj40vn5+fz5+fn/+fn5//n5+f/49/H/5Ndu/NjEIdLSugBdybIABsy1AAAAAAAAAAAAAAAAAADn5+cAqKioAPT09CH39/dy+Pj4tvj4+NX4+PjV+Pj4tvX063Lt6MMhOQAAAM+/RAAAAAAAAAAAAPAPAADAAwAAwAMAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABAACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCwUEDDgZExxWJx4tYiwiN2IsIjdWJx4tOBkTHAsFBAwAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wAbDAkKZS0jMYs+MWydRjeipko6x6tMO9utTTzjrU0846tMO9umSjrHnUY3oos+MWxlLSMxGwwJCv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgZFAAPBwUHcjMoPJtFNpqsTTzhs1A+/LVRP/+2UT//tVE//7VRP/+1UT//tVE//7ZRP/+1UT//s1A+/KxNPOGbRTaacTInPA8HBQc4GRMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yp4AUCQcGZVDNICtTjzktVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+0UT//s1A+/7JQPv+rTDvkkkEzgE8jGxn/xZoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAA////AGswJSqiSTivs1A++7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tFA+/7FPPf+xTz3/sU89/7FPPf+vTj37nkc3r2guJCr///8AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAP/DogB/VEwsqE09v7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7NQPv+vTj3/r049/69OPf+vTj3/r049/69OPf+uTjz/oUg4v20xJiz/nnsAAgEBAAAAAAAAAAAAAAAAAAAAAAD19fUAkp2fHdK2sbW5W0r/tVA+/7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+yUD7/rU08/6xNPP+tTTz/rU08/61NPP+tTTz/rU08/61NPP+sTTz/nkY3tWAqIR2pSzsAAAAAAAAAAAAAAAAAeXl5ADY2Ngnd39+O6tbT/blbSv+1UD7/tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//slA+/6xNPP+rTDv/q0w7/6tMO/+rTDv/q0w7/6tMO/+rTDv/q0w7/6tMO/+qTDv9lkM0jiUQDQlSJR0AAAAAAAAAAAD///8AxMTES/X29u3s2NX/uVtK/7VQPv+1UT//tVE//7VRP/+1UT//tVE//7VRP/+1UT//tVE//7FPPv+qTDv/qEs6/6hLOv+oSzr/qEs6/6hLOv+oSzr/qEs6/6hLOv+oSzr/qEs6/6lLOv+lSTnthDsuS/+TcgAAAAAAm5ubAHBwcA/o6Oix+vv8/+zY1P+5W0r/tVA+/7VRP/+1UT//tVE//7VRP/+1UT//tVE//7VRP/+xTz3/qEs6/6ZKOv+mSjr/pko6/6ZKOv+mSjr/pko6/6ZKOv+mSjr/pko6/6ZKOv+mSjr/pko6/6ZKOv+bRTaxSiEaD2cuJAD///8AycnJRfX19fD6+/z/69fU/7hYR/+0Tjv/tE48/7ROPP+0Tjz/tE48/7ROPP+0Tz3/r04+/6VJOv+jSDn/o0g5/6NIOf+jSDn/o0g5/6NIOf+jSDn/o0g5/6NIOf+jSDr/o0g5/6NIOf+jSDn/o0g6/6BHOfCCOS9F0FxKAAAAAALk5OSN+fn5//n6+v/y5+X/05uS/9CTiP/QlIn/0JSJ/9CUif/QlIn/0JSK/8yGb//AaDb/vWc0/71nNf+9ZzT/vWc0/71nNP+9ZjT/vWY0/71mNP+9ZjT/vGY0/7xmNP+8ZjT/vGY0/7xmNP+8ZjT/u2U0/7FiLY0AAAACk5OTFu/v78X5+fn/+fn5//n5+f/5+vr/+fn5//n5+f/5+fn/+fn5//n5+f/5+/3/99iy//KWI//ylSH/8ZUh//GVIf/xlSH/8ZUh//GVIf/xlSH/8ZUh//GVIf/xlSH/8ZUh//GVIf/xlSH/8ZUh//CUIf/vkyD/5Y0fxY1XExbDw8Mz9PT05fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n7/f/32LL/85cj//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/wlCD/7pIg/+6SIP/pjx/lunIZM9XV1VD39/f0+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fv9//fYsv/zlyP/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/75Mg/+uRH//qkB//6pAf/+iPH/TIfBtQ3d3dYfj4+Pn5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+/3/99iy//OXI//zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh/+6TIP/ojx//548f/+ePH//njx//5o4f+c1/HGHh4eFl+Pj4+vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n7/f/32LL/85cj//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/tkiD/5Y0f/+SNH//ljR//5Y0f/+WNH//kjB/6zn8cZeDg4Fr4+Pj3+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fv9//fYsv/zlyP/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/65Eg/+KMHv/iix7/4ose/+KLHv/iix7/4ose/+CLHvfLfRta3NzcQvf39+/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+/3/99iy//OXI//zliH/85Yh//OWIf/zliH/85Yh//OWIf/zliH/85Yh/+qRIP/gih7/34oe/9+KHv/fih7/34oe/9+KHv/fih7/3Yge78V6GkLS0tIj9fX12fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n7/f/32LH/85Yg//OVHv/zlR7/85Ue//OVHv/zlR7/85Ue//OVIf/pjyH/3ogf/92HH//dhx//3Ycf/92HH//dhx//3Ycf/92HH//ahh7ZunMZI56engjy8vKu+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fr7//jr2f/2ypL/9smP//bJkP/2yZD/9smQ//bJkP/2yZD/5rNI/9OeFP/SnhX/0p4V/9KeFf/SnhX/0Z0V/9GdFf/RnRX/0Z0V/8yWFq6KVBcI////AO3t7Wr5+fn++fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn6//n6/P/5+vz/+fr8//n6/P/5+vz/+fr8//n6/P/h013/0rsA/9O8AP/TvAD/07wA/9O8AP/TvAD/07wA/9O8AP/SvAD+yLMAav/mAADr6+sA4eHhJPb29tv5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/+LSW//TuwD/1LwA/9S8AP/UvAD/1LwA/9S8AP/UvAD/1LwA/9K6ANu/qgAkyLEAALu7uwAAAAAA8vLygfn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/4tJb/9O7AP/UvAD/1LwA/9S8AP/UvAD/1LwA/9S8AP/UvAD/zrYAgQAAAACfjQAAAAAAAOzs7ADk5OQe9vb2zPn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/i0lv/07sA/9S8AP/UvAD/1LwA/9S8AP/UvAD/1LwA/9K6AMzCrAAeybIAAAAAAAAAAAAAsLCwAP///wDv7+9O+Pj47Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/+LSW//TuwD/1LwA/9S8AP/UvAD/1LwA/9S8AP/TuwDsy7QATu7UAACXhQAAAAAAAAAAAAAAAAAA1tbWALS0tAPy8vJv+Pj49Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/4tJb/9O7AP/UvAD/1LwA/9S8AP/UvAD/07wA9M63AG6ZiQADtqIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4uLiANfX1wbz8/Nz+Pj48Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/i0lv/07sA/9S8AP/UvAD/1LwA/9O8APDPuABzuKMABsGrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4+PjANjY2ATy8vJZ+Pj42vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/+HSW//TugD/1LsA/9S8AP/TuwDazrcAWbejAATBqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1NTUAB8fHwDw8PAr9vb2nPj4+O35+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/7uas/+bZdv/j1mvt2cYznMu0ACsUFAAAtaEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOvr6wDj4+MG8vLyOvb29pD4+PjS+fn58vn5+f35+fn/+fn5//n5+f/5+fn/+fn5/fn5+fL4+frS9/j8kPT1/Trs8v8G8PP/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADh4eEA1tbWAu/v7xv09PRJ9vb2dvb29pf39/eo9/f3qPb29pf29vZ29PT0Se/v7xvW1tYC4eHhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gB///gAH//gAAf/wAAD/4AAAf8AAAD+AAAAfAAAADwAAAA4AAAAGAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABwAAAA8AAAAPgAAAH4AAAB/AAAA/4AAAf/gAAf/8AAP//wAP/', // @suppress longLineCheck
+
+    green: 'data:image/vndmicrosofticon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWJLAEpCMwptYks8eWxTdn1wVpd9cFaXeWxTdm1iSzxKQzMKbWJLAAAAAAAAAAAAAAAAAAAAAAA+OCsAAAAAAXBlTTSBdFmliHpe6Yp8X/2LfWD/i31g/4p8X/2HeV3pf3NYpW5jTDQAAAABPTcqAAAAAAB7e3sAlv//AIB1Xk+HeV3di31g/4t9YP+LfWD/i31g/4t9YP+Je1//h3pd/4d5Xf+DdVrddGhQTwAAAABDPC4A+fn5ANrb3DmupZPfinxf/4t9YP+LfWD/i31g/4t9YP+Iel7/hHdb/4R2W/+Edlv/hHdb/4BzWN9wZU05g3ZaALS0tA3w8PGuu7Sj/4h5W/+Je17/iXte/4t8X/+HeFz/gnNY/4FyWP+Bclj/gXJY/4FyWP+Bclj/fG1Url9NPA3h4eFK9/j48MvFuf+kmoP/ppuF/6abhf+JkHL/c4Rj/3OEY/9zhGP/coNj/3KDY/9yg2P/coNj/3CDYvBgf19K7e3tjPn5+f/39vb/9fTz//X08//09PP/itKw/0m+h/9Mv4n/TL+J/0y/if9Mv4n/TL+J/0y+iP9Lu4b/RrJ/jPHx8bL5+fn/+fn5//n5+f/5+fn/+fn5/4rXtP9Hwon/SsOL/0rDi/9Kw4v/SsOL/0nCiv9HvYb/RruF/0S1gbLz8/O2+fn5//n5+f/5+fn/+fn5//n5+f+K17P/R8KJ/0rDi/9Kw4v/SsOL/0nBif9GuYT/RbaC/0W2gv9Dsn+28vLymfn5+f/5+fn/+fn5//n5+f/5+fn/jdi1/0vDjP9OxI7/TsSO/0rAiv9FtoP/RLKA/0SygP9EsoD/Qq59mfHx8Vz4+Pj3+fn5//n5+f/5+fn/+fn5/9rw5v/H6tn/yOra/8Lp2f9e1b7/O8yz/z3MtP89zLT/Pcuy9zzApVzr6+sY9/f3xvn5+f/5+fn/+fn5//n5+f/7+vr//Pr7//z6+//z+fn/ZuPY/zbczv853c7/Od3O/zjbzcY10sYY////APT09Fb4+Pjy+fn5//n5+f/5+fn/+fn5//n5+f/6+fn/8Pj3/2Xj1/823Mz/OdzN/znczfI42MlWO+XWAOzs7ADm5uYF9vb2ePn5+fT5+fn/+fn5//n5+f/5+fn/+vn5//D49/9j4tf/NdvM/znczfQ42ct4Ncu9BTbRwgAAAAAA8PDwAOzs7Ab29vZd+Pj40vn5+fz5+fn/+fn5//n5+f/z+Pj/jung/FLf0tI42ctdNdHCBjfUxgAAAAAAAAAAAAAAAADn5+cAqKioAPT09CH39/dy+Pj4tvj4+NX4+PjV+Pj4tu329XLO7+whAFQmAGrUygAAAAAAAAAAAPAPAADAAwAAwAMAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABAACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCQgGDCsmHRxCOy4tS0M0N0tDNDdCOy4tKyYdHAkIBgwAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wAVEg4KTUU1MWtgSmx5bVOigHNYx4N2W9uFd1zjhXdc44N2W9uAc1jHeW1TomtgSmxNRjUxFRMOCv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACsnHgALCggHWE88PHdrUpqEd1vhiXxf/It9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/iXxf/IR3W+F3a1KaV048PAsKCAcrJx4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///AAPjcqGXJnT4CFeFzki31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+KfWD/iXxf/4l7Xv+DdlrkcGVNgDw2Khn//+sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AFJKOSp9cFavinxf+4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/inxf/4h6Xv+Iel3/iHpd/4h6Xv+GeV37eW1Ur1BINyr///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAP//3gBsZ1osgnVbv4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4l8X/+HeV3/hnlc/4Z5XP+GeVz/hnlc/4Z5XP+GeFz/fG9Vv1RLOiz/9LoAAgIBAAAAAAAAAAAAAAAAAAAAAAD19fUAl5ibHcbCurWShGn/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+Je1//hXhc/4R3W/+Fd1v/hXdb/4V3W/+Fd1v/hXdb/4V3W/+Ed1v/eW1TtUlCMh2CdVkAAAAAAAAAAAAAAAAAeXl5ADY2Ngne3t+O4t/Z/ZKFaf+LfV//i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/iXte/4R3W/+Ddlr/g3Za/4N2Wv+Ddlr/g3Za/4N2Wv+Ddlr/g3Za/4N2Wv+CdVr9c2dPjhwZEwk/OSsAAAAAAAAAAAD///8AxMTES/X19u3k4dv/koRp/4t9X/+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4h6Xv+CdVr/gXRZ/4F0Wf+BdFn/gXRZ/4F0Wf+BdFn/gXRZ/4F0Wf+BdFn/gXRZ/4F0Wf9+clftZVtGS/3jrgAAAAAAm5ubAHBwcA/o6Oix+/v7/+Pg2/+ShGn/i31f/4t9YP+LfWD/i31g/4t9YP+LfWD/i31g/4t9YP+Iel7/gXRZ/4BzWP+Ac1j/gHNY/4BzWP+Ac1j/gHNY/4BzWP+Ac1j/gHNY/4BzWP+Ac1j/gHNY/4BzWP93a1KxOTMnD1BHNwD///8AycnJRfX19fD7+/v/4+Da/5CCZ/+Jel3/iXtd/4l7Xf+Je13/iXtd/4l7Xf+Ke17/iHhd/4BxV/9/cFb/f3BW/39wVv9/cFb/f3BW/39wVv9/cFb/f3BW/39wVv9/cFb/f3BW/39wVv9/cFb/f3BW/31uVPBnWURFo45tAAAAAALk5OSN+fn5//r6+v/t7Oj/vLSk/7aunP+3rp3/t66d/7eunf+3rp3/uK+e/6Gmjv9vkG3/bI5r/2yOa/9sjmv/bI5r/2yOa/9sjmv/bI5r/2yOa/9sjmr/bI1q/2yNav9sjWr/bI1q/2uNav9rjWr/a41q/16GZI0AAAACk5OTFu/v78X5+fn/+fn5//n5+f/5+fr/+fn5//n5+f/5+fn/+fn5//n5+f/8+vv/wOfV/0vCi/9Kwor/SsKK/0rCiv9Kwor/SsKK/0rCiv9Kwor/SsKK/0rCiv9Kwor/SsKK/0rCiv9Kwor/SsKK/0nAif9Jv4j/RreCxStxUBbDw8Mz9PT05fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//z6+/+/59X/TMSM/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9JwYn/SL6I/0i+iP9GuoXlOJVqM9XV1VD39/f0+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn//Pr7/7/n1f9Mw4z/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/ScCJ/0e8hv9HvIb/R7yG/0a6hfQ9oXJQ3d3dYfj4+Pn5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/8+vv/v+fV/0zDjP9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0i/iP9GuoX/RrqE/0a6hP9GuoT/RrmD+T6ldWHh4eFl+Pj4+vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//z6+/+/59X/TMOM/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Ivof/RbiD/0W3gv9FuIP/RbiD/0W4g/9Ft4L6PqZ2ZeDg4Fr4+Pj3+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn//Pr7/7/n1f9Mw4z/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SL2H/0W2gv9FtYH/RbWB/0W1gf9FtYH/RbWB/0S0gPc+o3Ra3NzcQvf39+/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/8+vv/v+fV/0zDjP9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0rDi/9Kw4v/SsOL/0e8hv9EtID/RLOA/0SzgP9Es4D/RLOA/0SzgP9Es4D/Q7F/7zyecULS0tIj9fX12fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//z6+/+/59X/SsOL/0jCiv9Iwor/SMKK/0jCiv9Iwor/SMKK/0rCiv9HuoT/RLF+/0Owff9EsH3/RLB9/0Swff9EsH3/RLB9/0Swff9CrnzZOJZrI56engjy8vKu+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+vn6/9/x6f+l38X/o9/D/6Tfw/+k38P/pN/D/6Tfw/+k38T/a9Kz/0DBof9BwKH/QcCh/0HAof9BwKD/QcCg/0G/oP9Bv6D/Qb+g/0C4mK4tbU4I////AO3t7Wr5+fn++fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+vn6//v6+//7+vv/+/r7//v6+//7+vv//Pr7//v6+/+B597/NdvN/znczf853M3/OdzN/znczf853M3/OdzN/znczf85283+NtHDakb/+gDr6+sA4eHhJPb29tv5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/3/n3f823Mz/OdzN/znczf853M3/OdzN/znczf853M3/OdzN/zjay9s0x7kkNs/BALu7uwAAAAAA8vLygfn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/f+fd/zbbzP853M3/OdzN/znczf853M3/OdzN/znczf853M3/N9XHgQAAAAAspZoAAAAAAOzs7ADk5OQe9vb2zPn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f9/593/NtvM/znczf853M3/OdzN/znczf853M3/OdzN/zjay8w0yrweNtDCAAAAAAAAAAAAsLCwAP///wDv7+9O+Pj47Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/3/n3f8228z/OdzN/znczf853M3/OdzN/znczf8528zsN9PETkD45gAonJEAAAAAAAAAAAAAAAAA1tbWALS0tAPy8vJv+Pj49Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/f+fd/zbbzP853M3/OdzN/znczf853M3/OdvM9DjWx24qoJUDMb2wAAAAAAAAAAAAAAAAAAAAAAAAAAAA4uLiANfX1wbz8/Nz+Pj48Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f9/593/NtvM/znczf853M3/OdzN/znbzPA418hzMr6xBjTIugAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4+PjANjY2ATy8vJZ+Pj42vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/37m3f8z28z/N9zN/znczf8528zaONbIWTK/sgQ0yLsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1NTUAB8fHwDw8PAr9vb2nPj4+O35+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/vfDr/5Tq4v+L6ODtYODUnDTTxSsAGBsAMrywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOvr6wDj4+MG8vLyOvb29pD4+PjS+fn58vn5+f35+fn/+fn5//n5+f/5+fn/+fn5/fn5+fL6+PjS+vf3kPv09Tr/6u4G/+/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADh4eEA1tbWAu/v7xv09PRJ9vb2dvb29pf39/eo9/f3qPb29pf29vZ29PT0Se/v7xvW1tYC4eHhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gB///gAH//gAAf/wAAD/4AAAf8AAAD+AAAAfAAAADwAAAA4AAAAGAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABwAAAA8AAAAPgAAAH4AAAB/AAAA/4AAAf/gAAf/8AAP//wAP/', // @suppress longLineCheck
+
+    red: 'data:image/vndmicrosofticon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAEAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQxmbAC0RagpDGZs8ShysdkwdspdMHbKXShysdkMZmzwuEWoKQxmcAAAAAAAAAAAAAAAAAAAAAAAmDlgAAAAAAUQanzRPHrilUx/B6VQgxf1VIMb/VSDG/1Qgxf1TH8DpTh22pUMZnDQAAAABJQ5XAAAAAAB7ensA//8AAFUrr09SH8DdVSDG/1Ugxv9VIMb/VSDG/1Ugxv9UH8P/Ux/B/1IfwP9QHrrdRxqlTwAAAAAoD14A+fn5ANzf1zmMatPfVB7G/1Ugxv9VIMb/VSDG/1Ugxv9TH8L/UR68/1AevP9QHrz/UR68/04dt99EGaA5UB67ALS0sw3x8u+unYDd/1AZxP9THcX/Ux3F/1Qexf9THr//Tx23/08ctv9PHbb/Tx22/08dtv9PHbb/SxuurjkSfg3h4eFK+Pj38LWf5P97UtL/fVXS/31V0/9fOcz/SSfC/0knwP9JJ8D/SSfA/0knwP9JJ8D/SSfA/0gnv/A/KLNK7e3tjPn5+f/29fj/8vD3//Px9//y8Pf/fILz/zQ/8P83QvD/N0Lw/zdC8P83QvD/N0Lw/zdB7/82QOz/Mz3gjPHx8bL5+fn/+fn5//n6+f/5+vn/+fn5/36G9v8yQPT/NkP0/zZD9P82Q/T/NkP0/zVC8v80QOz/M0Dq/zI+47Lz8/O2+fn5//n5+f/5+fn/+fn5//n5+f99hvb/MkD0/zZD9P82Q/T/NkP0/zVC8f8zP+f/Mj7k/zI+5P8xPd628vLymfn5+f/5+fn/+fn5//n5+f/5+fn/gYn2/zdE9P87R/T/O0f0/zZF8P8yQOP/MT/e/zE/3v8xP97/Lz3ZmfHx8Vz4+Pj3+fn5//n5+f/5+fn/+fn5/9fZ+P/Bxfj/wsb4/7vD+P87j/X/Dnzx/xF98f8RffH/EXzw9xZv5Vzr6+sY9/f3xvn5+f/5+fn/+fn5//n5+f/7+/n//Pz5//38+f/x+Pn/OrD+/wCY//8Amf//AJn//wCZ/cYAlPMY////APT09Fb4+Pjy+fn5//n5+f/5+fn/+fn5//n5+f/6+fn/7vX5/zmu/v8Al///AJj//wCY/vIAlfpWAJ//AOzs7ADm5uYF9vb2ePn5+fT5+fn/+fn5//n5+f/5+fn/+vn5/+71+f85rf7/AJb//wCY//QAlvx4AIzrBQCQ8gAAAAAA8PDwAOzs7Ab29vZd+Pj40vn5+fz5+fn/+fn5//n5+f/x9vn/bsP8/CGk/tIAlvxdAJDyBgCT9QAAAAAAAAAAAAAAAADn5+cAqKioAPT09CH39/dy+Pj4tvj4+NX4+PjV+Pj4tuvy93LD4fUhAAC7AESo6wAAAAAAAAAAAPAPAADAAwAAwAMAAIABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIABAACAAQAAwAMAAPAPAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBgIMDBoKPRwoD14tLhFrNy4RazcoD14tGgo9HAYCDAwAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+3/wANBR0KLxJuMUEYmGxKHKyiTh22x1Aeu9tRHr3jUR6941Aeu9tOHbbHShysokEYmGwvEm4xDQUeCv+6/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABoKPgAHAxAHNhR9PEkbqppRHr3hVCDE/FUgxv9VIMf/VSDH/1Ugxv9VIMb/VSDH/1Ugx/9VIMb/VCDE/FEevOFIG6maNRR8PAcDEAcaCj0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADVUP8AJg5YGUYao4BRH77kVSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMX/VB/E/1Qfw/9QHrvkRRmggCUOVhnQTv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEA////ADITdSpMHbKvVCDE+1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VCDE/1Mfwv9TH8H/Ux/B/1Mfwv9SH7/7ShytrzEScSr///8AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAMto/wBVPoYsUSC3v1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1QfxP9SHsD/Uh6//1Iev/9SHr//Uh6//1Iev/9SHr//SxywvzMTdyymPf8AAQACAAAAAAAAAAAAAAAAAAAAAAD19fUAnaKQHbep1rVfLcn/VB/G/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9UH8P/UR6+/1Eevf9RHr3/UR69/1Eevf9RHr3/UR69/1Eevf9RHr3/ShuttS0RaB1PHrkAAAAAAAAAAAAAAAAAeXl5ADY2Ngnf4NyO18zu/V8tyf9UH8b/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VB/D/1EevP9QHrr/UB67/1Aeu/9QHrv/UB67/1Aeu/9QHrv/UB67/1Aeu/9QHrr9RhqkjhEGKAknDloAAAAAAAAAAAD///8AxMTES/b39O3Zzu//Xy3J/1Qfxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Mfwv9QHbr/Tx24/08duP9PHbj/Tx24/08duP9PHbj/Tx24/08duP9PHbj/Tx24/08duf9NHLTtPheRS5s5/wAAAAAAm5ubAHBwcA/o6Oix+/z6/9jO7/9fLcn/VB/G/1Ugxv9VIMb/VSDG/1Ugxv9VIMb/VSDG/1Ugxv9TH8H/Tx24/04dtv9OHbb/Th22/04dtv9OHbb/Th22/04dtv9OHbb/Th22/04dtv9OHbb/Th22/04dtv9JG6mxIw1RDzAScQD///8AycnJRfX19fD7/Pr/2M3v/1wqyP9SHMX/UhzF/1Icxf9SHMX/UhzF/1Icxf9THcX/Ux7A/04ctf9NHLL/Thyz/04cs/9NHLP/TRyz/00cs/9OHLP/Thyz/04cs/9OHLP/Thyz/04cs/9NHLP/Thyz/0wcsPA/Fo9FYyTkAAAAAALk5OSN+fn5//r6+f/n4vT/noDd/5Z22v+Wdtr/lnba/5Z22v+Wdtr/mHfb/35g1/9KMMr/SC/H/0gvx/9IL8f/SC/H/0gvx/9IL8b/SC/G/0gvxv9HL8b/Ry/G/0cvxv9HL8b/Ry/G/0cvxv9HL8X/Ry7F/z8tuI0AAAACk5OTFu/v78X5+fn/+fn5//n5+f/6+vn/+fr5//n6+f/5+vn/+fr5//n6+f/9/fn/ub73/zhF8v82Q/L/NkPy/zZD8v82Q/L/NkPy/zZD8v82Q/L/NkPy/zZD8v82Q/L/NkPy/zZD8v82Q/L/NkPy/zVC8f81QvD/Mz/mxR8njhbDw8Mz9PT05fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//z8+f+5vff/OEX0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P81QvH/NEHv/zRB7/8zQOrlKTO6M9XV1VD39/f0+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn//Pz5/7m99/84RfT/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NULw/zRA7P80QOv/NEDr/zNA6fQsN8lQ3d3dYfj4+Pn5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/8/Pn/ub33/zhF9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zVB7/8zQOn/Mz/o/zM/6P8zQOj/Mz/n+S04zmHh4eFl+Pj4+vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//z8+f+5vff/OEX0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P80Qe7/Mz/m/zM/5f8zP+b/Mz/m/zM/5v8yP+X6LjnPZeDg4Fr4+Pj3+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn//Pz5/7m99/84RfT/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NEHs/zI+4/8yPuP/Mj7j/zI+4/8yPuP/Mj7j/zI+4fctOMxa3NzcQvf39+/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/8/Pn/ub33/zhF9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zZD9P82Q/T/NkP0/zRA6/8xPeH/MT3g/zE94P8xPeD/MT3g/zE94P8xPeD/MT3e7ys2xkLS0tIj9fX12fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//z8+f+4vff/NkP0/zNB9P80QfT/NEH0/zRB9P80QfT/NEH0/zZC8/81P+n/Mjze/zI73f8yO93/Mjvd/zI73f8yO93/Mjvd/zI73f8xO9rZKTO7I56engjy8vKu+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+/r5/9ze+P+covf/mqD3/5qg9/+aoPf/mqD3/5qg9/+aoPf/UoLz/x1p5/8eaeb/Hmnm/x5p5v8eaeX/Hmnl/x5p5f8eaOX/Hmjl/yBh3a4jJokI////AO3t7Wr5+fn++fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+vr5//z8+f/8/Pn//Pz5//z8+f/8/Pn//Pz5//z8+f9dvfz/AJf+/wCZ/v8Amf7/AJn+/wCZ/v8Amf7/AJn+/wCZ/v8AmP7+AJLxagC4/wDr6+sA4eHhJPb29tv5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/1u8/f8Alv//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCW/NsAieckAI/xALu7uwAAAAAA8vLygfn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/W7z9/wCW//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJP3gQAAAAAAcr8AAAAAAOzs7ADk5OQe9vb2zPn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f9bvP3/AJb//wCY//8AmP//AJj//wCY//8AmP//AJj//wCW/MwAi+oeAJDxAAAAAAAAAAAAsLCwAP///wDv7+9O+Pj47Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/1u8/f8Alv//AJj//wCY//8AmP//AJj//wCY//8Al/7sAJL0TgCr/wAAa7QAAAAAAAAAAAAAAAAA1tbWALS0tAPy8vJv+Pj49Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/W7z9/wCW//8AmP//AJj//wCY//8AmP//AJj+9ACU+G4AbrgDAIPaAAAAAAAAAAAAAAAAAAAAAAAAAAAA4uLiANfX1wbz8/Nz+Pj48Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f9bvP3/AJb//wCY//8AmP//AJj//wCY/vAAlflzAITcBgCK5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4+PjANjY2ATy8vJZ+Pj42vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/1u7/f8Alf//AJf//wCY//8Al/7aAJT4WQCE3AQAiucAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1NTUAB8fHwDw8PAr9vb2nPj4+O35+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/rNv7/3bG/P9rwfztM6r7nACR9SsAER0AAIPZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOvr6wDj4+MG8vLyOvb29pD4+PjS+fn58vn5+f35+fn/+fn5//n5+f/5+fn/+fn5/fn5+fL6+fjS/Pj2kP338jr/+eIG//fqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADh4eEA1tbWAu/v7xv09PRJ9vb2dvb29pf39/eo9/f3qPb29pf29vZ29PT0Se/v7xvW1tYC4eHhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gB///gAH//gAAf/wAAD/4AAAf8AAAD+AAAAfAAAADwAAAA4AAAAGAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABwAAAA8AAAAPgAAAH4AAAB/AAAA/4AAAf/gAAf/8AAP//wAP/', // @suppress longLineCheck
+
+    yellow: 'data:image/vndmicrosofticon;base64,AAABAAIAICAAAAEAIACoEAAAJgAAABAQAAABACAAaAQAAM4QAAAoAAAAIAAAAEAAAAABACAAAAAAAAAQAAASCwAAEgsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAZKhQAOWAiAEV0KgBFdCoAOWAiABkqFAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ZAAAChAHAEp8JwBvu10AgNeSAInluACN7c4Aj/DXAI/w1wCN7c4AieW4AIDXkgBvu10ASnwnAAoQBwA8ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbLgAAAAAFAFmWMwB/1YwAj/DXAJX7+QCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJX7+QCP79cAftWMAFmVMwAAAAUAGy4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7v8AAD1mFQB6zXYAkPLdAJf+/gCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP7/AJf+/wCV/P4AjvDdAHjKdgA8ZBUA6f8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AABWkCYAh+KoAJb8+QCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJf+/wCV+v8AlPr/AJT6/wCV+v8Akvf5AIPdqABTjCYA//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICABb//wAka5wqAozquwCY/v8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCX/f8Ak/j/AJP3/wCT9/8Ak/f/AJP3/wCT9/8Akvb/AIbiuwBZlyoA//8AAAECAAAAAAAAAAAAAAAAAAAAAADz8/MAqJaJHZDD5rQLnP7/AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8Alvz/AJL2/wCR9P8AkfT/AJH0/wCR9P8AkfT/AJH0/wCR9P8AkfT/AITftABQhh0AjO0AAAAAAAAAAAAAAAAAfX19ADw8PAni3tuPuuD5/Quc//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJb8/wCQ8/8Aj/H/AI/x/wCP8f8Aj/H/AI/x/wCP8f8Aj/H/AI/x/wCP8f8AjvD9AH7UjwAiOQkASHkAAAAAAAgICAD///8AxcXFT/j19O+94vv/Cpz//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCV+/8Aj/H/AI3u/wCN7v8Aje7/AI3u/wCN7v8Aje7/AI3u/wCN7v8Aje7/AI3u/wCO7v8AiunvAHC8TwD//wAABQgAqKioAHp6ehHp6em3/fv5/7zh+v8KnP//AJj//wCY//8AmP//AJj//wCY//8AmP//AJj//wCY//8Alfr/AI7u/wCM6/8AjOv/AIzr/wCM6/8AjOv/AIzr/wCM6/8AjOv/AIzr/wCM6/8AjOv/AIzr/wCM6/8Ag9y3AERyEQBenQD///8AzMzMTfb29vP9+/n/vOH6/wqb//8Alv//AJb//wCW//8Alv//AJb//wCW//8Al///AJT5/wCL6/8Aiej/AIno/wCJ6P8Aiej/AIno/wCJ6P8Aiej/AIno/wCJ6P8Aiej/AIno/wCJ6P8Aiej/AIno/wCH5fMAb75NAMP/AAAAAAXl5eWX+fn5//v6+f/T6vr/Wbv9/0+3/f9Qt/3/ULf9/1C3/f9Qt/3/Ubj9/zew+/8InO//B5nr/weZ6/8Hmev/B5nq/weZ6v8Hmer/B5nq/weZ6v8Hmer/B5jq/weY6v8HmOn/B5jp/weY6f8HmOn/Bpjp/weP15cBAAAFpKSkHfDw8M/5+fn/+fn5//n5+f/1+Pn/9Pf5//T3+f/09/n/9Pf5//T3+f/4+Pn/o+T6/wq//f8Hv/3/CL/9/wi//f8Iv/3/CL/9/wi//f8Iv/3/CL/8/wi+/P8Ivvz/CL78/wi+/P8Ivvz/CL78/we9+/8HvPr/BrbxzwR9pR3Ly8tA9fX17Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//36+f+l5vv/CcL//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hv/3/Br36/wa9+v8GuvbsBZnLQNra2mD39/f4+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn//fr5/6Xm+/8Jwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B778/wa79/8Guvf/Brr3/wa59fgFo9hg4uLidPj4+P35+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/9+vn/peb7/wnB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//we++/8GufX/Brj0/wa49P8GuPT/Brfz/QWm3XTk5OR6+Pj4/fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//36+f+l5vv/CcH//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hvfr/Brfy/wa28f8GtvH/Brbx/wa28f8GtfD9BafdeuXl5W/4+Pj8+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn//fr5/6Xm+/8Jwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B7z5/wa17/8GtO7/BrTu/wa07v8GtO7/BrTu/waz7fwFpdtv4eHhVvj4+Pb5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/9+vn/peb7/wnB//8Hwf//B8H//wfB//8Hwf//B8H//wfB//8Hwf//B8H//we7+P8Gsu3/BrHr/wax6/8Gsev/BrHr/wax6/8Gsev/BrDq9gWh1Vba2toz9vb25vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//36+f+k5fv/BsH//wPA//8DwP//A8D//wPA//8DwP//A8D//wXA//8Guvb/BrDq/wau6P8Gruj/Bq7o/wau6P8Gruj/Bq7o/wau6P8GreXmBZnLM7+/vxH09PTC+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+/r5/83v+v9x2vz/btn9/2/Z/f9v2f3/b9n9/2/Z/f9v2f3/RdL5/yXG7v8mxOz/JsTs/ybE6/8mxOv/JsTr/yXE6/8lw+v/JcPr/yK95cIQirAR////APDw8IH5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+vn5//r5+f/6+fn/+vn5//r5+f/6+fn/+vn5//r5+f+H8Pz/Oer+/zzq/v886v7/POr+/zzq/v886v7/POr+/zzq/v886v3/OuDzgWz//wD09PQA5+fnNPf39+n5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/4Xw/f846///O+v//zvr//876///O+v//zvr//876///O+v//zvp/ek32+00Ouf6AMrKygCzs7MF8/Pzmvn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/hfD9/zjr//876///O+v//zvr//876///O+v//zvr//876///OuX5miqptwUwv88AAAAAAPPz8wDp6eku9/f33fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f+F8P3/OOv//zvr//876///O+v//zvr//876///O+v//zvp/d033O8uOuX5AAAAAAAAAAAAvr6+AP///wDx8fFl+Pj49fn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/4Xw/f846///O+v//zvr//876///O+v//zvr//876v71OeP2ZY7//wAus8IAAAAAAAAAAAAAAAAA4ODgANPT0wj09PSI+fn5+vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/hfD9/zjr//876///O+v//zvr//876///O+v/+jrm+Ygyx9gINdPlAAAAAAAAAAAAAAAAAAAAAAAAAAAA6enpAOHh4Q309PSM+fn5+Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f+F8P3/OOv//zvr//876///O+v//zvr//g65/qMNtXnDTjd7wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6enpAOLi4gr09PRw+Pj45/n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5/4Pw/f816///Oev//zvr//876v7nOub5cDbW5wo33O4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ODgANHR0QLx8fE89/f3sfn5+fX5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+f/5+fn/t/T7/4Xx/f+A8P31Xez8sTnk9zwuxdUCNtTkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAREREAP///wDo6OgM9PT0Tff396T4+Pjf+fn5+Pn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//n5+fj5+Pjf9vf3pPL09E3m6OgM7/3/APtbOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACMjIwD19fUA4uLiBvHx8Sn19fVd9vb2jff396739/e99/f3vff396729vaN9fX1XfHx8Snl4uIG9PX1AFEnIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/wD///gAH//gAAf/wAAD/4AAAf8AAAD+AAAAfAAAADwAAAA4AAAAGAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAGAAAABgAAAAcAAAAPgAAAH4AAAB/AAAA/4AAAf/AAAP/8AAP//wAP/KAAAABAAAAAgAAAAAQAgAAAAAAAABAAAEgsAABILAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABorgAAS34IAHTDNQCC22wAh+OMAIfjjACC22wAdMQ1AEx/CABorwAAAAAAAAAAAAAAAAAAAAAAAEBrAAAAAAAAecswAIzsngCU+OUAl/37AJj+/wCY/v8Al/37AJP35QCL6Z4Ad8gwAAAAAAA+aQAAAAAAcXd6AP8AAAAOiNtNAJP32gCY//8AmP//AJj//wCY//8AmP//AJb8/wCU+f8Ak/j/AI7w2gB+1E0AAAAAAEd4APn7/ADc2NU5T7P33gCX//8AmP//AJj//wCY//8AmP//AJX6/wCR8/8AkPL/AJDz/wCQ8/8AjOzeAHrOOQCR9AC3t7cO8e/vsGnA/f8Alf//AJf//wCX//8Al///AJP4/wCN7v8AjOz/AIzs/wCM7P8AjOz/AIzt/wCG4rAAY6oO4uLiT/j39/GIzfz/Mav+/zSs/v80rP7/FaH5/wOV7f8DlOv/A5Tr/wOU6/8DlOv/A5Tr/wOU6/8Dk+jxBIvVT+3t7ZT5+fn/8fb5/+vz+f/r9Pn/6vP5/1nR+/8EvPz/B738/we9/P8Hvfz/B738/we9/P8HvPv/B7r4/wax7ZTy8vK7+fn5//n5+f/6+fn/+vn5//n5+f9e1f3/A8D//wfB//8Hwf//B8H//wfB//8HwP3/Brv3/wa59f8GtO678/Pzwfn5+f/5+fn/+fn5//n5+f/4+fn/XtX9/wPA//8Hwf//B8H//wfB//8Hv/z/Brfz/wa17/8Gte//BrHqwfPz86X5+fn/+fn5//n5+f/5+fn/+Pn5/2DW/f8Gwf//CsL//wrC//8Jv/v/CLXu/wix6f8Isen/CLHp/wet5KXy8vJo+fn5+vn5+f/5+fn/+fn5//n5+f/I7vr/quf7/6zn+/+m5/v/Tdz5/yzV9P8u1fT/LtX0/y7U8/ooyOpo7OzsH/f399D5+fn/+fn5//n5+f/5+fn//Pr5//36+f/++vn/9fn5/2rv/v857P//POz//zzs//886/3QOuLzH////wD09PRh+fn59vn5+f/5+fn/+fn5//n5+f/5+fn/+fn5//H4+f9o7v7/OOv//zvr//876//2Ouf6YUH//wDu7u4A6enpB/b29oT5+fn3+fn5//n5+f/5+fn/+fn5//n5+f/x+Pn/Zu7+/zfr//876//3Ouj8hDfc7wc44PMAAAAAAPHx8QDu7u4I9vb2aPj4+Nn5+fn9+fn5//n5+f/5+fn/8/n5/4zx/P1S7P7ZO+n8aDfh9Ag55PcAAAAAAAAAAAAAAAAA6+vrAN/f3wH19fUo9/f3fvj4+MH4+Pje+Pj43vj4+MHq9vh+w/H2KADM5wFk4e8AAAAAAAAAAADwDwAA4AcAAMADAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAAgAEAAMADAADgBwAA' // @suppress longLineCheck
+  };
+
+  return {
+    FaviconsByHue: FaviconsByHue
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/filter.html b/trace-viewer/trace_viewer/core/filter.html
new file mode 100644
index 0000000..3ec06ee
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/filter.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  /**
+   * @constructor The generic base class for filtering a TraceModel based on
+   * various rules. The base class returns true for everything.
+   */
+  function Filter() { }
+
+  Filter.prototype = {
+    __proto__: Object.prototype,
+
+    matchCounter: function(counter) {
+      return true;
+    },
+
+    matchCpu: function(cpu) {
+      return true;
+    },
+
+    matchProcess: function(process) {
+      return true;
+    },
+
+    matchSlice: function(slice) {
+      return true;
+    },
+
+    matchThread: function(thread) {
+      return true;
+    }
+  };
+
+  /**
+   * @constructor A filter that matches objects by their name or category
+   * case insensitive.
+   * .findAllObjectsMatchingFilter
+   */
+  function TitleOrCategoryFilter(text) {
+    Filter.call(this);
+    this.text_ = text.toLowerCase();
+
+    if (!text.length)
+      throw new Error('Filter text is empty.');
+  }
+  TitleOrCategoryFilter.prototype = {
+    __proto__: Filter.prototype,
+
+    matchSlice: function(slice) {
+      if (slice.title === undefined && slice.category === undefined)
+        return false;
+      return slice.title.toLowerCase().indexOf(this.text_) !== -1 ||
+             slice.category.toLowerCase().indexOf(this.text_) !== -1;
+    }
+  };
+
+  /**
+   * @constructor A filter that matches objects with the exact given title.
+   */
+  function ExactTitleFilter(text) {
+    Filter.call(this);
+    this.text_ = text;
+
+    if (!text.length)
+      throw new Error('Filter text is empty.');
+  }
+  ExactTitleFilter.prototype = {
+    __proto__: Filter.prototype,
+
+    matchSlice: function(slice) {
+      return slice.title === this.text_;
+    }
+  };
+
+  return {
+    Filter: Filter,
+    TitleOrCategoryFilter: TitleOrCategoryFilter,
+    ExactTitleFilter: ExactTitleFilter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/filter_test.html b/trace-viewer/trace_viewer/core/filter_test.html
new file mode 100644
index 0000000..be0e310
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/filter_test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/base/unittest.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var TitleOrCategoryFilter = tv.c.TitleOrCategoryFilter;
+  var ExactTitleFilter = tv.c.ExactTitleFilter;
+
+  test('titleOrCategoryFilter', function() {
+    assert.throw(function() {
+      new TitleOrCategoryFilter();
+    });
+    assert.throw(function() {
+      new TitleOrCategoryFilter('');
+    });
+
+    var s0 = tv.c.test_utils.newSliceCategory('cat', 'a', 1, 3);
+    assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s0));
+    assert.isTrue(new TitleOrCategoryFilter('cat').matchSlice(s0));
+    assert.isTrue(new TitleOrCategoryFilter('at').matchSlice(s0));
+    assert.isFalse(new TitleOrCategoryFilter('b').matchSlice(s0));
+    assert.isFalse(new TitleOrCategoryFilter('X').matchSlice(s0));
+
+    var s1 = tv.c.test_utils.newSliceCategory('cat', 'abc', 1, 3);
+    assert.isTrue(new TitleOrCategoryFilter('abc').matchSlice(s1));
+    assert.isTrue(new TitleOrCategoryFilter('Abc').matchSlice(s1));
+    assert.isTrue(new TitleOrCategoryFilter('cat').matchSlice(s1));
+    assert.isTrue(new TitleOrCategoryFilter('Cat').matchSlice(s1));
+    assert.isFalse(new TitleOrCategoryFilter('cat1').matchSlice(s1));
+    assert.isFalse(new TitleOrCategoryFilter('X').matchSlice(s1));
+  });
+
+  test('exactTitleFilter', function() {
+    assert.throw(function() {
+      new ExactTitleFilter();
+    });
+    assert.throw(function() {
+      new ExactTitleFilter('');
+    });
+
+    var s0 = tv.c.test_utils.newSliceNamed('a', 1, 3);
+    assert.isTrue(new ExactTitleFilter('a').matchSlice(s0));
+    assert.isFalse(new ExactTitleFilter('b').matchSlice(s0));
+    assert.isFalse(new ExactTitleFilter('A').matchSlice(s0));
+
+    var s1 = tv.c.test_utils.newSliceNamed('abc', 1, 3);
+    assert.isTrue(new ExactTitleFilter('abc').matchSlice(s1));
+    assert.isFalse(new ExactTitleFilter('Abc').matchSlice(s1));
+    assert.isFalse(new ExactTitleFilter('bc').matchSlice(s1));
+    assert.isFalse(new ExactTitleFilter('a').matchSlice(s1));
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/find_control.html b/trace-viewer/trace_viewer/core/find_control.html
new file mode 100644
index 0000000..eea7346
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/find_control.html
@@ -0,0 +1,175 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/find_controller.html">
+<link rel="import" href="/core/timeline_track_view.html">
+
+<polymer-element name="tracing-find-control" constructor="TracingFindControl">
+  <template>
+    <style>
+      div.root {
+        -webkit-user-select: none;
+        display: -webkit-flex;
+        position: relative;
+      }
+      input {
+        -webkit-user-select: auto;
+        background-color: #f8f8f8;
+        border: 1px solid rgba(0, 0, 0, 0.5);
+        box-sizing: border-box;
+        height: 19px;
+        margin-bottom: 1px;
+        margin-left: 0;
+        margin-right: 0;
+        margin-top: 1px;
+        padding: 0;
+        width: 170px;
+      }
+      input:focus {
+        background-color: white;
+      }
+      .button {
+        background-color: #f8f8f8;
+        border: 1px solid rgba(0, 0, 0, 0.5);
+        border-left: none;
+        font-size: 14px;
+        height: 17px;
+        margin-left: 0;
+        margin-top: 1px;
+      }
+      .button :first-of-type {
+        margin-right: 0;
+      }
+      #hitCount {
+        height: 19px;
+        left: 0;
+        opacity: 0.25;
+        pointer-events: none;
+        position: absolute;
+        text-align: right;
+        top: 2px;
+        width: 170px;
+        z-index: 1;
+      }
+      #spinner {
+        visibility: hidden;
+        width: 8px;
+        height: 8px;
+        left: 154px;
+        pointer-events: none;
+        position: absolute;
+        top: 4px;
+        z-index: 1;
+
+        border: 2px solid transparent;
+        border-bottom: 2px solid rgba(0, 0, 0, 0.5);
+        border-right: 2px solid rgba(0, 0, 0, 0.5);
+        border-radius: 50%;
+
+        animation: spin 1s linear infinite;
+      }
+      @keyframes spin { 100% { transform: rotate(360deg); } }
+    </style>
+
+    <div class="root">
+      <input type='text' id='filter'
+          on-input="{{ filterTextChanged }}"
+          on-keypress="{{ filterKeyPress }}"
+          on-keydown="{{ filterKeyDown }}"
+          on-blur="{{ filterBlur }}"
+          on-focus="{{ filterFocus }}"
+          on-mouseup="{{ filterMouseUp }}" />
+      <div id="spinner"></div>
+      <div class="button" on-click="{{ findPrevious }}">&larr;</div>
+      <div class="button" on-click="{{ findNext }}">&rarr;</div>
+      <div id="hitCount">0 of 0</div>
+    </div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    filterKeyDown: function(e) {
+      e.stopPropagation();
+      if (e.keyCode !== 13) //
+        return;
+
+      e.shiftKey ? this.findPrevious() : this.findNext();
+    },
+
+    filterKeyPress: function(e) {
+      e.stopPropagation();
+    },
+
+    filterBlur: function(e) {
+      this.updateHitCountEl();
+    },
+
+    filterFocus: function(e) {
+      this.controller.reset();
+      this.$.filter.select();
+    },
+
+    // Prevent that the input text is deselected after focusing the find
+    // control with the mouse.
+    filterMouseUp: function(e) {
+      e.preventDefault();
+    },
+
+    get controller() {
+      return this.controller_;
+    },
+
+    set controller(c) {
+      this.controller_ = c;
+      this.updateHitCountEl();
+    },
+
+    focus: function() {
+      this.$.filter.focus();
+    },
+
+    get hasFocus() {
+      return this === document.activeElement;
+    },
+
+    filterTextChanged: function() {
+      this.controller.filterText = this.$.filter.value;
+      this.$.hitCount.textContent = '';
+      this.$.spinner.style.visibility = 'visible';
+      this.controller.updateFilterHits().then(function() {
+        this.$.spinner.style.visibility = 'hidden';
+        this.updateHitCountEl();
+      }.bind(this));
+    },
+
+    findNext: function() {
+      if (this.controller)
+        this.controller.findNext();
+      this.updateHitCountEl();
+    },
+
+    findPrevious: function() {
+      if (this.controller)
+        this.controller.findPrevious();
+      this.updateHitCountEl();
+    },
+
+    updateHitCountEl: function() {
+      if (!this.controller || !this.hasFocus) {
+        this.$.hitCount.textContent = '';
+        return;
+      }
+
+      var n = this.controller.filterHits.length;
+      var i = n === 0 ? -1 : this.controller.currentHitIndex;
+      this.$.hitCount.textContent = (i + 1) + ' of ' + n;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/find_control_test.html b/trace-viewer/trace_viewer/core/find_control_test.html
new file mode 100644
index 0000000..2e4fd5c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/find_control_test.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/find_control.html">
+<link rel="import" href="/core/test_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var ctl = new TracingFindControl();
+    ctl.controller = {
+      findNext: function() { },
+      findPrevious: function() { },
+      reset: function() {},
+
+      filterHits: ['a', 'b'],
+
+      currentHitIndex: 0
+    };
+
+    this.addHTMLOutput(ctl);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/find_controller.html b/trace-viewer/trace_viewer/core/find_controller.html
new file mode 100644
index 0000000..42b4ef3
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/find_controller.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/ui_state.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview FindController.
+ */
+tv.exportTo('tv.c', function() {
+  var Task = tv.b.Task;
+
+  function FindController() {
+    this.timeline_ = undefined;
+    this.filterText_ = '';
+    this.filterHits_ = new tv.c.Selection();
+    this.filterHitsDirty_ = true;
+    this.currentHitIndex_ = -1;
+  };
+
+  FindController.prototype = {
+    __proto__: Object.prototype,
+
+    get model() {
+      if (!this.timeline_)
+        return;
+      return this.timeline_.model;
+    },
+
+    get timeline() {
+      return this.timeline_;
+    },
+
+    set timeline(t) {
+      this.timeline_ = t;
+      this.filterHitsDirty_ = true;
+    },
+
+    get filterText() {
+      return this.filterText_;
+    },
+
+    set filterText(f) {
+      if (f == this.filterText_)
+        return;
+      this.filterText_ = f;
+      this.filterHitsDirty_ = true;
+    },
+
+    getFilterPromise_: function(filterText) {
+      if (!this.timeline_)
+        return;
+      var promise = Promise.resolve();
+
+      var filter = new tv.c.TitleOrCategoryFilter(filterText);
+      var filterHits = new tv.c.Selection();
+      var filterTask =
+          this.timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+              filter, filterHits);
+      promise = Task.RunWhenIdle(filterTask);
+      promise.then(function() {
+        this.filterHitsDirty_ = false;
+        this.filterHits_ = filterHits;
+        this.timeline.setHighlightAndClearSelection(filterHits);
+      }.bind(this));
+      return promise;
+    },
+
+    clearFindSelections_: function() {
+      this.timeline.setHighlightAndClearSelection(new tv.c.Selection());
+      this.timeline.removeXNavStringMarker();
+    },
+
+    /**
+     * Updates the filter hits based on the current filtering settings. Returns
+     * a promise which resolves when |filterHits| has been refreshed.
+     */
+    updateFilterHits: function() {
+      var promise = Promise.resolve();
+
+      if (!this.filterHitsDirty_)
+        return promise;
+
+      this.filterHits_ = new tv.c.Selection();
+      this.currentHitIndex_ = -1;
+
+      // Try constructing a UIState from the filterText.
+      // UIState.fromUserFriendlyString will throw an error only if the string
+      // is syntactically correct to a UI state string but with invalid values.
+      // It will return undefined if there is no syntactic match.
+      var stateFromString;
+      try {
+        stateFromString = tv.c.UIState.fromUserFriendlyString(
+          this.model, this.timeline.viewport, this.filterText);
+      } catch (e) {
+        var overlay = new tv.b.ui.Overlay();
+        overlay.textContent = e.message;
+        overlay.title = 'UI State Navigation Error';
+        overlay.visible = true;
+        return promise;
+      }
+
+      if (stateFromString !== undefined) {
+        this.timeline.navToPosition(stateFromString);
+      } else {
+        // filterText is not a navString here -- proceed with find and filter.
+        if (this.filterText.length === 0)
+          this.clearFindSelections_();
+        else
+          promise = this.getFilterPromise_(this.filterText);
+      }
+      return promise;
+    },
+
+    /**
+     * Returns the most recent filter hits as a tv.c.Selection. Call
+     * |updateFilterHits| to ensure this is up to date after the filter
+     * settings have been changed.
+     */
+    get filterHits() {
+      return this.filterHits_;
+    },
+
+    get currentHitIndex() {
+      return this.currentHitIndex_;
+    },
+
+    find_: function(dir) {
+      var firstHit = this.currentHitIndex_ === -1;
+      if (firstHit && dir < 0)
+        this.currentHitIndex_ = 0;
+
+      var N = this.filterHits.length;
+      this.currentHitIndex_ = (this.currentHitIndex_ + dir + N) % N;
+
+      if (!this.timeline)
+        return;
+
+      this.timeline.selection =
+          this.filterHits.subSelection(this.currentHitIndex_, 1);
+    },
+
+    findNext: function() {
+      this.find_(1);
+    },
+
+    findPrevious: function() {
+      this.find_(-1);
+    },
+
+    reset: function() {
+      this.filterText_ = '';
+      this.filterHitsDirty_ = true;
+    }
+  };
+
+  return {
+    FindController: FindController
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/find_controller_test.html b/trace-viewer/trace_viewer/core/find_controller_test.html
new file mode 100644
index 0000000..5e48605
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/find_controller_test.html
@@ -0,0 +1,302 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/core/find_controller.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Task = tv.b.Task;
+
+  /*
+   * Just enough of the Timeline to support the tests below.
+   */
+  var FakeTimeline = tv.b.ui.define('div');
+
+  FakeTimeline.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function() {
+      this.addAllObjectsMatchingFilterToSelectionReturnValue = [];
+
+      this.selection = new tv.c.Selection();
+      this.highlight = new tv.c.Selection();
+      this.keyHelp = '<keyHelp>';
+
+      // Put some simple UI in for testing purposes.
+      var noteEl = document.createElement('div');
+      noteEl.textContent = 'FakeTimeline:';
+      this.appendChild(noteEl);
+
+      this.statusEl_ = document.createElement('div');
+      this.appendChild(this.statusEl_);
+      this.refresh_();
+    },
+
+    refresh_: function() {
+      var status;
+      if (this.model)
+        status = 'model=set';
+      else
+        status = 'model=undefined';
+      this.statusEl_.textContent = status;
+    },
+
+    addAllObjectsMatchingFilterToSelectionAsTask: function(filter, selection) {
+      return new Task(function() {
+        var n = this.addAllObjectsMatchingFilterToSelectionReturnValue.length;
+        for (var i = 0; i < n; i++) {
+          selection.push(
+              this.addAllObjectsMatchingFilterToSelectionReturnValue[i]);
+        }
+      }, this);
+    },
+
+    setHighlightAndClearSelection: function(highlight) {
+      this.highlight = highlight;
+    }
+  };
+
+  function assertArrayShallowEquals(a, b, opt_message) {
+    if (a.length === b.length) {
+      var ok = true;
+      for (var i = 0; i < a.length; i++) {
+        ok &= (a[i] === b[i]);
+      }
+      if (ok)
+        return;
+    }
+
+    var message = opt_message || 'Expected array ' + a + ', got array ' + b;
+    throw new tv.b.unittest.TestError(message);
+  };
+
+  test('findControllerNoTimeline', function() {
+    var controller = new tv.c.FindController();
+    controller.findNext();
+    controller.findPrevious();
+  });
+
+  test('findControllerEmptyHit', function() {
+    var timeline = new FakeTimeline();
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+
+    timeline.selection = new tv.c.Selection();
+    timeline.highlight = new tv.c.Selection();
+    controller.findNext();
+    assertArrayShallowEquals([], timeline.selection);
+    assertArrayShallowEquals([], timeline.highlight);
+    controller.findPrevious();
+    assertArrayShallowEquals([], timeline.selection);
+    assertArrayShallowEquals([], timeline.highlight);
+  });
+
+  test('findControllerOneHit', function() {
+    var timeline = new FakeTimeline();
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+
+    var s1 = {guid: 1};
+    timeline.addAllObjectsMatchingFilterToSelectionReturnValue = [s1];
+    controller.filterText = 'asdf';
+    var promise = controller.updateFilterHits();
+    promise.then(function() {
+      assertArrayShallowEquals([], timeline.selection);
+      assertArrayShallowEquals([s1], timeline.highlight);
+      controller.findNext();
+      assertArrayShallowEquals([s1], timeline.selection);
+      assertArrayShallowEquals([s1], timeline.highlight);
+      controller.findNext();
+      assertArrayShallowEquals([s1], timeline.selection);
+      assertArrayShallowEquals([s1], timeline.highlight);
+      controller.findPrevious();
+      assertArrayShallowEquals([s1], timeline.selection);
+      assertArrayShallowEquals([s1], timeline.highlight);
+    });
+    return promise;
+  });
+
+  test('findControllerMultipleHits', function() {
+    var timeline = new FakeTimeline();
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+
+    var s1 = {guid: 1};
+    var s2 = {guid: 2};
+    var s3 = {guid: 3};
+
+    timeline.addAllObjectsMatchingFilterToSelectionReturnValue = [s1, s2, s3];
+    controller.filterText = 'asdf';
+    var promise = controller.updateFilterHits();
+    promise.then(function() {
+      // Loop through hits then when we wrap, try moving backward.
+      assertArrayShallowEquals([], timeline.selection);
+      assertArrayShallowEquals([s1, s2, s3], timeline.highlight);
+      controller.findNext();
+      assertArrayShallowEquals([s1], timeline.selection);
+      controller.findNext();
+      assertArrayShallowEquals([s2], timeline.selection);
+      controller.findNext();
+      assertArrayShallowEquals([s3], timeline.selection);
+      controller.findNext();
+      assertArrayShallowEquals([s1], timeline.selection);
+      controller.findPrevious();
+      assertArrayShallowEquals([s3], timeline.selection);
+      controller.findPrevious();
+      assertArrayShallowEquals([s2], timeline.selection);
+      assertArrayShallowEquals([s1, s2, s3], timeline.highlight);
+    });
+    return promise;
+  });
+
+  test('findControllerChangeFilterAfterNext', function() {
+    var timeline = new FakeTimeline();
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+
+    var s1 = {guid: 1};
+    var s2 = {guid: 2};
+    var s3 = {guid: 3};
+    var s4 = {guid: 4};
+
+    timeline.addAllObjectsMatchingFilterToSelectionReturnValue = [s1, s2, s3];
+    controller.filterText = 'asdf';
+    var promise = controller.updateFilterHits();
+    promise.then(function() {
+      // Loop through hits then when we wrap, try moving backward.
+      controller.findNext();
+      timeline.addAllObjectsMatchingFilterToSelectionReturnValue = [s4];
+
+      controller.filterText = 'asdfsf';
+      var nextPromise = controller.updateFilterHits();
+      nextPromise.then(function() {
+        controller.findNext();
+        assertArrayShallowEquals([s4], timeline.selection);
+      });
+    });
+    return promise;
+  });
+
+  test('findControllerSelectsAllItemsFirst', function() {
+    var timeline = new FakeTimeline();
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+
+    var s1 = {guid: 1};
+    var s2 = {guid: 2};
+    var s3 = {guid: 3};
+    timeline.addAllObjectsMatchingFilterToSelectionReturnValue = [s1, s2, s3];
+    controller.filterText = 'asdfsf';
+    var promise = controller.updateFilterHits();
+    promise.then(function() {
+      assertArrayShallowEquals([], timeline.selection);
+      assertArrayShallowEquals([s1, s2, s3], timeline.highlight);
+      controller.findNext();
+      assertArrayShallowEquals([s1], timeline.selection);
+      controller.findNext();
+      assertArrayShallowEquals([s2], timeline.selection);
+      assertArrayShallowEquals([s1, s2, s3], timeline.highlight);
+    });
+    return promise;
+  });
+
+  test('findControllerWithRealTimeline', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(new tv.c.trace_model.ThreadSlice(
+        '', 'a', 0, 1, {}, 3));
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+
+    // Test find with no filterText.
+    controller.findNext();
+
+    // Test find with filter txt.
+    controller.filterText = 'a';
+    var promise = controller.updateFilterHits();
+    promise.then(function() {
+      assert.deepEqual(timeline.selection, []);
+      assert.deepEqual(timeline.highlight, t1.sliceGroup.slices);
+
+      controller.findNext();
+      assert.equal(timeline.selection.length, 1);
+      assert.equal(timeline.selection[0], t1.sliceGroup.slices[0]);
+
+      controller.filterText = 'xxx';
+      var nextPromise = controller.updateFilterHits();
+      nextPromise.then(function() {
+        assert.equal(timeline.highlight.length, 0);
+        assert.equal(timeline.selection.length, 0);
+        controller.findNext();
+        assert.equal(timeline.selection.length, 0);
+        controller.findNext();
+        assert.equal(timeline.selection.length, 0);
+      });
+      return nextPromise;
+    });
+    return promise;
+  });
+
+  test('findControllerNavigation', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(2);
+    t1.sliceGroup.pushSlice(new tv.c.trace_model.ThreadSlice(
+        '', 'a', 0, 1, {}, 3));
+
+    var timeline = new tv.c.TimelineTrackView();
+    var vp = new tv.c.TimelineViewport(timeline);
+    timeline.model = model;
+    timeline.focusElement = timeline;
+    timeline.tabIndex = 0;
+    timeline.style.maxHeight = '600px';
+    this.addHTMLOutput(timeline);
+
+    vp.containerToTrackObj = {
+      getTrackByStableId: function(stableId) {
+        if (stableId === '1.2')
+          return { eventContainer: { stableId: '1.2' } };
+        return undefined;
+      }
+    };
+
+    var controller = new tv.c.FindController();
+    controller.timeline = timeline;
+    controller.filterText = '2000@1.2x7';
+
+    var cbCalls = 0;
+    timeline.navToPosition = function(uiState) {
+      assert.equal(uiState.location.xWorld, 2000);
+      assert.equal(
+          uiState.location.getContainingTrack(vp).eventContainer.stableId,
+          '1.2');
+      assert.equal(uiState.scaleX, 7);
+      cbCalls++;
+    };
+    controller.updateFilterHits();
+    assert.equal(cbCalls, 1);
+
+    cbCalls = 0;
+    timeline.removeXNavStringMarker = function() {
+      cbCalls++;
+    };
+    controller.filterText = '';
+    controller.updateFilterHits();
+    assert.equal(cbCalls, 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/importer/empty_importer.html b/trace-viewer/trace_viewer/core/importer/empty_importer.html
new file mode 100644
index 0000000..352f82a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/importer/empty_importer.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/core/importer/importer.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data importers.
+ */
+tv.exportTo('tv.c.importer', function() {
+  var Importer = tv.c.importer.Importer;
+  /**
+   * Importer for empty strings and arrays.
+   * @constructor
+   */
+  function EmptyImporter(events) {
+    this.importPriority = 0;
+  };
+
+  EmptyImporter.canImport = function(eventData) {
+    if (eventData instanceof Array && eventData.length == 0)
+      return true;
+    if (typeof(eventData) === 'string' || eventData instanceof String) {
+      return eventData.length == 0;
+    }
+    return false;
+  };
+
+  EmptyImporter.prototype = {
+    __proto__: Importer.prototype
+  };
+
+  Importer.register(EmptyImporter);
+
+  return {
+    EmptyImporter: EmptyImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/importer/importer.html b/trace-viewer/trace_viewer/core/importer/importer.html
new file mode 100644
index 0000000..6f18d43
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/importer/importer.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data importers.
+ */
+tv.exportTo('tv.c.importer', function() {
+  function Importer() { }
+
+  Importer.prototype = {
+    __proto__: Object.prototype,
+
+    /**
+     * Called by the Model to check whether the importer type stores the actual
+     * trace data or just holds it as container for further extraction.
+     */
+    isTraceDataContainer: function() {
+      return false;
+    },
+
+    /**
+     * Called by the Model to extract one or more subtraces from the event data.
+     */
+    extractSubtraces: function() {
+      return [];
+    },
+
+    /**
+     * Called to import events into the Model.
+     */
+    importEvents: function() {
+    },
+
+    /**
+     * Called to import sample data into the Model.
+     */
+    importSampleData: function() {
+    },
+
+    /**
+     * Called by the Model after all other importers have imported their
+     * events.
+     */
+    finalizeImport: function() {
+    },
+
+    /**
+     * Called by the Model to join references between objects, after final
+     * model bounds have been computed.
+     */
+    joinRefs: function() {
+    }
+  };
+
+
+  var options = new tv.b.ExtensionRegistryOptions(tv.b.BASIC_REGISTRY_MODE);
+  options.defaultMetadata = {};
+  options.mandatoryBaseClass = Importer;
+  tv.b.decorateExtensionRegistry(Importer, options);
+
+  Importer.findImporterFor = function(eventData) {
+    var typeInfo = Importer.findTypeInfoMatching(function(ti) {
+      return ti.constructor.canImport(eventData);
+    });
+    if (typeInfo)
+      return typeInfo.constructor;
+    return undefined;
+  };
+
+  return {
+    Importer: Importer
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/importer/simple_line_reader.html b/trace-viewer/trace_viewer/core/importer/simple_line_reader.html
new file mode 100644
index 0000000..1ade622
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/importer/simple_line_reader.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c.importer', function() {
+  /**
+   * @constructor
+   */
+  function SimpleLineReader(text) {
+    this.lines_ = text.split('\n');
+    this.curLine_ = 0;
+
+    this.savedLines_ = undefined;
+  }
+
+  SimpleLineReader.prototype = {
+    advanceToLineMatching: function(regex) {
+      for (; this.curLine_ < this.lines_.length; this.curLine_++) {
+        var line = this.lines_[this.curLine_];
+        if (this.savedLines_ !== undefined)
+          this.savedLines_.push(line);
+        if (regex.test(line))
+          return true;
+      }
+      return false;
+    },
+
+    get curLineNumber() {
+      return this.curLine_;
+    },
+
+    beginSavingLines: function() {
+      this.savedLines_ = [];
+    },
+
+    endSavingLinesAndGetResult: function() {
+      var tmp = this.savedLines_;
+      this.savedLines_ = undefined;
+      return tmp;
+    }
+  };
+
+  return {
+    SimpleLineReader: SimpleLineReader
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/location.html b/trace-viewer/trace_viewer/core/location.html
new file mode 100644
index 0000000..da91d23
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/location.html
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  /**
+   * YComponent is a class that handles storing the stableId and the percentage
+   * offset in the y direction of all tracks within a specific viewX and viewY
+   * coordinate.
+   * @constructor
+   */
+  function YComponent(stableId, yPercentOffset) {
+    this.stableId = stableId;
+    this.yPercentOffset = yPercentOffset;
+  }
+
+  YComponent.prototype = {
+    toDict: function() {
+      return {
+        stableId: this.stableId,
+        yPercentOffset: this.yPercentOffset
+      };
+    }
+  };
+
+  /**
+   * Location is a class that represents a spatial location on the timeline
+   * that is specified by percent offsets within tracks rather than specific
+   * points.
+   *
+   * @constructor
+   */
+  function Location(xWorld, yComponents) {
+    this.xWorld_ = xWorld;
+    this.yComponents_ = yComponents;
+  };
+
+  /**
+   * Returns a new Location given by x and y coordinates with respect to
+   * the timeline's drawing canvas.
+   */
+  Location.fromViewCoordinates = function(viewport, viewX, viewY) {
+    var dt = viewport.currentDisplayTransform;
+    var xWorld = dt.xViewToWorld(viewX);
+    var yComponents = [];
+
+    // Since we're given coordinates within the timeline canvas, we need to
+    // convert them to document coordinates to get the element.
+    var elem = document.elementFromPoint(
+          viewX + viewport.modelTrackContainer.canvas.offsetLeft,
+          viewY + viewport.modelTrackContainer.canvas.offsetTop);
+    // Build yComponents by calculating percentage offset with respect to
+    // each parent track.
+    while (elem instanceof tv.c.tracks.Track) {
+      if (elem.eventContainer) {
+        var boundRect = elem.getBoundingClientRect();
+        var yPercentOffset = (viewY - boundRect.top) / boundRect.height;
+        yComponents.push(
+            new YComponent(elem.eventContainer.stableId, yPercentOffset));
+      }
+      elem = elem.parentElement;
+    }
+
+    if (yComponents.length == 0)
+      return;
+    return new Location(xWorld, yComponents);
+  }
+
+  Location.fromStableIdAndTimestamp = function(viewport, stableId, ts) {
+    var xWorld = ts;
+    var yComponents = [];
+
+    // The y components' percentage offsets will be calculated with respect to
+    // the boundingRect's top of containing track.
+    var containerToTrack = viewport.containerToTrackObj;
+    var elem = containerToTrack.getTrackByStableId(stableId);
+    if (!elem)
+      return;
+
+    var firstY = elem.getBoundingClientRect().top;
+    while (elem instanceof tv.c.tracks.Track) {
+      if (elem.eventContainer) {
+        var boundRect = elem.getBoundingClientRect();
+        var yPercentOffset = (firstY - boundRect.top) / boundRect.height;
+        yComponents.push(
+            new YComponent(elem.eventContainer.stableId, yPercentOffset));
+      }
+      elem = elem.parentElement;
+    }
+
+    if (yComponents.length == 0)
+      return;
+    return new Location(xWorld, yComponents);
+  }
+
+  Location.prototype = {
+
+    get xWorld() {
+      return this.xWorld_;
+    },
+
+    /**
+     * Returns the first valid containing track based on the
+     * internal yComponents.
+     */
+    getContainingTrack: function(viewport) {
+      var containerToTrack = viewport.containerToTrackObj;
+      for (var i in this.yComponents_) {
+        var yComponent = this.yComponents_[i];
+        var track = containerToTrack.getTrackByStableId(yComponent.stableId);
+        if (track !== undefined)
+          return track;
+      }
+    },
+
+    /**
+     * Calculates and returns x and y coordinates of the current location with
+     * respect to the timeline's canvas.
+     */
+    toViewCoordinates: function(viewport) {
+      var dt = viewport.currentDisplayTransform;
+      var containerToTrack = viewport.containerToTrackObj;
+      var viewX = dt.xWorldToView(this.xWorld_);
+
+      var viewY = -1;
+      for (var index in this.yComponents_) {
+        var yComponent = this.yComponents_[index];
+        var track = containerToTrack.getTrackByStableId(yComponent.stableId);
+        if (track !== undefined) {
+          var boundRect = track.getBoundingClientRect();
+          viewY = yComponent.yPercentOffset * boundRect.height + boundRect.top;
+          break;
+        }
+      }
+
+      return {
+        viewX: viewX,
+        viewY: viewY
+      };
+    },
+
+    toDict: function() {
+      return {
+        xWorld: this.xWorld_,
+        yComponents: this.yComponents_
+      };
+    }
+  };
+
+  return {
+    Location: Location
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/location_test.html b/trace-viewer/trace_viewer/core/location_test.html
new file mode 100644
index 0000000..6a90adb
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/location_test.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/location.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Location = tv.c.Location;
+  var Model = tv.c.TraceModel;
+
+  test('locationObj', function() {
+    var model = new tv.c.TraceModel();
+    var process = model.getOrCreateProcess(123);
+    var thread = process.getOrCreateThread(456);
+
+    model.importTraces([], false, false, function() {
+      thread.asyncSliceGroup.push(
+        tv.c.test_utils.newAsyncSliceNamed('a', 80, 20, thread, thread));
+      thread.asyncSliceGroup.push(
+        tv.c.test_utils.newAsyncSliceNamed('a', 85, 10, thread, thread));
+    });
+
+    var timeline = new tv.c.TimelineTrackView();
+    var vp = new tv.c.TimelineViewport(timeline);
+    timeline.model = model;
+    timeline.focusElement = timeline;
+    timeline.tabIndex = 0;
+    timeline.style.maxHeight = '600px';
+    this.addHTMLOutput(timeline);
+
+    // Our stableId to track map is not automatically built. We need to
+    // search for the tracks and manually build the stableId map here.
+    var processTracks = document.getElementsByClassName('process-track-base');
+    vp.modelTrackContainer = {
+      addContainersToTrackMap: function(containerToTrackObj) {
+        // Invoking the process track's addContainersToTrackMap is enough to
+        // build the map for all children (i.e. Threads, AsyncSliceGroups)
+        // as well.
+        for (var i = 0; i < processTracks.length; i++)
+          processTracks[i].addContainersToTrackMap(containerToTrackObj);
+      },
+      addEventListener: function() {},
+      canvas: {
+        offsetLeft: tv.c.constants.HEADING_WIDTH,
+        offsetTop: 0
+      }
+    };
+    vp.rebuildContainerToTrackMap();
+
+    var asyncTrack =
+        vp.containerToTrackObj.getTrackByStableId('123.456.AsyncSliceGroup');
+    assert.isNotNull(asyncTrack);
+    assert.isFalse(asyncTrack.expanded); // Make sure this starts unexpanded.
+
+    // Hack to allow Location to find the element we're looking for.
+    // This ensures the correct behaviour of document.elementFrompoint(x,y) of
+    // an originally off-screen element.
+    asyncTrack.scrollIntoView();
+
+    var boundRect = asyncTrack.getBoundingClientRect();
+    var viewX = boundRect.left;
+    var viewY = boundRect.top + boundRect.height / 2;
+    var location = Location.fromViewCoordinates(vp, viewX, viewY);
+    assert.equal(asyncTrack, location.getContainingTrack(vp));
+    assert.deepEqual(location.toViewCoordinates(vp),
+                     { viewX: viewX, viewY: viewY });
+
+    // Try expanding the multi-row track so that the dimensions of the thread
+    // track changes.
+    asyncTrack.expanded = true;
+    // Expanding the track causes the height to double. We can calculate the new
+    // viewY with respect to the track's old boundRect. ViewX remains unchanged.
+    var expandedViewY = boundRect.top + boundRect.height;
+    assert.deepEqual(location.toViewCoordinates(vp),
+                     { viewX: viewX, viewY: expandedViewY });
+
+    // Test the functionality of fromStableIdAndTimestamp.
+    var locationFromCoord =
+        Location.fromViewCoordinates(vp, viewX, boundRect.top);
+    var locationFromStableId =
+        Location.fromStableIdAndTimestamp(vp, '123.456.AsyncSliceGroup',
+                                          location.xWorld);
+    assert.deepEqual(locationFromCoord, locationFromStableId);
+
+    // Undo scroll.
+    document.getElementById('results-container').scrollTop = 0;
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/scripting_control.html b/trace-viewer/trace_viewer/core/scripting_control.html
new file mode 100644
index 0000000..3e4f179
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/scripting_control.html
@@ -0,0 +1,183 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/core/scripting_controller.html">
+<link rel="import" href="/core/timeline_track_view.html">
+
+<polymer-element
+    name="tracing-scripting-control" constructor="TracingScriptingControl">
+  <template>
+    <style>
+      :host {
+        flex: 1 1 auto;
+      }
+      .root {
+        font-family: monospace;
+        cursor: text;
+
+        padding: 2px;
+        margin: 2px;
+        border: 1px solid rgba(0, 0, 0, 0.5);
+        background: white;
+
+        height: 100px;
+        overflow-y: auto;
+
+        transition-property: opacity, height, padding, margin;
+        transition-duration: .2s;
+        transition-timing-function: ease-out;
+      }
+      .hidden {
+        margin-top: 0px;
+        margin-bottom: 0px;
+        padding-top: 0px;
+        padding-bottom: 0px;
+        height: 0px;
+        opacity: 0;
+      }
+      .focused {
+        outline: auto 5px -webkit-focus-ring-color;
+      }
+      #history {
+        -webkit-user-select: text;
+        color: #777;
+      }
+      #prompt {
+        -webkit-user-select: text;
+        -webkit-user-modify: read-write-plaintext-only;
+        text-overflow: clip !important;
+        text-decoration: none !important;
+      }
+      #prompt:focus {
+        outline: none;
+      }
+      #prompt br {
+        display: none;
+      }
+      #prompt ::before {
+        content: ">";
+        color: #468;
+      }
+    </style>
+
+    <div id="root" class="root hidden" tabindex="0"
+         on-focus="{{ onConsoleFocus }}">
+      <div id='history'></div>
+      <div id='prompt'
+           on-keypress="{{ promptKeyPress }}"
+           on-keydown="{{ promptKeyDown }}"
+           on-blur="{{ onConsoleBlur }}">
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    _isEnterKey: function(event) {
+      // Check if in IME.
+      return event.keyCode !== 229 && event.keyIdentifier === 'Enter';
+    },
+
+    _setFocused: function(focused) {
+      var promptEl = this.$.prompt;
+      if (focused) {
+        promptEl.focus();
+        this.$.root.classList.add('focused');
+        // Move cursor to the end of any existing text.
+        if (promptEl.innerText.length > 0) {
+          var sel = window.getSelection();
+          sel.collapse(promptEl.firstChild, promptEl.innerText.length);
+        }
+      } else {
+        promptEl.blur();
+        this.$.root.classList.remove('focused');
+        // Workaround for crbug.com/89026 to ensure the prompt doesn't retain
+        // keyboard focus.
+        var dummyInput = document.createElement('input');
+        dummyInput.focus();
+        dummyInput.setSelectionRange(0, 0);
+        dummyInput.blur();
+      }
+    },
+
+    onConsoleFocus: function(e) {
+      e.stopPropagation();
+      this._setFocused(true);
+    },
+
+    onConsoleBlur: function(e) {
+      e.stopPropagation();
+      this._setFocused(false);
+    },
+
+    promptKeyDown: function(e) {
+      e.stopPropagation();
+      if (!this._isEnterKey(e))
+        return;
+      var promptEl = this.$.prompt;
+      var command = promptEl.innerText;
+      if (command.length === 0)
+        return;
+      promptEl.innerText = '';
+      this.addLine_(String.fromCharCode(187) + ' ' + command);
+
+      try {
+        var result = this.controller_.executeCommand(command);
+      } catch (e) {
+        result = e.stack || e.stackTrace;
+      }
+
+      if (result instanceof tv.b.Task) {
+        // TODO(skyostil): Show a cool spinner.
+        tv.b.Task.RunWhenIdle(result);
+      } else {
+        this.addLine_(result);
+      }
+    },
+
+    addLine_: function(line) {
+      var historyEl = this.$.history;
+      if (historyEl.innerText.length !== 0)
+        historyEl.innerText += '\n';
+      historyEl.innerText += line;
+    },
+
+    promptKeyPress: function(e) {
+      e.stopPropagation();
+    },
+
+    toggleVisibility: function() {
+      var root = this.$.root;
+      if (!this.visible) {
+        root.classList.remove('hidden');
+        this._setFocused(true);
+      } else {
+        root.classList.add('hidden');
+        this._setFocused(false);
+      }
+    },
+
+    get hasFocus() {
+      return this === document.activeElement;
+    },
+
+    get visible() {
+      var root = this.$.root;
+      return !root.classList.contains('hidden');
+    },
+
+    get controller() {
+      return this.controller_;
+    },
+
+    set controller(c) {
+      this.controller_ = c;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/scripting_control_test.html b/trace-viewer/trace_viewer/core/scripting_control_test.html
new file mode 100644
index 0000000..da94495
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/scripting_control_test.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/scripting_control.html">
+<link rel="import" href="/core/test_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var ctl = new TracingScriptingControl();
+    this.addHTMLOutput(ctl);
+    ctl.toggleVisibility();
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/scripting_controller.html b/trace-viewer/trace_viewer/core/scripting_controller.html
new file mode 100644
index 0000000..b998c1d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/scripting_controller.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/polymer_utils.html">
+<link rel="import" href="/extras/tquery/tquery.html">
+
+<polymer-element name='tv-c-scripting-controller'>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.timeline_ = undefined;
+      this.scriptObjectNames_ = [];
+      this.scriptObjectValues_ = [];
+
+      // Register all scripting objects.
+      var objects = tv.b.getPolymerElementsThatSubclass(
+          'tv-c-scripting-object');
+      objects.forEach(function(className) {
+        var obj = document.createElement(className);
+        if (!obj.scriptName)
+          return;
+        this.addScriptObject(obj.scriptName, obj.scriptValue);
+        // Also make the object available to the DevTools inspector.
+        window[obj.scriptName] = obj.scriptValue;
+      }.bind(this));
+    },
+
+    get timeline() {
+      return this.timeline_;
+    },
+
+    set timeline(t) {
+      this.timeline_ = t;
+      this.scriptObjectValues_.forEach(function(v) {
+        if (v.onTimelineChanged)
+          v.onTimelineChanged(t);
+      });
+    },
+
+    addScriptObject: function(name, value) {
+      this.scriptObjectNames_.push(name);
+      this.scriptObjectValues_.push(value);
+    },
+
+    executeCommand: function(command) {
+      var f = new Function(
+          this.scriptObjectNames_, 'return eval(' + command + ')');
+      return f.apply(null, this.scriptObjectValues_);
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/scripting_controller_test.html b/trace-viewer/trace_viewer/core/scripting_controller_test.html
new file mode 100644
index 0000000..cce9449
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/scripting_controller_test.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/scripting_controller.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('scriptingControllerBasicArithmetic', function() {
+    var controller = document.createElement('tv-c-scripting-controller');
+    var result = controller.executeCommand('1 + 1');
+    assert.equal(result, 2);
+  });
+
+  test('scriptingControllerNonLocalContext', function() {
+    var controller = document.createElement('tv-c-scripting-controller');
+    var x = 1;
+    controller.executeCommand('x = 2');
+    assert.equal(x, 1);
+  });
+
+  test('scriptingControllerModifyGlobalContext', function() {
+    var controller = document.createElement('tv-c-scripting-controller');
+    window._x = 1;
+    controller.executeCommand('_x = 2');
+    assert.equal(window._x, 2);
+    delete window._x;
+  });
+
+  test('scriptingControllerPersistentContext', function() {
+    var controller = document.createElement('tv-c-scripting-controller');
+    controller.executeCommand('a = 42');
+    var result = controller.executeCommand('a');
+    assert.equal(result, 42);
+  });
+
+  test('scriptingControllerAddScriptObject', function() {
+    var controller = document.createElement('tv-c-scripting-controller');
+    controller.addScriptObject('z', 123);
+    var result = controller.executeCommand('z');
+    assert.equal(result, 123);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/scripting_object.html b/trace-viewer/trace_viewer/core/scripting_object.html
new file mode 100644
index 0000000..28e6a8f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/scripting_object.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<!--
+Copyright 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/utils.html">
+
+<polymer-element name='tv-c-scripting-object'>
+  <script>
+  'use strict';
+  Polymer({
+    // Name under which this object is accessible in the console.
+    get scriptName() {
+      throw new Error('Not implemented');
+    },
+
+    // Value to bind the console object to. Defaults to "this".
+    get scriptValue() {
+      return this;
+    },
+
+    // Called when the active timeline changes.
+    onTimelineChanged: function(timeline) {
+      // No-op by default.
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/selection.html b/trace-viewer/trace_viewer/core/selection.html
new file mode 100644
index 0000000..c64fba3
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/selection.html
@@ -0,0 +1,277 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/events.html">
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Code for the viewport.
+ */
+tv.exportTo('tv.c', function() {
+
+  var EventRegistry = tv.c.trace_model.EventRegistry;
+
+  var RequestSelectionChangeEvent = tv.b.Event.bind(
+      undefined, 'requestSelectionChange', true, false);
+
+  /**
+   * Represents a selection within a  and its associated set of tracks.
+   * @constructor
+   */
+  function Selection(opt_events) {
+    // sunburst_zoom_level is used by sunburst chart to remember
+    // zoom level across selection changes.
+    // TODO(gholap): get rid of this eventually when
+    //               selections support frames.
+    this.sunburst_zoom_level = undefined;
+
+    this.bounds_dirty_ = true;
+    this.bounds_ = new tv.b.Range();
+    this.length_ = 0;
+    this.guid_ = tv.b.GUID.allocate();
+    this.pushed_guids_ = {};
+
+    if (opt_events) {
+      if (opt_events instanceof Array) {
+        for (var i = 0; i < opt_events.length; i++)
+          this.push(opt_events[i]);
+      } else {
+        this.push(opt_events);
+      }
+    }
+  }
+  Selection.prototype = {
+    __proto__: Object.prototype,
+
+    get bounds() {
+      if (this.bounds_dirty_) {
+        this.bounds_.reset();
+        for (var i = 0; i < this.length_; i++)
+          this[i].addBoundsToRange(this.bounds_);
+        this.bounds_dirty_ = false;
+      }
+      return this.bounds_;
+    },
+
+    get duration() {
+      if (this.bounds_.isEmpty)
+        return 0;
+      return this.bounds_.max - this.bounds_.min;
+    },
+
+    get length() {
+      return this.length_;
+    },
+
+    get guid() {
+      return this.guid_;
+    },
+
+    clear: function() {
+      for (var i = 0; i < this.length_; ++i)
+        delete this[i];
+      this.length_ = 0;
+      this.bounds_dirty_ = true;
+    },
+
+    // push pushes only unique events.
+    // If an event has been already pushed, do nothing.
+    push: function(event) {
+      if (event.guid == undefined)
+        throw new Error('Event must have a GUID');
+
+      if (this.contains(event))
+        return event;
+
+      this.pushed_guids_[event.guid] = true;
+      this[this.length_++] = event;
+      this.bounds_dirty_ = true;
+      return event;
+    },
+
+    contains: function(event) {
+      return this.pushed_guids_[event.guid];
+    },
+
+    addSelection: function(selection) {
+      for (var i = 0; i < selection.length; i++)
+        this.push(selection[i]);
+    },
+
+    subSelection: function(index, count) {
+      count = count || 1;
+
+      var selection = new Selection();
+      selection.bounds_dirty_ = true;
+      if (index < 0 || index + count > this.length_)
+        throw new Error('Index out of bounds');
+
+      for (var i = index; i < index + count; i++)
+        selection.push(this[i]);
+
+      return selection;
+    },
+
+    getEventsOrganizedByBaseType: function(opt_pruneEmpty) {
+      var events = {};
+      var allTypeInfos = EventRegistry.getAllRegisteredTypeInfos();
+      allTypeInfos.forEach(function(eventTypeInfo) {
+        events[eventTypeInfo.metadata.name] = new Selection();
+        if (this.sunburst_zoom_level !== undefined)
+          events[eventTypeInfo.metadata.name].sunburst_zoom_level =
+              this.sunburst_zoom_level;
+      }, this);
+
+      this.forEach(function(event, i) {
+        var maxEventIndex = -1;
+        var maxEventTypeInfo = undefined;
+        allTypeInfos.forEach(function(eventTypeInfo, eventIndex) {
+          if (!(event instanceof eventTypeInfo.constructor))
+            return;
+          if (eventIndex > maxEventIndex) {
+            maxEventIndex = eventIndex;
+            maxEventTypeInfo = eventTypeInfo;
+          }
+        });
+        if (maxEventIndex == -1)
+          throw new Error('Unrecgonized event type');
+        events[maxEventTypeInfo.metadata.name].push(event);
+      });
+      if (opt_pruneEmpty) {
+        var prunedEvents = {};
+        for (var eventType in events) {
+          if (events[eventType].length > 0)
+            prunedEvents[eventType] = events[eventType];
+        }
+        return prunedEvents;
+      } else {
+        return events;
+      }
+    },
+
+    getEventsOrganizedByTitle: function() {
+      var eventsByTitle = {};
+      for (var i = 0; i < this.length; i++) {
+        var event = this[i];
+        if (event.title === undefined)
+          throw new Error('An event didn\'t have a title!');
+        if (eventsByTitle[event.title] == undefined) {
+          eventsByTitle[event.title] = [];
+        }
+        eventsByTitle[event.title].push(event);
+      }
+      return eventsByTitle;
+    },
+
+    enumEventsOfType: function(type, func) {
+      for (var i = 0; i < this.length_; i++)
+        if (this[i] instanceof type)
+          func(this[i]);
+    },
+
+    get userFriendlyName() {
+      if (this.length === 0) {
+        throw new Error('Empty selection');
+      }
+
+      var eventsByBaseType = this.getEventsOrganizedByBaseType(true);
+      var eventTypeName = tv.b.dictionaryKeys(eventsByBaseType)[0];
+
+      if (this.length === 1) {
+        var tmp = EventRegistry.getUserFriendlySingularName(eventTypeName);
+        return this[0].userFriendlyName;
+      }
+
+      var numEventTypes = tv.b.dictionaryLength(eventsByBaseType);
+      if (numEventTypes !== 1) {
+        return this.length + ' events of various types';
+      }
+
+      var tmp = EventRegistry.getUserFriendlyPluralName(eventTypeName);
+      return this.length + ' ' + tmp;
+    },
+
+    /**
+     * Helper for selection previous or next.
+     * @param {boolean} offset If positive, select one forward (next).
+     *   Else, select previous.
+     *
+     * @param {TimelineViewport} viewport The viewport to use to determine what
+     * is near to the current selection.
+     *
+     * @return {boolean} true if current selection changed.
+     */
+    getShiftedSelection: function(viewport, offset) {
+      var newSelection = new Selection();
+      for (var i = 0; i < this.length_; i++) {
+        var event = this[i];
+
+        var addEventToNewSelection = function(event) {
+        };
+
+        // If this is a flow event, and we have a next/prev item in the chain
+        // then we use that as the item to move too. Otherwise, we let the
+        // normal movement for a slice kick in and use that.
+        if (event instanceof tv.c.trace_model.FlowEvent) {
+          if ((offset > 0) && event.nextFlowEvent) {
+            newSelection.push(event.nextFlowEvent);
+            continue;
+          } else if ((offset < 0) && event.previousFlowEvent) {
+            newSelection.push(event.previousFlowEvent);
+            continue;
+          }
+        }
+
+        var track = viewport.trackForEvent(event);
+        track.addItemNearToProvidedEventToSelection(
+            event, offset, newSelection);
+      }
+
+      if (newSelection.length == 0)
+        return undefined;
+      return newSelection;
+    },
+
+    forEach: function(fn, opt_this) {
+      for (var i = 0; i < this.length; i++)
+        fn.call(opt_this, this[i], i);
+    },
+
+    map: function(fn, opt_this) {
+      var res = [];
+      for (var i = 0; i < this.length; i++)
+        res.push(fn.call(opt_this, this[i], i));
+      return res;
+    },
+
+    every: function(fn, opt_this) {
+      for (var i = 0; i < this.length; i++)
+        if (!fn.call(opt_this, this[i], i))
+          return false;
+      return true;
+    },
+
+    some: function(fn, opt_this) {
+      for (var i = 0; i < this.length; i++)
+        if (fn.call(opt_this, this[i], i))
+          return true;
+      return false;
+    }
+  };
+
+  return {
+    Selection: Selection,
+    RequestSelectionChangeEvent: RequestSelectionChangeEvent
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/selection_test.html b/trace-viewer/trace_viewer/core/selection_test.html
new file mode 100644
index 0000000..35ac581
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/selection_test.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/tracks/slice_track.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('selectionObject', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 5, {}, 1));
+
+    var sel = new tv.c.Selection();
+    sel.push(t1.sliceGroup.slices[0]);
+
+    assert.equal(sel.bounds.min, 1);
+    assert.equal(sel.bounds.max, 4);
+    assert.equal(sel[0], t1.sliceGroup.slices[0]);
+
+    sel.push(t1.sliceGroup.slices[1]);
+    assert.equal(sel.bounds.min, 1);
+    assert.equal(sel.bounds.max, 6);
+    assert.equal(sel[1], t1.sliceGroup.slices[1]);
+
+    sel.clear();
+    assert.equal(sel.length, 0);
+  });
+
+  test('shiftedSelection', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 5, {}, 1));
+
+    var viewport = new tv.c.TimelineViewport();
+    var track = new tv.c.tracks.SliceTrack(viewport);
+    viewport.modelTrackContainer = track;
+    track.slices = t1.sliceGroup.slices;
+
+    viewport.rebuildEventToTrackMap();
+
+
+    var sel = new tv.c.Selection();
+    sel.push(t1.sliceGroup.slices[0]);
+
+    var shifted = sel.getShiftedSelection(track.viewport, 1);
+    assert.equal(shifted.length, 1);
+    assert.equal(shifted[0], t1.sliceGroup.slices[1]);
+  });
+
+  test('uniqueContents', function() {
+    var sample1 = {guid: 1};
+    var sample2 = {guid: 2};
+
+    var selection = new tv.c.Selection();
+
+    selection.push(sample1);
+    selection.push(sample2);
+    assert.equal(selection.length, 2);
+
+    selection.push(sample1);
+    assert.equal(selection.length, 2);
+  });
+
+  test('userFriendlyNameSingular', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    var seleciton = new tv.c.Selection(t1.sliceGroup.slices[0]);
+    assert.isDefined(seleciton.userFriendlyName);
+  });
+
+  test('userFriendlyNamePlural', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 2, {}, 3));
+    var seleciton = new tv.c.Selection([
+        t1.sliceGroup.slices[0],
+        t1.sliceGroup.slices[1]
+    ]);
+    assert.isDefined(seleciton.userFriendlyName);
+  });
+
+  test('userFriendlyNameMixedPlural', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 2, {}, 3));
+
+    var i10 = new tv.c.trace_model.ObjectInstance(
+    {}, '0x1000', 'cat', 'name', 10);
+    var s10 = i10.addSnapshot(10, {foo: 1});
+
+    var seleciton = new tv.c.Selection([
+        t1.sliceGroup.slices[0],
+        s10
+    ]);
+    assert.isDefined(seleciton.userFriendlyName);
+  });
+
+
+  test('groupEventsByTitle', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 2, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'b', 0, 3, {}, 3));
+    var selection = new tv.c.Selection([
+        t1.sliceGroup.slices[0],
+        t1.sliceGroup.slices[1],
+        t1.sliceGroup.slices[2]
+    ]);
+
+    var eventsByTitle = selection.getEventsOrganizedByTitle();
+    assert.equal(2, tv.b.dictionaryLength(eventsByTitle));
+    assert.sameMembers(eventsByTitle['a'],
+                 [t1.sliceGroup.slices[0], t1.sliceGroup.slices[1]]);
+    assert.sameMembers(eventsByTitle['b'],
+                 [t1.sliceGroup.slices[2]]);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/side_panel/side_panel.html b/trace-viewer/trace_viewer/core/side_panel/side_panel.html
new file mode 100644
index 0000000..cd80ecc
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/side_panel/side_panel.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/ui.html">
+
+<polymer-element name='tv-c-side-panel'>
+  <script>
+  'use strict';
+  Polymer({
+    ready: function() {
+      this.objectInstance_ = undefined;
+    },
+
+    get rangeOfInterest() {
+      throw new Error('Not implemented');
+    },
+
+    set rangeOfInterest(rangeOfInterest) {
+      throw new Error('Not implemented');
+    },
+
+    get selection() {
+      throw new Error('Not implemented');
+    },
+
+    set selection(selection) {
+      throw new Error('Not implemented');
+    },
+
+    get model() {
+      throw new Error('Not implemented');
+    },
+
+    set model(model) {
+      throw new Error('Not implemented');
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/core/side_panel/side_panel_container.html b/trace-viewer/trace_viewer/core/side_panel/side_panel_container.html
new file mode 100644
index 0000000..25fa380
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/side_panel/side_panel_container.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel='import' href='/base/polymer_utils.html'>
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/core/side_panel/side_panel.html">
+
+<polymer-element name='tv-c-side-panel-container' is='HTMLUnknownElement'>
+  <template>
+    <style>
+    :host {
+      align-items: stretch;
+      display: -webkit-flex;
+    }
+
+    :host[expanded] > active-panel-container {
+      -webkit-flex: 1 1 auto;
+      border-left: 1px solid black;
+      display: -webkit-flex;
+    }
+
+    :host:not([expanded]) > active-panel-container {
+      display: none;
+    }
+
+    tab-strip {
+      -webkit-flex: 0 0 auto;
+      -webkit-flex-direction: column;
+      -webkit-user-select: none;
+      background-color: rgb(236, 236, 236);
+      border-left: 1px solid black;
+      cursor: default;
+      display: -webkit-flex;
+      min-width: 18px; /* workaround for flexbox and writing-mode mixing bug */
+      padding: 10px 0 10px 0;
+      font-size: 12px;
+    }
+
+    tab-strip > tab-strip-label {
+      -webkit-writing-mode: vertical-rl;
+      display: inline;
+      margin-right: 1px;
+      min-height: 20px;
+      padding: 15px 3px 15px 1px;
+    }
+
+    tab-strip >
+        tab-strip-label:not([enabled]) {
+      color: rgb(128, 128, 128);
+    }
+
+    tab-strip > tab-strip-label[selected] {
+      background-color: white;
+      border: 1px solid rgb(163, 163, 163);
+      border-left: none;
+      padding: 14px 2px 14px 1px;
+    }
+    </style>
+
+    <active-panel-container id='active_panel_container'>
+    </active-panel-container>
+    <tab-strip id='tab_strip'></tab-strip>
+  </template>
+
+  <script>
+  'use strict';
+  Polymer({
+    ready: function() {
+      this.activePanelContainer_ = this.$.active_panel_container;
+      this.tabStrip_ = this.$.tab_strip;
+
+      this.model_ = undefined;
+      this.rangeOfInterest_ = new tv.b.Range();
+    },
+
+    get model() {
+      return this.model_;
+    },
+
+    set model(model) {
+      this.model_ = model;
+      this.activePanelType_ = undefined;
+      this.updateContents_();
+    },
+
+    get expanded() {
+      this.hasAttribute('expanded');
+    },
+
+    get activePanel() {
+      if (this.activePanelContainer_.children.length === 0)
+        return undefined;
+      return this.activePanelContainer_.children[0];
+    },
+
+    get activePanelType() {
+      return this.activePanelType_;
+    },
+
+    set activePanelType(panelType) {
+      if (this.model_ === undefined)
+        throw new Error('Cannot activate panel without a model');
+
+      var panel = undefined;
+      if (panelType)
+          panel = document.createElement(panelType);
+
+      if (panel !== undefined && !panel.supportsModel(this.model_))
+        throw new Error('Cannot activate panel: does not support this model');
+
+      if (this.activePanelType) {
+        this.getLabelElementForPanelType_(
+            this.activePanelType).removeAttribute('selected');
+      }
+      this.activePanelContainer_.textContent = '';
+
+      if (panelType === undefined) {
+        this.removeAttribute('expanded');
+        this.activePanelType_ = undefined;
+        return;
+      }
+
+      this.getLabelElementForPanelType_(panelType).
+          setAttribute('selected', true);
+      this.setAttribute('expanded', true);
+
+      this.activePanelContainer_.appendChild(panel);
+      panel.rangeOfInterest = this.rangeOfInterest_;
+      panel.selection = this.selection_;
+      panel.model = this.model_;
+
+      this.activePanelType_ = panelType;
+    },
+
+    getPanelTypeForConstructor_: function(constructor) {
+      for (var i = 0; i < this.tabStrip_.children.length; i++) {
+        if (this.tabStrip_.children[i].panelType.constructor == constructor)
+          return this.tabStrip_.children[i].panelType;
+      }
+    },
+
+    getLabelElementForPanelType_: function(panelType) {
+      for (var i = 0; i < this.tabStrip_.children.length; i++) {
+        if (this.tabStrip_.children[i].panelType == panelType)
+          return this.tabStrip_.children[i];
+      }
+      return undefined;
+    },
+
+    updateContents_: function() {
+      var previouslyActivePanelType = this.activePanelType;
+
+      this.tabStrip_.textContent = '';
+      var supportedPanelTypes = [];
+
+      var panelTypes = tv.b.getPolymerElementsThatSubclass('tv-c-side-panel');
+      panelTypes.forEach(function(panelType) {
+        var labelEl = document.createElement('tab-strip-label');
+        var panel = document.createElement(panelType);
+
+        labelEl.textContent = panel.textLabel;
+        labelEl.panelType = panelType;
+
+        var supported = panel.supportsModel(this.model_);
+        if (this.model_ && supported.supported) {
+          supportedPanelTypes.push(panelType);
+          labelEl.setAttribute('enabled', true);
+          labelEl.addEventListener('click', function() {
+            this.activePanelType =
+                this.activePanelType === panelType ? undefined : panelType;
+          }.bind(this));
+        } else {
+          labelEl.title = 'Not supported for the current trace: ' +
+              supported.reason;
+        }
+        this.tabStrip_.appendChild(labelEl);
+      }, this);
+
+      // Restore the active panel, or collapse
+      if (previouslyActivePanelType &&
+          supportedPanelTypes.indexOf(previouslyActivePanelType) != -1) {
+        this.activePanelType = previouslyActivePanelType;
+        this.setAttribute('expanded', true);
+      } else {
+        this.activePanelContainer_.textContent = '';
+        this.removeAttribute('expanded');
+      }
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      this.selection_ = selection;
+      if (this.activePanel)
+        this.activePanel.selection = selection;
+    },
+
+    get rangeOfInterest() {
+      return this.rangeOfInterest_;
+    },
+
+    set rangeOfInterest(range) {
+      if (range == undefined)
+        throw new Error('Must not be undefined');
+      this.rangeOfInterest_ = range;
+      if (this.activePanel)
+        this.activePanel.rangeOfInterest = range;
+    }
+  });
+  </script>
+</polymer-element>
+
diff --git a/trace-viewer/trace_viewer/core/side_panel/side_panel_container_test.html b/trace-viewer/trace_viewer/core/side_panel/side_panel_container_test.html
new file mode 100644
index 0000000..d337a08
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/side_panel/side_panel_container_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/side_panel/side_panel.html">
+<link rel="import" href="/extras/side_panel/time_summary.html">
+<link rel="import" href="/core/side_panel/side_panel_container.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function createModel() {
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+      var browserProcess = m.getOrCreateProcess(1);
+      var browserMain = browserProcess.getOrCreateThread(2);
+      browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+      browserMain.sliceGroup.endSlice(10);
+      browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+      browserMain.sliceGroup.endSlice(30);
+    });
+    return m;
+  }
+
+  test('instantiateCollapsed', function() {
+    var container = document.createElement('tv-c-side-panel-container');
+    container.model = createModel();
+    this.addHTMLOutput(container);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/test_utils.html b/trace-viewer/trace_viewer/core/test_utils.html
new file mode 100644
index 0000000..026e162
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/test_utils.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/counter.html">
+<link rel="import" href="/core/trace_model/slice.html">
+<link rel="import" href="/core/trace_model/slice_group.html">
+<link rel="import" href="/core/trace_model/stack_frame.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper functions for use in tracing tests.
+ */
+tv.exportTo('tv.c.test_utils', function() {
+  function newAsyncSlice(start, duration, startThread, endThread) {
+    return newAsyncSliceNamed('a', start, duration, startThread, endThread);
+  }
+
+  function newAsyncSliceNamed(name, start, duration, startThread, endThread) {
+    var asyncSliceConstructor =
+        tv.c.trace_model.AsyncSlice.getConstructor('', name);
+
+    var s = new asyncSliceConstructor('', name, 0, start);
+    s.duration = duration;
+    s.startThread = startThread;
+    s.endThread = endThread;
+    return s;
+  }
+
+  function newCounter(parent) {
+    return newCounterNamed(parent, 'a');
+  }
+
+  function newCounterNamed(parent, name) {
+    var s = new tv.c.trace_model.Counter(parent, name, null, name);
+    return s;
+  }
+
+  function newCounterCategory(parent, category, name) {
+    var s = new tv.c.trace_model.Counter(parent, name, category, name);
+    return s;
+  }
+
+  function newCounterSeries() {
+    var s = new tv.c.trace_model.CounterSeries('a', 0);
+    return s;
+  }
+
+  function newSlice(start, duration) {
+    return newSliceNamed('a', start, duration);
+  }
+
+  function newSliceNamed(name, start, duration) {
+    var s = new tv.c.trace_model.Slice('', name, 0, start, {}, duration);
+    return s;
+  }
+
+  function newSampleNamed(thread, sampleName, category, frameNames, start) {
+    var model;
+    if (thread.parent)
+      model = thread.parent.model;
+    else
+      model = undefined;
+    var sf = newStackTrace(model, category, frameNames);
+    var s = new tv.c.trace_model.Sample(undefined, thread,
+                                        sampleName, start,
+                                        sf,
+                                        1);
+    return s;
+  }
+
+  function newSliceCategory(category, name, start, duration) {
+    var s = new tv.c.trace_model.Slice(
+        category, name, 0, start, {}, duration);
+    return s;
+  }
+
+  function newSliceEx(options) {
+    if (options.start === undefined)
+      throw new Error('Too little info');
+
+    var title = options.title ? options.title : 'a';
+
+    var colorId = options.colorId ? options.colorId : 0;
+
+    var duration;
+    if (options.duration !== undefined) {
+      if (options.end !== undefined) throw new Error('TMI');
+      duration = options.duration;
+    } else if (options.end !== undefined) {
+      if (options.duration !== undefined) throw new Error('TMI');
+      duration = options.end - options.start;
+    } else {
+      throw new Error('Too little info');
+    }
+
+    var cpuStart = options.cpuStart;
+    var cpuDuration;
+    if (options.cpuDuration !== undefined) {
+      if (cpuStart !== undefined) throw new Error('Too little info');
+      if (options.cpuEnd !== undefined) throw new Error('TMI');
+      cpuDuration = options.cpuDuration;
+    } else if (options.cpuEnd !== undefined) {
+      if (cpuStart === undefined) throw new Error('Too little info');
+      if (options.cpuDuration !== undefined) throw new Error('TMI');
+      cpuDuration = options.cpuEnd - cpuStart;
+    }
+
+    var slice = new tv.c.trace_model.Slice(
+        options.cat ? options.cat : 'cat',
+        title,
+        colorId,
+        options.start,
+        options.args ? options.args : {},
+        duration,
+        cpuStart, cpuDuration);
+
+
+    return slice;
+  }
+
+  function newStackTrace(model, category, titles) {
+    var frame = undefined;
+    for (var i = 0; i < titles.length; i++) {
+      frame = new tv.c.trace_model.StackFrame(frame, tv.b.GUID.allocate(),
+                                                 category, titles[i], 7);
+      if (model)
+        model.addStackFrame(frame);
+    }
+    return frame;
+  }
+
+  function findSliceNamed(slices, name) {
+    if (slices instanceof tv.c.trace_model.SliceGroup)
+      slices = slices.slices;
+    for (var i = 0; i < slices.length; i++)
+      if (slices[i].title == name)
+        return slices[i];
+      return undefined;
+  }
+
+  function newModel(customizeModelCallback) {
+    var io = new tv.c.ImportOptions();
+    io.customizeModelCallback = customizeModelCallback;
+    io.shiftWorldToZero = false;
+    return new tv.c.TraceModel([], io);
+  }
+
+  function newModelWithAuditor(customizeModelCallback, auditor) {
+    var io = new tv.c.ImportOptions();
+    io.customizeModelCallback = customizeModelCallback;
+    io.shiftWorldToZero = false;
+    io.auditorConstructors = [auditor];
+    return new tv.c.TraceModel([], io);
+  }
+
+  return {
+    newAsyncSlice: newAsyncSlice,
+    newAsyncSliceNamed: newAsyncSliceNamed,
+    newCounter: newCounter,
+    newCounterNamed: newCounterNamed,
+    newCounterCategory: newCounterCategory,
+    newCounterSeries: newCounterSeries,
+    newSlice: newSlice,
+    newSliceNamed: newSliceNamed,
+    newSliceEx: newSliceEx,
+    newSampleNamed: newSampleNamed,
+    newSliceCategory: newSliceCategory,
+    newStackTrace: newStackTrace,
+    newModel: newModel,
+    newModelWithAuditor: newModelWithAuditor,
+    findSliceNamed: findSliceNamed
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_display_transform.html b/trace-viewer/trace_viewer/core/timeline_display_transform.html
new file mode 100644
index 0000000..316795d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_display_transform.html
@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/utils.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  function TimelineDisplayTransform(opt_that) {
+    if (opt_that) {
+      this.set(opt_that);
+      return;
+    }
+    this.scaleX = 1;
+    this.panX = 0;
+    this.panY = 0;
+  }
+
+  TimelineDisplayTransform.prototype = {
+    set: function(that) {
+      this.scaleX = that.scaleX;
+      this.panX = that.panX;
+      this.panY = that.panY;
+    },
+
+    clone: function() {
+      return new TimelineDisplayTransform(this);
+    },
+
+    equals: function(that) {
+      var eq = true;
+      if (that === undefined || that === null)
+        return false;
+      eq &= this.panX === that.panX;
+      eq &= this.panY === that.panY;
+      eq &= this.scaleX === that.scaleX;
+      return !!eq;
+    },
+
+    almostEquals: function(that) {
+      var eq = true;
+      if (that === undefined || that === null)
+        return false;
+      eq &= Math.abs(this.panX - that.panX) < 0.001;
+      eq &= Math.abs(this.panY - that.panY) < 0.001;
+      eq &= Math.abs(this.scaleX - that.scaleX) < 0.001;
+      return !!eq;
+    },
+
+    incrementPanXInViewUnits: function(xDeltaView) {
+      this.panX += this.xViewVectorToWorld(xDeltaView);
+    },
+
+    xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) {
+      if (typeof viewX == 'string') {
+        if (viewX === 'left') {
+          viewX = 0;
+        } else if (viewX === 'center') {
+          viewX = viewWidth / 2;
+        } else if (viewX === 'right') {
+          viewX = viewWidth - 1;
+        } else {
+          throw new Error('viewX must be left|center|right or number.');
+        }
+      }
+      this.panX = (viewX / this.scaleX) - worldX;
+    },
+
+    xPanWorldBoundsIntoView: function(worldMin, worldMax, viewWidth) {
+      if (this.xWorldToView(worldMin) < 0)
+        this.xPanWorldPosToViewPos(worldMin, 'left', viewWidth);
+      else if (this.xWorldToView(worldMax) > viewWidth)
+        this.xPanWorldPosToViewPos(worldMax, 'right', viewWidth);
+    },
+
+    xSetWorldBounds: function(worldMin, worldMax, viewWidth) {
+      var worldWidth = worldMax - worldMin;
+      var scaleX = viewWidth / worldWidth;
+      var panX = -worldMin;
+      this.setPanAndScale(panX, scaleX);
+    },
+
+    setPanAndScale: function(p, s) {
+      this.scaleX = s;
+      this.panX = p;
+    },
+
+    xWorldToView: function(x) {
+      return (x + this.panX) * this.scaleX;
+    },
+
+    xWorldVectorToView: function(x) {
+      return x * this.scaleX;
+    },
+
+    xViewToWorld: function(x) {
+      return (x / this.scaleX) - this.panX;
+    },
+
+    xViewVectorToWorld: function(x) {
+      return x / this.scaleX;
+    },
+
+    applyTransformToCanvas: function(ctx) {
+      ctx.transform(this.scaleX, 0, 0, 1, this.panX * this.scaleX, 0);
+    }
+  };
+
+  return {
+    TimelineDisplayTransform: TimelineDisplayTransform
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_display_transform_animations.html b/trace-viewer/trace_viewer/core/timeline_display_transform_animations.html
new file mode 100644
index 0000000..0ff4910
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_display_transform_animations.html
@@ -0,0 +1,174 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/ui/animation.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  var kDefaultPanAnimatoinDurationMs = 100.0;
+
+  /**
+   * Pans a TimelineDisplayTransform by a given amount.
+   * @constructor
+   * @extends {tv.b.ui.Animation}
+   * @param {Number} deltaX The total amount of change to the transform's panX.
+   * @param {Number} deltaY The total amount of change to the transform's panY.
+   * @param {Number=} opt_durationMs How long the pan animation should run.
+   * Defaults to kDefaultPanAnimatoinDurationMs.
+   */
+  function TimelineDisplayTransformPanAnimation(
+      deltaX, deltaY, opt_durationMs) {
+    this.deltaX = deltaX;
+    this.deltaY = deltaY;
+    if (opt_durationMs === undefined)
+      this.durationMs = kDefaultPanAnimatoinDurationMs;
+    else
+      this.durationMs = opt_durationMs;
+
+    this.startPanX = undefined;
+    this.startPanY = undefined;
+    this.startTimeMs = undefined;
+  }
+
+  TimelineDisplayTransformPanAnimation.prototype = {
+    __proto__: tv.b.ui.Animation.prototype,
+
+    get affectsPanY() {
+      return this.deltaY !== 0;
+    },
+
+    canTakeOverFor: function(existingAnimation) {
+      return existingAnimation instanceof TimelineDisplayTransformPanAnimation;
+    },
+
+    takeOverFor: function(existing, timestamp, target) {
+      var remainingDeltaXOnExisting = existing.goalPanX - target.panX;
+      var remainingDeltaYOnExisting = existing.goalPanY - target.panY;
+      var remainingTimeOnExisting = timestamp - (
+          existing.startTimeMs + existing.durationMs);
+      remainingTimeOnExisting = Math.max(remainingTimeOnExisting, 0);
+
+      this.deltaX += remainingDeltaXOnExisting;
+      this.deltaY += remainingDeltaYOnExisting;
+      this.durationMs += remainingTimeOnExisting;
+    },
+
+    start: function(timestamp, target) {
+      this.startTimeMs = timestamp;
+      this.startPanX = target.panX;
+      this.startPanY = target.panY;
+    },
+
+    tick: function(timestamp, target) {
+      var percentDone = (timestamp - this.startTimeMs) / this.durationMs;
+      percentDone = tv.b.clamp(percentDone, 0, 1);
+
+      target.panX = tv.b.lerp(percentDone, this.startPanX, this.goalPanX);
+      if (this.affectsPanY)
+        target.panY = tv.b.lerp(percentDone, this.startPanY, this.goalPanY);
+      return timestamp >= this.startTimeMs + this.durationMs;
+    },
+
+    get goalPanX() {
+      return this.startPanX + this.deltaX;
+    },
+
+    get goalPanY() {
+      return this.startPanY + this.deltaY;
+    }
+  };
+
+  /**
+   * Zooms in/out on a specified location in the world.
+   *
+   * Zooming in and out is all about keeping the area under the mouse cursor,
+   * here called the "focal point" in the same place under the zoom. If one
+   * simply changes the scale, the area under the mouse cursor will change. To
+   * keep the focal point from moving during the zoom, the pan needs to change
+   * in order to compensate. Thus, a ZoomTo animation is given both a focal
+   * point in addition to the amount by which to zoom.
+   *
+   * @constructor
+   * @extends {tv.b.ui.Animation}
+   * @param {Number} goalFocalPointXWorld The X coordinate in the world which is
+   * of interest.
+   * @param {Number} goalFocalPointXView Where on the screen the
+   * goalFocalPointXWorld should stay centered during the zoom.
+   * @param {Number} goalFocalPointY Where the panY should be when the zoom
+   * completes.
+   * @param {Number} zoomInRatioX The ratio of the current scaleX to the goal
+   * scaleX.
+   */
+  function TimelineDisplayTransformZoomToAnimation(
+      goalFocalPointXWorld,
+      goalFocalPointXView,
+      goalFocalPointY,
+      zoomInRatioX,
+      opt_durationMs) {
+    this.goalFocalPointXWorld = goalFocalPointXWorld;
+    this.goalFocalPointXView = goalFocalPointXView;
+    this.goalFocalPointY = goalFocalPointY;
+    this.zoomInRatioX = zoomInRatioX;
+    if (opt_durationMs === undefined)
+      this.durationMs = kDefaultPanAnimatoinDurationMs;
+    else
+      this.durationMs = opt_durationMs;
+
+    this.startTimeMs = undefined;
+    this.startScaleX = undefined;
+    this.goalScaleX = undefined;
+    this.startPanY = undefined;
+  }
+
+  TimelineDisplayTransformZoomToAnimation.prototype = {
+    __proto__: tv.b.ui.Animation.prototype,
+
+    get affectsPanY() {
+      return this.startPanY != this.goalFocalPointY;
+    },
+
+    canTakeOverFor: function(existingAnimation) {
+      return false;
+    },
+
+    takeOverFor: function(existingAnimation, timestamp, target) {
+      this.goalScaleX = target.scaleX * this.zoomInRatioX;
+    },
+
+    start: function(timestamp, target) {
+      this.startTimeMs = timestamp;
+      this.startScaleX = target.scaleX;
+      this.goalScaleX = this.zoomInRatioX * target.scaleX;
+      this.startPanY = target.panY;
+    },
+
+    tick: function(timestamp, target) {
+      var percentDone = (timestamp - this.startTimeMs) / this.durationMs;
+      percentDone = tv.b.clamp(percentDone, 0, 1);
+
+      target.scaleX = tv.b.lerp(percentDone, this.startScaleX, this.goalScaleX);
+      if (this.affectsPanY) {
+        target.panY = tv.b.lerp(
+            percentDone, this.startPanY, this.goalFocalPointY);
+      }
+
+      target.xPanWorldPosToViewPos(
+          this.goalFocalPointXWorld, this.goalFocalPointXView);
+      return timestamp >= this.startTimeMs + this.durationMs;
+    }
+  };
+
+  return {
+    TimelineDisplayTransformPanAnimation:
+        TimelineDisplayTransformPanAnimation,
+    TimelineDisplayTransformZoomToAnimation:
+        TimelineDisplayTransformZoomToAnimation
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_display_transform_animations_test.html b/trace-viewer/trace_viewer/core/timeline_display_transform_animations_test.html
new file mode 100644
index 0000000..5b84a49
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_display_transform_animations_test.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/timeline_display_transform.html">
+<link rel="import" href="/core/timeline_display_transform_animations.html">
+<link rel="import" href="/base/ui/animation_controller.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var TimelineDisplayTransform = tv.c.TimelineDisplayTransform;
+  var TimelineDisplayTransformPanAnimation =
+      tv.c.TimelineDisplayTransformPanAnimation;
+  var TimelineDisplayTransformZoomToAnimation =
+      tv.c.TimelineDisplayTransformZoomToAnimation;
+
+  test('panBasic', function() {
+    var target = new TimelineDisplayTransform();
+    target.cloneAnimationState = function() {
+      return this.clone();
+    };
+
+    var a = new TimelineDisplayTransformPanAnimation(10, 20, 100);
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+    controller.queueAnimation(a, 0);
+
+    assert.isTrue(a.affectsPanY);
+    tv.b.forcePendingRAFTasksToRun(50);
+    assert.isAbove(target.panX, 0);
+    tv.b.forcePendingRAFTasksToRun(100);
+    assert.isFalse(controller.hasActiveAnimation);
+    assert.equal(target.panX, 10);
+    assert.equal(target.panY, 20);
+  });
+
+  test('zoomBasic', function() {
+    var target = new TimelineDisplayTransform();
+    target.panY = 30;
+    target.cloneAnimationState = function() {
+      return this.clone();
+    };
+
+    var a = new TimelineDisplayTransformZoomToAnimation(10, 20, 30, 5, 100);
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+    controller.queueAnimation(a, 0);
+
+    assert.isFalse(a.affectsPanY);
+    tv.b.forcePendingRAFTasksToRun(100);
+    assert.equal(target.scaleX, 5);
+  });
+
+  test('panTakeover', function() {
+    var target = new TimelineDisplayTransform();
+    target.cloneAnimationState = function() {
+      return this.clone();
+    };
+
+    var b = new TimelineDisplayTransformPanAnimation(10, 0, 100);
+    var a = new TimelineDisplayTransformPanAnimation(10, 0, 100);
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+    controller.queueAnimation(a, 0);
+
+    tv.b.forcePendingRAFTasksToRun(50);
+    controller.queueAnimation(b, 50);
+
+    tv.b.forcePendingRAFTasksToRun(100);
+    assert.isTrue(controller.hasActiveAnimation);
+
+    tv.b.forcePendingRAFTasksToRun(150);
+    assert.isFalse(controller.hasActiveAnimation);
+    assert.equal(target.panX, 20);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_display_transform_test.html b/trace-viewer/trace_viewer/core/timeline_display_transform_test.html
new file mode 100644
index 0000000..ab29107
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_display_transform_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/timeline_display_transform.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var TimelineDisplayTransform = tv.c.TimelineDisplayTransform;
+
+  test('basics', function() {
+    var a = new TimelineDisplayTransform();
+    a.panX = 0;
+    a.panY = 0;
+    a.scaleX = 1;
+
+    var b = new TimelineDisplayTransform();
+    b.panX = 10;
+    b.panY = 0;
+    b.scaleX = 1;
+
+    assert.isFalse(a.equals(b));
+    assert.isFalse(a.almostEquals(b));
+
+    var c = b.clone();
+    assert.isTrue(b.equals(c));
+    assert.isTrue(b.almostEquals(c));
+
+    c.set(a);
+    assert.isTrue(a.equals(c));
+    assert.isTrue(a.almostEquals(c));
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/timeline_interest_range.html b/trace-viewer/trace_viewer/core/timeline_interest_range.html
new file mode 100644
index 0000000..fe33453
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_interest_range.html
@@ -0,0 +1,248 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/range.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  /**
+   * @constructor
+   */
+  function SnapIndicator(y, height) {
+    this.y = y;
+    this.height = height;
+  }
+
+  /**
+   * The interesting part of the world.
+   *
+   * @constructor
+   */
+  function TimelineInterestRange(vp) {
+    this.viewport_ = vp;
+
+    this.range_ = new tv.b.Range();
+
+    this.leftSelected_ = false;
+    this.rightSelected_ = false;
+
+    this.leftSnapIndicator_ = undefined;
+    this.rightSnapIndicator_ = undefined;
+  }
+
+  TimelineInterestRange.prototype = {
+    get isEmpty() {
+      return this.range_.isEmpty;
+    },
+
+    reset: function() {
+      this.range_.reset();
+      this.leftSelected_ = false;
+      this.rightSelected_ = false;
+      this.leftSnapIndicator_ = undefined;
+      this.rightSnapIndicator_ = undefined;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    get min() {
+      return this.range_.min;
+    },
+
+    set min(min) {
+      this.range_.min = min;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    get max() {
+      return this.range_.max;
+    },
+
+    set max(max) {
+      this.range_.max = max;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    set: function(range) {
+      this.range_.reset();
+      this.range_.addRange(range);
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    setMinAndMax: function(min, max) {
+      this.range_.min = min;
+      this.range_.max = max;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    get range() {
+      return this.range_.range;
+    },
+
+    asRangeObject: function() {
+      var range = new tv.b.Range();
+      range.addRange(this.range_);
+      return range;
+    },
+
+    get leftSelected() {
+      return this.leftSelected_;
+    },
+
+    set leftSelected(leftSelected) {
+      if (this.leftSelected_ == leftSelected)
+        return;
+      this.leftSelected_ = leftSelected;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    get rightSelected() {
+      return this.rightSelected_;
+    },
+
+    set rightSelected(rightSelected) {
+      if (this.rightSelected_ == rightSelected)
+        return;
+      this.rightSelected_ = rightSelected;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    get leftSnapIndicator() {
+      return this.leftSnapIndicator_;
+    },
+
+    set leftSnapIndicator(leftSnapIndicator) {
+      this.leftSnapIndicator_ = leftSnapIndicator;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    get rightSnapIndicator() {
+      return this.rightSnapIndicator_;
+    },
+
+    set rightSnapIndicator(rightSnapIndicator) {
+      this.rightSnapIndicator_ = rightSnapIndicator;
+      this.viewport_.dispatchChangeEvent();
+    },
+
+    draw: function(ctx, viewLWorld, viewRWorld) {
+      if (this.range_.isEmpty)
+        return;
+      var dt = this.viewport_.currentDisplayTransform;
+
+      var markerLWorld = this.min;
+      var markerRWorld = this.max;
+
+      var markerLView = Math.round(dt.xWorldToView(markerLWorld));
+      var markerRView = Math.round(dt.xWorldToView(markerRWorld));
+
+      ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+      if (markerLWorld > viewLWorld) {
+        ctx.fillRect(dt.xWorldToView(viewLWorld), 0,
+            markerLView, ctx.canvas.height);
+      }
+
+      if (markerRWorld < viewRWorld) {
+        ctx.fillRect(markerRView, 0,
+            dt.xWorldToView(viewRWorld), ctx.canvas.height);
+      }
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      ctx.lineWidth = Math.round(pixelRatio);
+      if (this.range_.range > 0) {
+        this.drawLine_(ctx, viewLWorld, viewRWorld,
+                       ctx.canvas.height, this.min, this.leftSelected_);
+        this.drawLine_(ctx, viewLWorld, viewRWorld,
+                       ctx.canvas.height, this.max, this.rightSelected_);
+      } else {
+        this.drawLine_(ctx, viewLWorld, viewRWorld,
+                       ctx.canvas.height, this.min,
+                       this.leftSelected_ || this.rightSelected_);
+      }
+      ctx.lineWidth = 1;
+    },
+
+    drawLine_: function(ctx, viewLWorld, viewRWorld, height, ts, selected) {
+      if (ts < viewLWorld || ts >= viewRWorld)
+        return;
+
+      var dt = this.viewport_.currentDisplayTransform;
+      var viewX = Math.round(dt.xWorldToView(ts));
+
+      // Apply subpixel translate to get crisp lines.
+      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+      ctx.save();
+      ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+      ctx.beginPath();
+      tv.c.drawLine(ctx, viewX, 0, viewX, height);
+      if (selected)
+        ctx.strokeStyle = 'rgb(255, 0, 0)';
+      else
+        ctx.strokeStyle = 'rgb(0, 0, 0)';
+      ctx.stroke();
+
+      ctx.restore();
+    },
+
+    drawIndicators: function(ctx, viewLWorld, viewRWorld) {
+      if (this.leftSnapIndicator_) {
+        this.drawIndicator_(ctx, viewLWorld, viewRWorld,
+                            this.range_.min,
+                            this.leftSnapIndicator_,
+                            this.leftSelected_);
+      }
+      if (this.rightSnapIndicator_) {
+        this.drawIndicator_(ctx, viewLWorld, viewRWorld,
+                            this.range_.max,
+                            this.rightSnapIndicator_,
+                            this.rightSelected_);
+      }
+    },
+
+    drawIndicator_: function(ctx, viewLWorld, viewRWorld,
+                             xWorld, si, selected) {
+      var dt = this.viewport_.currentDisplayTransform;
+
+      var viewX = Math.round(dt.xWorldToView(xWorld));
+
+      // Apply subpixel translate to get crisp lines.
+      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+      ctx.save();
+      ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var viewY = si.y * devicePixelRatio;
+      var viewHeight = si.height * devicePixelRatio;
+      var arrowSize = 4 * pixelRatio;
+
+      if (selected)
+        ctx.fillStyle = 'rgb(255, 0, 0)';
+      else
+        ctx.fillStyle = 'rgb(0, 0, 0)';
+      tv.c.drawTriangle(ctx,
+          viewX - arrowSize * 0.75, viewY,
+          viewX + arrowSize * 0.75, viewY,
+          viewX, viewY + arrowSize);
+      ctx.fill();
+      tv.c.drawTriangle(ctx,
+          viewX - arrowSize * 0.75, viewY + viewHeight,
+          viewX + arrowSize * 0.75, viewY + viewHeight,
+          viewX, viewY + viewHeight - arrowSize);
+      ctx.fill();
+
+      ctx.restore();
+    }
+  };
+
+  return {
+    SnapIndicator: SnapIndicator,
+    TimelineInterestRange: TimelineInterestRange
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_track_view.css b/trace-viewer/trace_viewer/core/timeline_track_view.css
new file mode 100644
index 0000000..64ebdc4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_track_view.css
@@ -0,0 +1,38 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.timeline-track-view * {
+  -webkit-user-select: none;
+  cursor: default;
+}
+
+.timeline-track-view .tool-button {
+  cursor: pointer;
+}
+
+.timeline-track-view {
+  -webkit-box-orient: vertical;
+  display: -webkit-box;
+  position: relative;
+}
+
+.model-track-container {
+  -webkit-box-flex: 1;
+  overflow: auto;
+}
+
+.drag-box {
+  background-color: rgba(0, 0, 255, 0.25);
+  border: 1px solid rgb(0, 0, 96);
+  font-size: 75%;
+  position: fixed;
+}
+
+.timeline-track-view > .hint-text {
+  position: absolute;
+  bottom: 6px;
+  right: 6px;
+  font-size: 8pt;
+}
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/timeline_track_view.html b/trace-viewer/trace_viewer/core/timeline_track_view.html
new file mode 100644
index 0000000..c425780
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_track_view.html
@@ -0,0 +1,1170 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/base/ui/common.css">
+<link rel="stylesheet" href="/core/timeline_track_view.css">
+
+<link rel="import" href="/base/events.html">
+<link rel="import" href="/base/properties.html">
+<link rel="import" href="/base/settings.html">
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/mouse_mode_selector.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/timeline_display_transform_animations.html">
+<link rel="import" href="/core/timing_tool.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/trace_model/x_marker_annotation.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+<link rel="import" href="/core/tracks/trace_model_track.html">
+<link rel="import" href="/core/tracks/ruler_track.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Interactive visualizaiton of TraceModel objects
+ * based loosely on gantt charts. Each thread in the TraceModel is given a
+ * set of Tracks, one per subrow in the thread. The TimelineTrackView class
+ * acts as a controller, creating the individual tracks, while Tracks
+ * do actual drawing.
+ *
+ * Visually, the TimelineTrackView produces (prettier) visualizations like the
+ * following:
+ *    Thread1:  AAAAAAAAAA         AAAAA
+ *                  BBBB              BB
+ *    Thread2:     CCCCCC                 CCCCC
+ *
+ */
+tv.exportTo('tv.c', function() {
+  var Selection = tv.c.Selection;
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var Viewport = tv.c.TimelineViewport;
+
+  var tempDisplayTransform = new tv.c.TimelineDisplayTransform();
+
+  function intersectRect_(r1, r2) {
+    var results = new Object;
+    if (r2.left > r1.right || r2.right < r1.left ||
+        r2.top > r1.bottom || r2.bottom < r1.top) {
+      return false;
+    }
+    results.left = Math.max(r1.left, r2.left);
+    results.top = Math.max(r1.top, r2.top);
+    results.right = Math.min(r1.right, r2.right);
+    results.bottom = Math.min(r1.bottom, r2.bottom);
+    results.width = (results.right - results.left);
+    results.height = (results.bottom - results.top);
+    return results;
+  }
+
+  /**
+   * Renders a TraceModel into a div element, making one
+   * Track for each subrow in each thread of the model, managing
+   * overall track layout, and handling user interaction with the
+   * viewport.
+   *
+   * @constructor
+   * @extends {HTMLDivElement}
+   */
+  var TimelineTrackView = tv.b.ui.define('div');
+
+  TimelineTrackView.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    model_: null,
+
+    decorate: function() {
+
+      this.classList.add('timeline-track-view');
+
+      this.viewport_ = new Viewport(this);
+      this.viewportDisplayTransformAtMouseDown_ = null;
+
+      this.rulerTrackContainer_ =
+          new tv.c.tracks.DrawingContainer(this.viewport_);
+      this.appendChild(this.rulerTrackContainer_);
+      this.rulerTrackContainer_.invalidate();
+
+      this.rulerTrack_ = new tv.c.tracks.RulerTrack(this.viewport_);
+      this.rulerTrackContainer_.appendChild(this.rulerTrack_);
+
+      this.upperModelTrack_ = new tv.c.tracks.TraceModelTrack(this.viewport_);
+      this.upperModelTrack_.upperMode = true;
+      this.rulerTrackContainer_.appendChild(this.upperModelTrack_);
+
+      this.modelTrackContainer_ =
+          new tv.c.tracks.DrawingContainer(this.viewport_);
+      this.appendChild(this.modelTrackContainer_);
+      this.modelTrackContainer_.style.display = 'block';
+      this.modelTrackContainer_.invalidate();
+
+      this.viewport_.modelTrackContainer = this.modelTrackContainer_;
+
+      this.modelTrack_ = new tv.c.tracks.TraceModelTrack(this.viewport_);
+      this.modelTrackContainer_.appendChild(this.modelTrack_);
+
+      this.timingTool_ = new tv.c.TimingTool(this.viewport_,
+                                                this);
+
+      this.initMouseModeSelector();
+
+      this.dragBox_ = this.ownerDocument.createElement('div');
+      this.dragBox_.className = 'drag-box';
+      this.appendChild(this.dragBox_);
+      this.hideDragBox_();
+
+      this.initHintText_();
+
+      this.bindEventListener_(document, 'keypress', this.onKeypress_, this);
+      this.bindEventListener_(document, 'keydown', this.onKeydown_, this);
+      this.bindEventListener_(document, 'keyup', this.onKeyup_, this);
+
+      this.bindEventListener_(this, 'dblclick', this.onDblClick_, this);
+      this.bindEventListener_(this, 'mousewheel', this.onMouseWheel_, this);
+
+      this.addEventListener('mousemove', this.onMouseMove_);
+
+      this.addEventListener('touchstart', this.onTouchStart_);
+      this.addEventListener('touchmove', this.onTouchMove_);
+      this.addEventListener('touchend', this.onTouchEnd_);
+
+      this.mouseViewPosAtMouseDown_ = {x: 0, y: 0};
+      this.lastMouseViewPos_ = {x: 0, y: 0};
+
+      this.lastTouchViewPositions_ = [];
+
+      this.selection_ = new Selection();
+      this.highlight_ = new Selection();
+
+      this.isPanningAndScanning_ = false;
+      this.isZooming_ = false;
+    },
+
+    /**
+     * Wraps the standard addEventListener but automatically binds the provided
+     * func to the provided target, tracking the resulting closure. When detach
+     * is called, these listeners will be automatically removed.
+     */
+    bindEventListener_: function(object, event, func, target) {
+      if (!this.boundListeners_)
+        this.boundListeners_ = [];
+      var boundFunc = func.bind(target);
+      this.boundListeners_.push({object: object,
+        event: event,
+        boundFunc: boundFunc});
+      object.addEventListener(event, boundFunc);
+    },
+
+    initMouseModeSelector: function() {
+      this.mouseModeSelector_ = new tv.b.ui.MouseModeSelector(this);
+      this.appendChild(this.mouseModeSelector_);
+
+      this.mouseModeSelector_.addEventListener('beginpan',
+          this.onBeginPanScan_.bind(this));
+      this.mouseModeSelector_.addEventListener('updatepan',
+          this.onUpdatePanScan_.bind(this));
+      this.mouseModeSelector_.addEventListener('endpan',
+          this.onEndPanScan_.bind(this));
+
+      this.mouseModeSelector_.addEventListener('beginselection',
+          this.onBeginSelection_.bind(this));
+      this.mouseModeSelector_.addEventListener('updateselection',
+          this.onUpdateSelection_.bind(this));
+      this.mouseModeSelector_.addEventListener('endselection',
+          this.onEndSelection_.bind(this));
+
+      this.mouseModeSelector_.addEventListener('beginzoom',
+          this.onBeginZoom_.bind(this));
+      this.mouseModeSelector_.addEventListener('updatezoom',
+          this.onUpdateZoom_.bind(this));
+      this.mouseModeSelector_.addEventListener('endzoom',
+          this.onEndZoom_.bind(this));
+
+      this.mouseModeSelector_.addEventListener('entertiming',
+          this.timingTool_.onEnterTiming.bind(this.timingTool_));
+      this.mouseModeSelector_.addEventListener('begintiming',
+          this.timingTool_.onBeginTiming.bind(this.timingTool_));
+      this.mouseModeSelector_.addEventListener('updatetiming',
+          this.timingTool_.onUpdateTiming.bind(this.timingTool_));
+      this.mouseModeSelector_.addEventListener('endtiming',
+          this.timingTool_.onEndTiming.bind(this.timingTool_));
+      this.mouseModeSelector_.addEventListener('exittiming',
+          this.timingTool_.onExitTiming.bind(this.timingTool_));
+
+      var m = tv.b.ui.MOUSE_SELECTOR_MODE;
+      this.mouseModeSelector_.supportedModeMask =
+          m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING;
+      this.mouseModeSelector_.settingsKey =
+          'timelineTrackView.mouseModeSelector';
+      this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0));
+      this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0));
+      this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0));
+      this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0));
+
+      this.mouseModeSelector_.setKeyCodeCondition(function() {
+        // Return false when FindControl is active so that MouseMode keyboard
+        // shortcuts are ignored.
+        return this.listenToKeys_;
+      }.bind(this));
+
+      this.mouseModeSelector_.setModifierForAlternateMode(
+          m.SELECTION, tv.b.ui.MODIFIER.SHIFT);
+      this.mouseModeSelector_.setModifierForAlternateMode(
+          m.PANSCAN, tv.b.ui.MODIFIER.SPACE);
+      this.mouseModeSelector_.setModifierForAlternateMode(
+          m.ZOOM, tv.b.ui.MODIFIER.CMD_OR_CTRL);
+    },
+
+    detach: function() {
+      this.modelTrack_.detach();
+      this.upperModelTrack_.detach();
+
+      for (var i = 0; i < this.boundListeners_.length; i++) {
+        var binding = this.boundListeners_[i];
+        binding.object.removeEventListener(binding.event, binding.boundFunc);
+      }
+      this.boundListeners_ = undefined;
+      this.viewport_.detach();
+    },
+
+    get viewport() {
+      return this.viewport_;
+    },
+
+    get model() {
+      return this.model_;
+    },
+
+    set model(model) {
+      if (!model)
+        throw new Error('Model cannot be null');
+
+      var modelInstanceChanged = this.model_ !== model;
+      this.model_ = model;
+      this.modelTrack_.model = model;
+      this.upperModelTrack_.model = model;
+
+      // Set up a reasonable viewport.
+      if (modelInstanceChanged)
+        this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this));
+
+      tv.b.setPropertyAndDispatchChange(this, 'model', model);
+    },
+
+    get hasVisibleContent() {
+      return this.modelTrack_.hasVisibleContent ||
+          this.upperModelTrack_.hasVisibleContent;
+    },
+
+    setInitialViewport_: function() {
+      // We need the canvas size to be up-to-date at this point. We maybe in
+      // here before the raf fires, so the size may have not been updated since
+      // the canvas was resized.
+      this.modelTrackContainer_.updateCanvasSizeIfNeeded_();
+      var w = this.modelTrackContainer_.canvas.width;
+
+      var min;
+      var range;
+
+      if (this.model_.bounds.isEmpty) {
+        min = 0;
+        range = 1000;
+      } else if (this.model_.bounds.range === 0) {
+        min = this.model_.bounds.min;
+        range = 1000;
+      } else {
+        min = this.model_.bounds.min;
+        range = this.model_.bounds.range;
+      }
+
+      var boost = range * 0.15;
+      tempDisplayTransform.set(this.viewport_.currentDisplayTransform);
+      tempDisplayTransform.xSetWorldBounds(min - boost,
+                                           min + range + boost,
+                                           w);
+      this.viewport_.setDisplayTransformImmediately(tempDisplayTransform);
+    },
+
+    /**
+     * @param {Filter} filter The filter to use for finding matches.
+     * @param {Selection} selection The selection to add matches to.
+     * @return {Task} which performs the filtering.
+     */
+    addAllObjectsMatchingFilterToSelectionAsTask: function(filter, selection) {
+      return this.modelTrack_.addAllObjectsMatchingFilterToSelectionAsTask(
+          filter, selection);
+      this.upperModelTrack_.addAllObjectsMatchingFilterToSelection(
+          filter, selection);
+    },
+
+    /**
+     * @return {Element} The element whose focused state determines
+     * whether to respond to keyboard inputs.
+     * Defaults to the parent element.
+     */
+    get focusElement() {
+      if (this.focusElement_)
+        return this.focusElement_;
+      return this.parentElement;
+    },
+
+    /**
+     * Sets the element whose focus state will determine whether
+     * to respond to keyboard input.
+     */
+    set focusElement(value) {
+      this.focusElement_ = value;
+    },
+
+    get listenToKeys_() {
+      if (!this.viewport_.isAttachedToDocumentOrInTestMode)
+        return false;
+      if (document.activeElement instanceof TracingFindControl)
+        return false;
+      if (document.activeElement instanceof TracingScriptingControl)
+        return false;
+      if (!this.focusElement_)
+        return true;
+      if (this.focusElement.tabIndex >= 0) {
+        if (document.activeElement == this.focusElement)
+          return true;
+        return tv.b.ui.elementIsChildOf(document.activeElement,
+                                        this.focusElement);
+      }
+      return true;
+    },
+
+    onMouseMove_: function(e) {
+
+      // Zooming requires the delta since the last mousemove so we need to avoid
+      // tracking it when the zoom interaction is active.
+      if (this.isZooming_)
+        return;
+
+      this.storeLastMousePos_(e);
+    },
+
+    onTouchStart_: function(e) {
+      this.storeLastTouchPositions_(e);
+      this.focusElements_();
+    },
+
+    onTouchMove_: function(e) {
+      e.preventDefault();
+      this.onUpdateTransformForTouch_(e);
+    },
+
+    onTouchEnd_: function(e) {
+      this.storeLastTouchPositions_(e);
+      this.focusElements_();
+    },
+
+    onKeypress_: function(e) {
+      var vp = this.viewport_;
+      if (!this.listenToKeys_)
+        return;
+      if (document.activeElement.nodeName == 'INPUT')
+        return;
+      var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
+      var curMouseV, curCenterW;
+      switch (e.keyCode) {
+
+        case 119:  // w
+        case 44:   // ,
+          this.zoomBy_(1.5, true);
+          break;
+        case 115:  // s
+        case 111:  // o
+          this.zoomBy_(1 / 1.5, true);
+          break;
+        case 103:  // g
+          this.onGridToggle_(true);
+          break;
+        case 71:  // G
+          this.onGridToggle_(false);
+          break;
+        case 87:  // W
+        case 60:  // <
+          this.zoomBy_(10, true);
+          break;
+        case 83:  // S
+        case 79:  // O
+          this.zoomBy_(1 / 10, true);
+          break;
+        case 97:  // a
+          this.queueSmoothPan_(viewWidth * 0.3, 0);
+          break;
+        case 100:  // d
+        case 101:  // e
+          this.queueSmoothPan_(viewWidth * -0.3, 0);
+          break;
+        case 65:  // A
+          this.queueSmoothPan_(viewWidth * 0.5, 0);
+          break;
+        case 68:  // D
+          this.queueSmoothPan_(viewWidth * -0.5, 0);
+          break;
+        case 48:  // 0
+          this.setInitialViewport_();
+          break;
+        case 102:  // f
+          this.zoomToSelection();
+          break;
+        case 'm'.charCodeAt(0):
+          this.setCurrentSelectionAsInterestRange_();
+          break;
+        case 104:  // h
+          this.toggleHighDetails_();
+          break;
+      }
+    },
+
+    // Not all keys send a keypress.
+    onKeydown_: function(e) {
+      if (!this.listenToKeys_)
+        return;
+      var sel;
+      var vp = this.viewport_;
+      var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
+
+      switch (e.keyCode) {
+        case 37:   // left arrow
+          sel = this.selection.getShiftedSelection(
+              this.viewport, -1);
+
+          if (sel) {
+            this.setSelectionAndClearHighlight(sel);
+            this.panToSelection();
+            e.preventDefault();
+          } else {
+            this.queueSmoothPan_(viewWidth * 0.3, 0);
+          }
+          break;
+        case 39:   // right arrow
+          sel = this.selection.getShiftedSelection(
+              this.viewport, 1);
+          if (sel) {
+            this.setSelectionAndClearHighlight(sel);
+            this.panToSelection();
+            e.preventDefault();
+          } else {
+            this.queueSmoothPan_(-viewWidth * 0.3, 0);
+          }
+          break;
+        case 9:    // TAB
+          if (this.focusElement.tabIndex == -1) {
+            if (e.shiftKey)
+              this.selectPrevious_(e);
+            else
+              this.selectNext_(e);
+            e.preventDefault();
+          }
+          break;
+      }
+    },
+
+    onKeyup_: function(e) {
+      if (!this.listenToKeys_)
+        return;
+      if (!e.shiftKey) {
+        if (this.dragBeginEvent_) {
+          this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
+              this.dragBoxXEnd_, this.dragBoxYEnd_);
+        }
+      }
+
+    },
+
+    onDblClick_: function(e) {
+      if (this.mouseModeSelector_.mode !==
+          tv.b.ui.MOUSE_SELECTOR_MODE.SELECTION)
+        return;
+
+      if (!this.selection.length || !this.selection[0].title)
+        return;
+
+      var selection = new Selection();
+      var filter = new tv.c.ExactTitleFilter(this.selection[0].title);
+      this.modelTrack_.addAllObjectsMatchingFilterToSelection(filter,
+                                                              selection);
+
+      this.setSelectionAndClearHighlight(selection);
+    },
+
+    onMouseWheel_: function(e) {
+      if (!e.altKey)
+        return;
+
+      var delta = e.wheelDelta / 120;
+      var zoomScale = Math.pow(1.5, delta);
+      this.zoomBy_(zoomScale);
+      e.preventDefault();
+    },
+
+    queueSmoothPan_: function(viewDeltaX, deltaY) {
+      var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld(
+          viewDeltaX);
+      var animation = new tv.c.TimelineDisplayTransformPanAnimation(
+          deltaX, deltaY);
+      this.viewport_.queueDisplayTransformAnimation(animation);
+    },
+
+    /**
+     * Zoom in or out on the timeline by the given scale factor.
+     * @param {Number} scale The scale factor to apply.  If <1, zooms out.
+     * @param {boolean} Whether to change the zoom level smoothly.
+     */
+    zoomBy_: function(scale, smooth) {
+      if (scale <= 0) {
+        return;
+      }
+
+      smooth = !!smooth;
+      var vp = this.viewport_;
+      var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio;
+      var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld(
+          goalFocalPointXView);
+      if (smooth) {
+        var animation = new tv.c.TimelineDisplayTransformZoomToAnimation(
+            goalFocalPointXWorld, goalFocalPointXView,
+            vp.currentDisplayTransform.panY,
+            scale);
+        vp.queueDisplayTransformAnimation(animation);
+      } else {
+        tempDisplayTransform.set(vp.currentDisplayTransform);
+        tempDisplayTransform.scaleX = tempDisplayTransform.scaleX * scale;
+        tempDisplayTransform.xPanWorldPosToViewPos(
+            goalFocalPointXWorld, goalFocalPointXView, viewWidth);
+        vp.setDisplayTransformImmediately(tempDisplayTransform);
+      }
+    },
+
+    /**
+     * Zoom into the current selection.
+     */
+    zoomToSelection: function() {
+      if (!this.selectionOfInterest.length)
+        return;
+
+      var bounds = this.selectionOfInterest.bounds;
+      if (!bounds.range)
+        return;
+
+      var worldCenter = bounds.center;
+      var viewCenter = this.modelTrackContainer_.canvas.width / 2;
+      var adjustedWorldRange = bounds.range * 1.25;
+      var newScale = this.modelTrackContainer_.canvas.width /
+          adjustedWorldRange;
+      var zoomInRatio = newScale /
+          this.viewport_.currentDisplayTransform.scaleX;
+
+      var animation = new tv.c.TimelineDisplayTransformZoomToAnimation(
+          worldCenter, viewCenter,
+          this.viewport_.currentDisplayTransform.panY,
+          zoomInRatio);
+      this.viewport_.queueDisplayTransformAnimation(animation);
+    },
+
+    /**
+     * Pan the view so the current selection becomes visible.
+     */
+    panToSelection: function() {
+      if (!this.selectionOfInterest.length)
+        return;
+
+      var bounds = this.selectionOfInterest.bounds;
+      var worldCenter = bounds.center;
+      var viewWidth = this.modelTrackContainer_.canvas.width;
+
+      var dt = this.viewport_.currentDisplayTransform;
+      if (false && !bounds.range) {
+        if (dt.xWorldToView(bounds.center) < 0 ||
+            dt.xWorldToView(bounds.center) > viewWidth) {
+          tempDisplayTransform.set(dt);
+          tempDisplayTransform.xPanWorldPosToViewPos(
+              worldCenter, 'center', viewWidth);
+          var deltaX = tempDisplayTransform.panX - dt.panX;
+          var animation = new tv.c.TimelineDisplayTransformPanAnimation(
+              deltaX, 0);
+          this.viewport_.queueDisplayTransformAnimation(animation);
+        }
+        return;
+      }
+
+      tempDisplayTransform.set(dt);
+      tempDisplayTransform.xPanWorldBoundsIntoView(
+          bounds.min,
+          bounds.max,
+          viewWidth);
+      var deltaX = tempDisplayTransform.panX - dt.panX;
+      var animation = new tv.c.TimelineDisplayTransformPanAnimation(
+          deltaX, 0);
+      this.viewport_.queueDisplayTransformAnimation(animation);
+    },
+
+    navToPosition: function(uiState) {
+      var location = uiState.location;
+      var scaleX = uiState.scaleX;
+      var track = location.getContainingTrack(this.viewport_);
+
+      var worldCenter = location.xWorld;
+      var viewCenter = this.modelTrackContainer_.canvas.width / 5;
+      var zoomInRatio = scaleX /
+          this.viewport_.currentDisplayTransform.scaleX;
+
+      // Vertically scroll so track is in view.
+      track.scrollIntoViewIfNeeded();
+
+      // Perform zoom and panX animation.
+      var animation = new tv.c.TimelineDisplayTransformZoomToAnimation(
+          worldCenter, viewCenter,
+          this.viewport_.currentDisplayTransform.panY,
+          zoomInRatio);
+      this.viewport_.queueDisplayTransformAnimation(animation);
+
+      // Add an X Marker Annotation at the specified timestamp.
+      if (this.xNavStringMarker_)
+        this.model.removeAnnotation(this.xNavStringMarker_);
+      this.xNavStringMarker_ =
+          new tv.c.trace_model.XMarkerAnnotation(worldCenter);
+      this.model.addAnnotation(this.xNavStringMarker_);
+    },
+
+    removeXNavStringMarker: function() {
+      if (!this.xNavStringMarker_)
+        return;
+      this.model.removeAnnotation(this.xNavStringMarker_);
+    },
+
+    setCurrentSelectionAsInterestRange_: function() {
+      var selectionBounds = this.selection.bounds;
+      if (selectionBounds.empty) {
+        this.viewport_.interestRange.reset();
+        return;
+      }
+
+      if (this.viewport_.interestRange.min == selectionBounds.min &&
+          this.viewport_.interestRange.max == selectionBounds.max)
+        this.viewport_.interestRange.reset();
+      else
+        this.viewport_.interestRange.set(selectionBounds);
+    },
+
+    toggleHighDetails_: function() {
+      this.viewport_.highDetails = !this.viewport_.highDetails;
+    },
+
+    /**
+     * Sets the selected events and changes the SelectionState of the events to
+     *   SELECTED.
+     * @param {Selection} selection A Selection of the new selected events.
+     */
+    set selection(selection) {
+      this.setSelectionAndHighlight(selection, this.highlight_);
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    /**
+     * Sets the highlighted events and changes the SelectionState of the events
+     *   to HIGHLIGHTED. All other events are set to DIMMED, except SELECTED
+     *   ones.
+     * @param {Selection} selection A Selection of the new selected events.
+     */
+    set highlight(highlight) {
+      this.setSelectionAndHighlight(this.selection_, highlight);
+    },
+
+    get highlight() {
+      return this.highlight_;
+    },
+
+    /**
+     * Getter for events of interest, primarily SELECTED and secondarily
+     *   HIGHLIGHTED events.
+     */
+    get selectionOfInterest() {
+      if (!this.selection_.length && this.highlight_.length)
+        return this.highlight_;
+      return this.selection_;
+    },
+
+    /**
+     * Sets the selected events, changes the SelectionState of the events to
+     *   SELECTED and clears the highlighted events.
+     * @param {Selection} selection A Selection of the new selected events.
+     */
+    setSelectionAndClearHighlight: function(selection) {
+      this.setSelectionAndHighlight(selection, null);
+    },
+
+    /**
+     * Sets the highlighted events, changes the SelectionState of the events to
+     *   HIGHLIGHTED and clears the selected events. All other events are set to
+     *   DIMMED.
+     * @param {Selection} highlight A Selection of the new highlighted events.
+     */
+    setHighlightAndClearSelection: function(highlight) {
+      this.setSelectionAndHighlight(null, highlight);
+    },
+
+    /**
+     * Sets both selected and highlighted events. If an event is both it will be
+     *   set to SELECTED. All other events are set to DIMMED.
+     * @param {Selection} selection A Selection of the new selected events.
+     * @param {Selection} highlight A Selection of the new highlighted events.
+     */
+    setSelectionAndHighlight: function(selection, highlight) {
+      if (selection === this.selection_ && highlight === this.highlight_)
+        return;
+
+      if ((selection !== null && !(selection instanceof Selection)) ||
+          (highlight !== null && !(highlight instanceof Selection))) {
+        throw new Error('Expected Selection');
+      }
+
+      if (highlight && highlight.length) {
+        // Set all events to DIMMED. This needs to be done before clearing the
+        // old highlight, so that the old events are still available. This is
+        // also necessary when the highlight doesn't change, because it might
+        // have overlapping events with selection.
+        this.resetEventsTo_(SelectionState.DIMMED);
+
+        // Switch the highlight.
+        if (highlight !== this.highlight_)
+          this.highlight_ = highlight;
+
+        // Set HIGHLIGHTED on the events of the new highlight.
+        this.setSelectionState_(highlight, SelectionState.HIGHLIGHTED);
+      } else {
+        // If no highlight is active the SelectionState needs to be cleared.
+        // Note that this also clears old SELECTED events, so it doesn't need
+        // to be called again when setting the selection.
+        this.resetEventsTo_(SelectionState.NONE);
+        this.highlight_ = new Selection();
+      }
+
+      if (selection && selection.length) {
+        // Switch the selection
+        if (selection !== this.selection_)
+          this.selection_ = selection;
+
+        // Set SELECTED on the events of the new highlight.
+        this.setSelectionState_(selection, SelectionState.SELECTED);
+      } else
+        this.selection_ = new Selection();
+
+      tv.b.dispatchSimpleEvent(this, 'selectionChange');
+      this.showHintText_('Press \'m\' to mark current selection');
+
+      if (this.selectionOfInterest.length) {
+        var track = this.viewport_.trackForEvent(this.selectionOfInterest[0]);
+        if (track)
+          track.scrollIntoViewIfNeeded();
+      }
+
+      this.viewport_.dispatchChangeEvent(); // Triggers a redraw.
+    },
+
+    /**
+     * Sets a new SelectionState on all events in the selection.
+     * @param {Selection} selection The affected selection.
+     * @param {SelectionState} selectionState The new selection state.
+     */
+    setSelectionState_: function(selection, selectionState) {
+      for (var i = 0; i < selection.length; i++)
+        selection[i].selectionState = selectionState;
+    },
+
+    /**
+     * Resets all events to the provided SelectionState. When the SelectionState
+     *   changes from or to DIMMED all events in the model need to get updated.
+     * @param {SelectionState} selectionState The SelectionState to reset to.
+     */
+    resetEventsTo_: function(selectionState) {
+      var dimmed = this.highlight_.length;
+      var resetAll = (dimmed && selectionState !== SelectionState.DIMMED) ||
+                     (!dimmed && selectionState === SelectionState.DIMMED);
+      if (resetAll) {
+        this.model.iterateAllEvents(
+            function(event) { event.selectionState = selectionState; });
+      } else {
+        this.setSelectionState_(this.selection_, selectionState);
+        this.setSelectionState_(this.highlight_, selectionState);
+      }
+    },
+
+    hideDragBox_: function() {
+      this.dragBox_.style.left = '-1000px';
+      this.dragBox_.style.top = '-1000px';
+      this.dragBox_.style.width = 0;
+      this.dragBox_.style.height = 0;
+    },
+
+    setDragBoxPosition_: function(xStart, yStart, xEnd, yEnd) {
+      var loY = Math.min(yStart, yEnd);
+      var hiY = Math.max(yStart, yEnd);
+      var loX = Math.min(xStart, xEnd);
+      var hiX = Math.max(xStart, xEnd);
+      var modelTrackRect = this.modelTrack_.getBoundingClientRect();
+      var dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY};
+
+      dragRect.right = dragRect.left + dragRect.width;
+      dragRect.bottom = dragRect.top + dragRect.height;
+
+      var modelTrackContainerRect =
+          this.modelTrackContainer_.getBoundingClientRect();
+      var clipRect = {
+        left: modelTrackContainerRect.left,
+        top: modelTrackContainerRect.top,
+        right: modelTrackContainerRect.right,
+        bottom: modelTrackContainerRect.bottom
+      };
+
+      var headingWidth = window.getComputedStyle(
+          this.querySelector('heading')).width;
+      var trackTitleWidth = parseInt(headingWidth);
+      clipRect.left = clipRect.left + trackTitleWidth;
+
+      var finalDragBox = intersectRect_(clipRect, dragRect);
+
+      this.dragBox_.style.left = finalDragBox.left + 'px';
+      this.dragBox_.style.width = finalDragBox.width + 'px';
+      this.dragBox_.style.top = finalDragBox.top + 'px';
+      this.dragBox_.style.height = finalDragBox.height + 'px';
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var canv = this.modelTrackContainer_.canvas;
+      var dt = this.viewport_.currentDisplayTransform;
+      var loWX = dt.xViewToWorld(
+          (loX - canv.offsetLeft) * pixelRatio);
+      var hiWX = dt.xViewToWorld(
+          (hiX - canv.offsetLeft) * pixelRatio);
+
+      var roundedDuration = Math.round((hiWX - loWX) * 100) / 100;
+      this.dragBox_.textContent = roundedDuration + 'ms';
+
+      var e = new tv.b.Event('selectionChanging');
+      e.loWX = loWX;
+      e.hiWX = hiWX;
+      this.dispatchEvent(e);
+    },
+
+    onGridToggle_: function(left) {
+      var tb = left ? this.selection.bounds.min : this.selection.bounds.max;
+
+      // Toggle the grid off if the grid is on, the marker position is the same
+      // and the same element is selected (same timebase).
+      if (this.viewport_.gridEnabled &&
+          this.viewport_.gridSide === left &&
+          this.viewport_.gridInitialTimebase === tb) {
+        this.viewport_.gridside = undefined;
+        this.viewport_.gridEnabled = false;
+        this.viewport_.gridInitialTimebase = undefined;
+        return;
+      }
+
+      // Shift the timebase left until its just left of model_.bounds.min.
+      var numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) /
+          this.viewport_.gridStep_);
+
+      this.viewport_.gridEnabled = true;
+      this.viewport_.gridSide = left;
+      this.viewport_.gridInitialTimebase = tb;
+      this.viewport_.gridTimebase = tb -
+          (numIntervalsSinceStart + 1) * this.viewport_.gridStep_;
+    },
+
+    storeLastMousePos_: function(e) {
+      this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+    },
+
+    storeLastTouchPositions_: function(e) {
+      this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e);
+    },
+
+    extractRelativeMousePosition_: function(e) {
+      var canv = this.modelTrackContainer_.canvas;
+      return {
+        x: e.clientX - canv.offsetLeft,
+        y: e.clientY - canv.offsetTop
+      };
+    },
+
+    extractRelativeTouchPositions_: function(e) {
+      var canv = this.modelTrackContainer_.canvas;
+
+      var touches = [];
+      for (var i = 0; i < e.touches.length; ++i) {
+        touches.push({
+          x: e.touches[i].clientX - canv.offsetLeft,
+          y: e.touches[i].clientY - canv.offsetTop
+        });
+      }
+      return touches;
+    },
+
+    storeInitialMouseDownPos_: function(e) {
+
+      var position = this.extractRelativeMousePosition_(e);
+
+      this.mouseViewPosAtMouseDown_.x = position.x;
+      this.mouseViewPosAtMouseDown_.y = position.y;
+    },
+
+    focusElements_: function() {
+      if (document.activeElement)
+        document.activeElement.blur();
+      if (this.focusElement.tabIndex >= 0)
+        this.focusElement.focus();
+    },
+
+    storeInitialInteractionPositionsAndFocus_: function(e) {
+
+      this.storeInitialMouseDownPos_(e);
+      this.storeLastMousePos_(e);
+
+      this.focusElements_();
+    },
+
+    onBeginPanScan_: function(e) {
+      var vp = this.viewport_;
+      this.viewportDisplayTransformAtMouseDown_ =
+          vp.currentDisplayTransform.clone();
+      this.isPanningAndScanning_ = true;
+
+      this.storeInitialInteractionPositionsAndFocus_(e);
+      e.preventDefault();
+    },
+
+    onUpdatePanScan_: function(e) {
+      if (!this.isPanningAndScanning_)
+        return;
+
+      var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x -
+          this.mouseViewPosAtMouseDown_.x);
+
+      var yDelta = this.lastMouseViewPos_.y -
+          this.mouseViewPosAtMouseDown_.y;
+
+      tempDisplayTransform.set(this.viewportDisplayTransformAtMouseDown_);
+      tempDisplayTransform.incrementPanXInViewUnits(xDeltaView);
+      tempDisplayTransform.panY -= yDelta;
+      this.viewport_.setDisplayTransformImmediately(tempDisplayTransform);
+
+      e.preventDefault();
+      e.stopPropagation();
+
+      this.storeLastMousePos_(e);
+    },
+
+    onEndPanScan_: function(e) {
+      this.isPanningAndScanning_ = false;
+
+      this.storeLastMousePos_(e);
+
+      if (!e.isClick)
+        e.preventDefault();
+    },
+
+    onBeginSelection_: function(e) {
+      var canv = this.modelTrackContainer_.canvas;
+      var rect = this.modelTrack_.getBoundingClientRect();
+      var canvRect = canv.getBoundingClientRect();
+
+      var inside = rect &&
+          e.clientX >= rect.left &&
+          e.clientX < rect.right &&
+          e.clientY >= rect.top &&
+          e.clientY < rect.bottom &&
+          e.clientX >= canvRect.left &&
+          e.clientX < canvRect.right;
+
+      if (!inside)
+        return;
+
+      this.dragBeginEvent_ = e;
+
+      this.storeInitialInteractionPositionsAndFocus_(e);
+      e.preventDefault();
+    },
+
+    onUpdateSelection_: function(e) {
+      if (!this.dragBeginEvent_)
+        return;
+
+      // Update the drag box
+      this.dragBoxXStart_ = this.dragBeginEvent_.clientX;
+      this.dragBoxXEnd_ = e.clientX;
+      this.dragBoxYStart_ = this.dragBeginEvent_.clientY;
+      this.dragBoxYEnd_ = e.clientY;
+      this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_,
+          this.dragBoxXEnd_, this.dragBoxYEnd_);
+
+    },
+
+    onEndSelection_: function(e) {
+      e.preventDefault();
+
+      if (!this.dragBeginEvent_)
+        return;
+
+      // Stop the dragging.
+      this.hideDragBox_();
+      var eDown = this.dragBeginEvent_;
+      this.dragBeginEvent_ = null;
+
+      // Figure out extents of the drag.
+      var loY = Math.min(eDown.clientY, e.clientY);
+      var hiY = Math.max(eDown.clientY, e.clientY);
+      var loX = Math.min(eDown.clientX, e.clientX);
+      var hiX = Math.max(eDown.clientX, e.clientX);
+
+      // Convert to worldspace.
+      var canv = this.modelTrackContainer_.canvas;
+      var worldOffset = canv.getBoundingClientRect().left;
+      var loVX = loX - worldOffset;
+      var hiVX = hiX - worldOffset;
+
+      // Figure out what has been selected.
+      var selection = new Selection();
+      this.modelTrack_.addIntersectingItemsInRangeToSelection(
+          loVX, hiVX, loY, hiY, selection);
+
+      // Activate the new selection.
+      var selection_change_event = new tv.c.RequestSelectionChangeEvent();
+      selection_change_event.selection = selection;
+      this.dispatchEvent(selection_change_event);
+    },
+
+    onBeginZoom_: function(e) {
+      this.isZooming_ = true;
+
+      this.storeInitialInteractionPositionsAndFocus_(e);
+      e.preventDefault();
+    },
+
+    onUpdateZoom_: function(e) {
+      if (!this.isZooming_)
+        return;
+      var newPosition = this.extractRelativeMousePosition_(e);
+
+      var zoomScaleValue = 1 + (this.lastMouseViewPos_.y -
+          newPosition.y) * 0.01;
+
+      this.zoomBy_(zoomScaleValue, false);
+      this.storeLastMousePos_(e);
+    },
+
+    onEndZoom_: function(e) {
+      this.isZooming_ = false;
+
+      if (!e.isClick)
+        e.preventDefault();
+    },
+
+    computeTouchCenter_: function(positions) {
+      var xSum = 0;
+      var ySum = 0;
+      for (var i = 0; i < positions.length; ++i) {
+        xSum += positions[i].x;
+        ySum += positions[i].y;
+      }
+      return {
+        x: xSum / positions.length,
+        y: ySum / positions.length
+      };
+    },
+
+    computeTouchSpan_: function(positions) {
+      var xMin = Number.MAX_VALUE;
+      var yMin = Number.MAX_VALUE;
+      var xMax = Number.MIN_VALUE;
+      var yMax = Number.MIN_VALUE;
+      for (var i = 0; i < positions.length; ++i) {
+        xMin = Math.min(xMin, positions[i].x);
+        yMin = Math.min(yMin, positions[i].y);
+        xMax = Math.max(xMax, positions[i].x);
+        yMax = Math.max(yMax, positions[i].y);
+      }
+      return Math.sqrt((xMin - xMax) * (xMin - xMax) +
+          (yMin - yMax) * (yMin - yMax));
+    },
+
+    onUpdateTransformForTouch_: function(e) {
+      var newPositions = this.extractRelativeTouchPositions_(e);
+      var currentPositions = this.lastTouchViewPositions_;
+
+      var newCenter = this.computeTouchCenter_(newPositions);
+      var currentCenter = this.computeTouchCenter_(currentPositions);
+
+      var newSpan = this.computeTouchSpan_(newPositions);
+      var currentSpan = this.computeTouchSpan_(currentPositions);
+
+      var vp = this.viewport_;
+      var viewWidth = this.modelTrackContainer_.canvas.clientWidth;
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var xDelta = pixelRatio * (newCenter.x - currentCenter.x);
+      var yDelta = newCenter.y - currentCenter.y;
+      var zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1;
+
+      var viewFocus = pixelRatio * newCenter.x;
+      var worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus);
+
+      tempDisplayTransform.set(vp.currentDisplayTransform);
+      tempDisplayTransform.scaleX *= zoomScaleValue;
+      tempDisplayTransform.xPanWorldPosToViewPos(
+          worldFocus, viewFocus, viewWidth);
+      tempDisplayTransform.incrementPanXInViewUnits(xDelta);
+      tempDisplayTransform.panY -= yDelta;
+      vp.setDisplayTransformImmediately(tempDisplayTransform);
+      this.storeLastTouchPositions_(e);
+    },
+
+    initHintText_: function() {
+      this.hintTextBox_ = this.ownerDocument.createElement('div');
+      this.hintTextBox_.className = 'hint-text';
+      this.hintTextBox_.style.display = 'none';
+      this.appendChild(this.hintTextBox_);
+
+      this.pendingHintTextClearTimeout_ = undefined;
+    },
+
+    showHintText_: function(text) {
+      if (this.pendingHintTextClearTimeout_) {
+        window.clearTimeout(this.pendingHintTextClearTimeout_);
+        this.pendingHintTextClearTimeout_ = undefined;
+      }
+      this.pendingHintTextClearTimeout_ = setTimeout(
+          this.hideHintText_.bind(this), 1000);
+      this.hintTextBox_.textContent = text;
+      this.hintTextBox_.style.display = '';
+    },
+
+    hideHintText_: function() {
+      this.pendingHintTextClearTimeout_ = undefined;
+      this.hintTextBox_.style.display = 'none';
+    }
+  };
+
+  return {
+    TimelineTrackView: TimelineTrackView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_track_view_test.html b/trace-viewer/trace_viewer/core/timeline_track_view_test.html
new file mode 100644
index 0000000..3db10c8
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_track_view_test.html
@@ -0,0 +1,323 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Selection = tv.c.Selection;
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var Task = tv.b.Task;
+
+  function contains(array, element) {
+    for (var i = 0; i < array.length; i++) {
+      if (array[i] === element) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  function checkSelectionStates(timeline, selection, highlight) {
+    selection = selection || [];
+    highlight = highlight || [];
+
+    // The objects timeline.selection and timeline.highlight are not actually
+    // Arrays, they are Selection objects. Here we are not checking the other
+    // properties of the selection and highlight, only the numbered properties.
+    assert.equal(timeline.selection.length, selection.length);
+    assert.equal(timeline.highlight.length, highlight.length);
+    for (var i = 0; i < selection.length; i++)
+      assert.strictEqual(timeline.selection[i], selection[i]);
+    for (var i = 0; i < highlight.length; i++)
+      assert.strictEqual(timeline.highlight[i], highlight[i]);
+
+    timeline.model.iterateAllEvents(function(event) {
+      if (contains(selection, event))
+        assert.equal(event.selectionState, SelectionState.SELECTED);
+      else if (contains(highlight, event))
+        assert.equal(event.selectionState, SelectionState.HIGHLIGHTED);
+      else if (highlight.length)
+        assert.equal(event.selectionState, SelectionState.DIMMED);
+      else
+        assert.equal(event.selectionState, SelectionState.NONE);
+    });
+  };
+
+  test('instantiate', function() {
+    var model = new tv.c.TraceModel();
+    var num_threads = 500;
+    model.importTraces([], false, false, function() {
+      var p100 = model.getOrCreateProcess(100);
+      for (var i = 0; i < num_threads; i++) {
+        var t = p100.getOrCreateThread(101 + i);
+        if (i % 2 == 0) {
+          t.sliceGroup.beginSlice('cat', 'a', 100);
+          t.sliceGroup.endSlice(110);
+        } else {
+          t.sliceGroup.beginSlice('cat', 'b', 50);
+          t.sliceGroup.endSlice(120);
+        }
+      }
+    });
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+    timeline.focusElement = timeline;
+    timeline.tabIndex = 0;
+    timeline.style.maxHeight = '600px';
+    this.addHTMLOutput(timeline);
+  });
+
+  test('addAllObjectsMatchingFilterToSelectionAsTask', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'a', 0, 1, {}, 3));
+    t1.sliceGroup.pushSlice(
+        new tv.c.trace_model.ThreadSlice('', 'b', 0, 1.1, {}, 2.8));
+
+    var t1asg = t1.asyncSliceGroup;
+    t1asg.slices.push(
+        tv.c.test_utils.newAsyncSliceNamed('a', 0, 1, t1, t1));
+    t1asg.slices.push(
+        tv.c.test_utils.newAsyncSliceNamed('b', 1, 2, t1, t1));
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    var expected = [t1asg.slices[0],
+                    t1.sliceGroup.slices[0]];
+    var result = new tv.c.Selection();
+    var filterTask = timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+        new tv.c.TitleOrCategoryFilter('a'), result);
+    Task.RunSynchronously(filterTask);
+    assert.equal(result.length, 2);
+    assert.equal(result[0], expected[0]);
+    assert.equal(result[1], expected[1]);
+
+    var expected = [t1asg.slices[1],
+                    t1.sliceGroup.slices[1]];
+    var result = new tv.c.Selection();
+    var filterTask = timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+        new tv.c.TitleOrCategoryFilter('b'), result);
+    Task.RunSynchronously(filterTask);
+    assert.equal(result.length, 2);
+    assert.equal(result[0], expected[0]);
+    assert.equal(result[1], expected[1]);
+  });
+
+  test('emptyThreadsDeleted', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    assert.isFalse(timeline.hasVisibleContent);
+  });
+
+  test('filteredCounters', function() {
+    var model = new tv.c.TraceModel();
+    var c1 = model.kernel.getOrCreateCpu(0);
+    c1.getOrCreateCounter('', 'b');
+
+    var p1 = model.getOrCreateProcess(1);
+    var ctr = p1.getOrCreateCounter('', 'a');
+    var series = new tv.c.trace_model.CounterSeries('a', 0);
+    series.addCounterSample(0, 1);
+    ctr.addSeries(series);
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    assert.isTrue(timeline.hasVisibleContent);
+  });
+
+  test('filteredCpus', function() {
+    var model = new tv.c.TraceModel();
+    var c1 = model.kernel.getOrCreateCpu(1);
+    c1.getOrCreateCounter('', 'a');
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    assert.isTrue(timeline.hasVisibleContent);
+  });
+
+  test('filteredProcesses', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    p1.getOrCreateCounter('', 'a');
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    assert.isTrue(timeline.hasVisibleContent);
+  });
+
+  test('filteredThreads', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(2);
+    t1.sliceGroup.pushSlice(tv.c.test_utils.newSlice(0, 1));
+
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    assert.isTrue(timeline.hasVisibleContent);
+  });
+
+  test('selectionAndHighlight', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'ab', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'b', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'B'}
+    ];
+    var model = new tv.c.TraceModel(events);
+    var timeline = new tv.c.TimelineTrackView();
+    timeline.model = model;
+
+    var selection = new Selection();
+    var filterTask = timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+        new tv.c.TitleOrCategoryFilter('a'), selection);
+    Task.RunSynchronously(filterTask);
+
+    var highlight = new Selection();
+    var filterTask = timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+        new tv.c.TitleOrCategoryFilter('b'), highlight);
+    Task.RunSynchronously(filterTask);
+
+    // Test for faulty input.
+    assert.throw(function() {
+      timeline.selection = 'selection';
+    });
+
+    assert.throw(function() {
+      timeline.highlight = 1;
+    });
+
+    assert.throw(function() {
+      timeline.setSelectionAndHighlight(0, false);
+    });
+
+    // Check state after reset.
+    timeline.setSelectionAndHighlight(null, null);
+    checkSelectionStates(timeline, null, null);
+
+    // Add selection only.
+    timeline.selection = selection;
+    assert.equal(timeline.selection, selection);
+    checkSelectionStates(timeline, selection, null);
+
+    // Reset selection.
+    timeline.selection = null;
+    assert.equal(timeline.selection.length, 0);
+    checkSelectionStates(timeline, null, null);
+
+    // Add highlight only.
+    timeline.highlight = highlight;
+    assert.equal(timeline.highlight, highlight);
+    checkSelectionStates(timeline, null, highlight);
+
+    // Reset highlight
+    timeline.highlight = null;
+    assert.equal(timeline.highlight.length, 0);
+    checkSelectionStates(timeline, null, null);
+
+    // Add selection and highlight.
+    timeline.setSelectionAndHighlight(selection, highlight);
+    checkSelectionStates(timeline, selection, highlight);
+
+    // Selection replaces old selection.
+    var subSelection = selection.subSelection(0, 1);
+    timeline.selection = subSelection;
+    checkSelectionStates(timeline, subSelection, highlight);
+
+    // Highlight replaces old highlight.
+    var subHighlight = highlight.subSelection(1, 2);
+    timeline.highlight = subHighlight;
+    checkSelectionStates(timeline, subSelection, subHighlight);
+
+    // Set selection and clear highlight.
+    timeline.setSelectionAndClearHighlight(selection);
+    checkSelectionStates(timeline, selection, null);
+
+    // Set highlight and clear selection.
+    timeline.setHighlightAndClearSelection(highlight);
+    checkSelectionStates(timeline, null, highlight);
+
+    // Reset both.
+    timeline.setSelectionAndHighlight(null, null);
+    checkSelectionStates(timeline, null, null);
+  });
+
+  test('interestRange', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+    var model = new tv.c.TraceModel(events);
+    var trackView = new tv.c.TimelineTrackView();
+    trackView.model = model;
+    this.addHTMLOutput(trackView);
+
+    var slice = model.processes[52].threads[53].sliceGroup.slices[2];
+    trackView.viewport.interestRange.setMinAndMax(slice.start, slice.end);
+  });
+
+  test('emptyInterestRange', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+    var model = new tv.c.TraceModel(events);
+    var trackView = new tv.c.TimelineTrackView();
+    trackView.model = model;
+    this.addHTMLOutput(trackView);
+    trackView.viewport.interestRange.reset();
+  });
+
+
+  test('thinnestInterestRange', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'b', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'c', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'c', args: {}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'a', args: {}, pid: 52, ts: 634, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+    var model = new tv.c.TraceModel(events);
+    var trackView = new tv.c.TimelineTrackView();
+    trackView.model = model;
+    this.addHTMLOutput(trackView);
+    trackView.viewport.interestRange.reset();
+
+    var slice = model.processes[52].threads[53].sliceGroup.slices[2];
+    trackView.viewport.interestRange.setMinAndMax(slice.start, slice.start);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_view.css b/trace-viewer/trace_viewer/core/timeline_view.css
new file mode 100644
index 0000000..06c841d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_view.css
@@ -0,0 +1,204 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+x-timeline-view {
+  -webkit-flex-direction: column;
+  cursor: default;
+  display: -webkit-flex;
+  font-family: sans-serif;
+  padding: 0;
+}
+
+x-timeline-view > .control > .title {
+  font-size: 14px;
+  height: 22px;
+  padding-left: 2px;
+  padding-right: 8px;
+  padding-top: 2px;
+  flex: 1 0 auto;
+}
+
+x-timeline-view > .control {
+  background-color: #e6e6e6;
+  background-image: -webkit-gradient(linear,
+                                     0 0,
+                                     0 100%,
+                                     from(#E5E5E5),
+                                     to(#D1D1D1));
+  flex: 0 0 auto;
+  overflow-x: auto;
+}
+
+x-timeline-view > .control > .bar {
+  display: flex;
+}
+
+x-timeline-view > .control::-webkit-scrollbar{
+  height: 0px;
+}
+
+x-timeline-view > .control > .bar > #right-controls {
+  margin-left: auto;
+}
+
+x-timeline-view > .control > #collapsing-controls {
+  display: -webkit-flex;
+}
+
+x-timeline-view > .control .controls {
+  display: -webkit-flex;
+  flex: 0 0 auto;
+}
+
+x-timeline-view > .control > .bar > span {
+  padding-left: 5px;
+  padding-right: 10px;
+}
+
+x-timeline-view > .control > .bar > .controls button,
+x-timeline-view > .control > .bar > .controls label {
+  font-size: 14px;
+  height: 22px;
+  margin: 1px 2px 1px 2px;
+}
+
+x-timeline-view > .control > .bar > .spacer {
+  -webkit-flex: 1 1 auto;
+}
+
+x-timeline-view > middle-container {
+  -webkit-flex: 1 1 auto;
+  -webkit-flex-direction: row;
+  border-bottom: 1px solid #8e8e8e;
+  display: -webkit-flex;
+  min-height: 0;
+}
+
+x-timeline-view > middle-container > track-view-container {
+  -webkit-flex: 1 1 auto;
+  display: -webkit-flex;
+  min-height: 0;
+  min-width: 0;
+}
+
+x-timeline-view > middle-container > track-view-container > * {
+  -webkit-flex: 1 1 auto;
+}
+
+x-timeline-view > middle-container > x-timeline-view-side-panel-container {
+  -webkit-flex: 0 0 auto;
+}
+
+x-timeline-view > x-drag-handle {
+  -webkit-flex: 0 0 auto;
+}
+
+x-timeline-view > tracing-analysis-view {
+  -webkit-flex: 0 0 auto;
+}
+
+x-timeline-view .selection {
+  margin: 2px;
+}
+
+x-timeline-view .selection ul {
+  margin: 0;
+}
+
+.button {
+  background-color: #f8f8f8;
+  border: 1px solid rgba(0, 0, 0, 0.5);
+  color: rgba(0,0,0,0.8);
+  font-size: 14px;
+  height: 19px;
+  margin: 1px;
+  min-width: 23px;
+  text-align: center;
+}
+
+.button:hover {
+  background-color: rgba(255, 255, 255, 1.0);
+  border: 1px solid rgba(0, 0, 0, 0.8);
+  box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+  color: rgba(0, 0, 0, 1);
+}
+
+.view-info-button {
+  padding-left: 4px;
+  padding-right: 4px;
+  width: auto;
+}
+
+.view-info-button:hover {
+  border: solid 1px;
+}
+
+.metadata-dialog-text {
+  font-family: monospace;
+  overflow: auto;
+  white-space: pre;
+}
+
+.view-help-text {
+  -webkit-flex: 1 1 auto;
+  -webkit-flex-direction: row;
+  display: -webkit-flex;
+  width: 700px;
+}
+.view-help-text .column {
+  width: 50%;
+}
+.view-help-text h2 {
+  font-size: 1.2em;
+  margin: 0;
+  margin-top: 5px;
+  text-align: center;
+}
+.view-help-text h3 {
+  margin: 0;
+  margin-left: 126px;
+  margin-top: 10px;
+}
+.view-help-text .pair {
+  -webkit-flex: 1 1 auto;
+  -webkit-flex-direction: row;
+  display: -webkit-flex;
+}
+.view-help-text .command {
+  font-family: monospace;
+  margin-right: 5px;
+  text-align: right;
+  width: 150px;
+}
+.view-help-text .action {
+  font-size: 0.9em;
+  text-align: left;
+  width: 200px;
+}
+.view-help-text .mouse-mode-icon {
+  border: 1px solid #888;
+  border-radius: 3px;
+  box-shadow: inset 0 0 2px rgba(0,0,0,0.3);
+  display: inline-block;
+  height: 26px;
+  margin-right: 1px;
+  position: relative;
+  top: 4px;
+  width: 27px;
+  zoom: 0.75;
+}
+.view-help-text .mouse-mode-icon.pan-mode {
+  background-position: -1px -11px;
+}
+.view-help-text .mouse-mode-icon.select-mode {
+  background-position: -1px -41px;
+}
+.view-help-text .mouse-mode-icon.zoom-mode {
+  background-position: -1px -71px;
+}
+.view-help-text .mouse-mode-icon.timing-mode {
+  background-position: -1px -101px;
+}
+
diff --git a/trace-viewer/trace_viewer/core/timeline_view.html b/trace-viewer/trace_viewer/core/timeline_view.html
new file mode 100644
index 0000000..4123965
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_view.html
@@ -0,0 +1,653 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/base/ui/common.css">
+<link rel="stylesheet" href="/core/timeline_view.css">
+
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/settings.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/overlay.html">
+<link rel="import" href="/base/ui/drag_handle.html">
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/core/favicons.html">
+<link rel="import" href="/core/find_control.html">
+<link rel="import" href="/core/find_controller.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/scripting_control.html">
+<link rel="import" href="/core/scripting_controller.html">
+<link rel="import" href="/core/side_panel/side_panel_container.html">
+
+<template id="timeline-view-template">
+  <div class="control">
+    <div class="bar">
+      <div id="left-controls" class="controls"></div>
+      <div class="title">^_^</div>
+      <div id="right-controls" class="controls"></div>
+    </div>
+    <div id="collapsing-controls" class="controls"></div>
+  </div>
+  <middle-container>
+    <track-view-container></track-view-container>
+    <tv-c-side-panel-container></tv-c-side-panel-container>
+  </middle-container>
+  <x-drag-handle></x-drag-handle>
+  <tracing-analysis-view id="analysis"></tracing-analysis-view>
+</template>
+
+<template id="help-btn-template">
+  <div class="button view-help-button">?</div>
+  <div class="view-help-text">
+    <div class="column left">
+      <h2>Navigation</h2>
+      <div class='pair'>
+        <div class='command'>w/s</div>
+        <div class='action'>Zoom in/out (+shift: faster)</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>a/d</div>
+        <div class='action'>Pan left/right (+shift: faster)</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>&rarr;/shift-TAB</div>
+        <div class='action'>Select previous event</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>&larr;/TAB</div>
+        <div class='action'>Select next event</div>
+      </div>
+
+      <h2>Mouse Controls</h2>
+      <div class='pair'>
+        <div class='command'>click</div>
+        <div class='action'>Select event</div>
+      </div>
+      <div class='pair'>
+        <div class='command'>alt-mousewheel</div>
+        <div class='action'>Zoom in/out</div>
+      </div>
+
+      <h3>
+        <span class='mouse-mode-icon select-mode'></span>
+        Select mode
+      </h3>
+      <div class='pair'>
+        <div class='command'>drag</div>
+        <div class='action'>Box select</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>double click</div>
+        <div class='action'>Select all events with same title</div>
+      </div>
+
+      <h3>
+        <span class='mouse-mode-icon pan-mode'></span>
+        Pan mode
+      </h3>
+      <div class='pair'>
+        <div class='command'>drag</div>
+        <div class='action'>Pan the view</div>
+      </div>
+
+      <h3>
+        <span class='mouse-mode-icon zoom-mode'></span>
+        Zoom mode
+      </h3>
+      <div class='pair'>
+        <div class='command'>drag</div>
+        <div class='action'>Zoom in/out by dragging up/down</div>
+      </div>
+
+      <h3>
+        <span class='mouse-mode-icon timing-mode'></span>
+        Timing mode
+      </h3>
+      <div class='pair'>
+        <div class='command'>drag</div>
+        <div class='action'>Create or move markers</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>double click</div>
+        <div class='action'>Set marker range to slice</div>
+      </div>
+    </div>
+
+    <div class="column right">
+      <h2>General</h2>
+      <div class='pair'>
+        <div class='command'>1-4</div>
+        <div class='action'>Switch mouse mode</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>shift</div>
+        <div class='action'>Hold for temporary select</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>space</div>
+        <div class='action'>Hold for temporary pan</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'><span class='mod'></span></div>
+        <div class='action'>Hold for temporary zoom</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>/</div>
+        <div class='action'>Search</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>enter</div>
+        <div class='action'>Step through search results</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>f</div>
+        <div class='action'>Zoom into selection</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>z/0</div>
+        <div class='action'>Reset zoom and pan</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>g/G</div>
+        <div class='action'>Toggle 60hz grid</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>v</div>
+        <div class='action'>Highlight VSync</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>h</div>
+        <div class='action'>Toggle low/high details</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>m</div>
+        <div class='action'>Mark current selection</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>`</div>
+        <div class='action'>Show or hide the scripting console</div>
+      </div>
+
+      <div class='pair'>
+        <div class='command'>?</div>
+        <div class='action'>Show help</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<template id="metadata-btn-template">
+  <div class="button view-metadata-button view-info-button">Metadata</div>
+  <div class="info-button-text metadata-dialog-text"></div>
+</template>
+
+<template id="console-btn-template">
+  <div class="button view-console-button">&#187;</div>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview View visualizes TRACE_EVENT events using the
+ * tv.c.Timeline component and adds in selection summary and control buttons.
+ */
+tv.exportTo('tv.c', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  /**
+   * View
+   * @constructor
+   * @extends {HTMLUnknownElement}
+   */
+  var TimelineView = tv.b.ui.define('x-timeline-view');
+
+  TimelineView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      var node = tv.b.instantiateTemplate('#timeline-view-template', THIS_DOC);
+      this.appendChild(node);
+
+      this.titleEl_ = this.querySelector('.title');
+      this.leftControlsEl_ = this.querySelector('#left-controls');
+      this.rightControlsEl_ = this.querySelector('#right-controls');
+      this.collapsingControlsEl_ = this.querySelector('#collapsing-controls');
+      this.sidePanelContainer_ = this.querySelector(
+          'tv-c-side-panel-container');
+      this.trackViewContainer_ = this.querySelector('track-view-container');
+
+
+      this.findCtl_ = new TracingFindControl();
+      this.findCtl_.controller = new tv.c.FindController();
+      this.scriptingCtl_ = new TracingScriptingControl();
+      this.scriptingCtl_.controller = document.createElement(
+          'tv-c-scripting-controller');
+
+      this.showFlowEvents_ = false;
+      this.rightControls.appendChild(tv.b.ui.createCheckBox(
+          this, 'showFlowEvents',
+          'tv.c.TimelineView.showFlowEvents', false,
+          'Flow events'));
+      this.highlightVSync_ = false;
+      this.highlightVSyncCheckbox_ = tv.b.ui.createCheckBox(
+          this, 'highlightVSync',
+          'tv.c.TimelineView.highlightVSync', false,
+          'Highlight VSync');
+      this.rightControls.appendChild(this.highlightVSyncCheckbox_);
+
+      this.rightControls.appendChild(this.createMetadataButton_());
+      this.rightControls.appendChild(this.findCtl_);
+      this.rightControls.appendChild(this.createConsoleButton_());
+      this.rightControls.appendChild(this.createHelpButton_());
+      this.collapsingControls.appendChild(this.scriptingCtl_);
+
+      this.dragEl_ = this.querySelector('x-drag-handle');
+      tv.b.ui.decorate(this.dragEl_, tv.b.ui.DragHandle);
+
+      this.analysisEl_ = this.querySelector('#analysis');
+
+      this.addEventListener('requestSelectionChange',
+                            this.onRequestSelectionChange_.bind(this));
+
+      // Bookkeeping.
+      this.onViewportChanged_ = this.onViewportChanged_.bind(this);
+      this.onSelectionChanged_ = this.onSelectionChanged_.bind(this);
+      document.addEventListener('keydown', this.onKeyDown_.bind(this), true);
+      document.addEventListener('keypress', this.onKeypress_.bind(this), true);
+
+      this.dragEl_.target = this.analysisEl_;
+
+      // State management on selection change.
+      this.selections_ = {};
+      window.addEventListener('popstate', this.onPopState_.bind(this));
+    },
+
+    updateDocumentFavicon: function() {
+      var hue;
+      if (!this.model)
+        hue = 'blue';
+      else
+        hue = this.model.faviconHue;
+
+      var faviconData = tv.c.FaviconsByHue[hue];
+      if (faviconData === undefined)
+        faviconData = tv.c.FaviconsByHue['blue'];
+
+      // Find link if its there
+      var link = document.head.querySelector('link[rel="shortcut icon"]');
+      if (!link) {
+        link = document.createElement('link');
+        link.rel = 'shortcut icon';
+        document.head.appendChild(link);
+      }
+      link.href = faviconData;
+    },
+
+    get showFlowEvents() {
+      return this.showFlowEvents_;
+    },
+
+    set showFlowEvents(showFlowEvents) {
+      this.showFlowEvents_ = showFlowEvents;
+      if (!this.trackView_)
+        return;
+      this.trackView_.viewport.showFlowEvents = showFlowEvents;
+    },
+
+    get highlightVSync() {
+      return this.highlightVSync_;
+    },
+
+    set highlightVSync(highlightVSync) {
+      this.highlightVSync_ = highlightVSync;
+      if (!this.trackView_)
+        return;
+      this.trackView_.viewport.highlightVSync = highlightVSync;
+    },
+
+    createHelpButton_: function() {
+      var node = tv.b.instantiateTemplate('#help-btn-template', THIS_DOC);
+      var showEl = node.querySelector('.view-help-button');
+      var helpTextEl = node.querySelector('.view-help-text');
+
+      var dlg = new tv.b.ui.Overlay();
+      dlg.title = 'chrome://tracing Help';
+      dlg.classList.add('view-help-overlay');
+      dlg.appendChild(node);
+
+      function onClick(e) {
+        dlg.visible = !dlg.visible;
+
+        var mod = tv.isMac ? 'cmd ' : 'ctrl';
+        var spans = helpTextEl.querySelectorAll('span.mod');
+        for (var i = 0; i < spans.length; i++) {
+          spans[i].textContent = mod;
+        }
+
+        // Stop event so it doesn't trigger new click listener on document.
+        e.stopPropagation();
+        return false;
+      }
+      showEl.addEventListener('click', onClick.bind(this));
+
+      return showEl;
+    },
+
+    createConsoleButton_: function() {
+      var node = tv.b.instantiateTemplate('#console-btn-template', THIS_DOC);
+      var toggleEl = node.querySelector('.view-console-button');
+
+      function onClick(e) {
+        this.scriptingCtl_.toggleVisibility();
+        e.stopPropagation();
+        return false;
+      }
+      toggleEl.addEventListener('click', onClick.bind(this));
+
+      return toggleEl;
+    },
+
+    createMetadataButton_: function() {
+      var node = tv.b.instantiateTemplate('#metadata-btn-template', THIS_DOC);
+      var showEl = node.querySelector('.view-metadata-button');
+      var textEl = node.querySelector('.info-button-text');
+
+      var dlg = new tv.b.ui.Overlay();
+      dlg.title = 'Metadata for trace';
+      dlg.classList.add('view-metadata-overlay');
+      dlg.appendChild(node);
+
+      function onClick(e) {
+        dlg.visible = true;
+
+        var metadataStrings = [];
+
+        var model = this.model;
+        for (var data in model.metadata) {
+          var meta = model.metadata[data];
+          var name = JSON.stringify(meta.name);
+          var value = JSON.stringify(meta.value, undefined, ' ');
+
+          metadataStrings.push(name + ': ' + value);
+        }
+        textEl.textContent = metadataStrings.join('\n');
+
+        e.stopPropagation();
+        return false;
+      }
+      showEl.addEventListener('click', onClick.bind(this));
+
+      function updateVisibility() {
+        showEl.style.display =
+            (this.model && this.model.metadata.length) ? '' : 'none';
+      }
+      var updateVisibility_ = updateVisibility.bind(this);
+      updateVisibility_();
+      this.addEventListener('modelChange', updateVisibility_);
+
+      return showEl;
+    },
+
+    get leftControls() {
+      return this.leftControlsEl_;
+    },
+
+    get rightControls() {
+      return this.rightControlsEl_;
+    },
+
+    get collapsingControls() {
+      return this.collapsingControlsEl_;
+    },
+
+    get viewTitle() {
+      return this.titleEl_.textContent.substring(
+          this.titleEl_.textContent.length - 2);
+    },
+
+    set viewTitle(text) {
+      if (text === undefined) {
+        this.titleEl_.textContent = '';
+        this.titleEl_.hidden = true;
+        return;
+      }
+      this.titleEl_.hidden = false;
+      this.titleEl_.textContent = text;
+    },
+
+    get model() {
+      if (this.trackView_)
+        return this.trackView_.model;
+      return undefined;
+    },
+
+    set model(model) {
+      var modelInstanceChanged = model != this.model;
+      var modelValid = model && !model.bounds.isEmpty;
+
+      // Remove old trackView if the model has completely changed.
+      if (modelInstanceChanged) {
+        this.trackViewContainer_.textContent = '';
+        if (this.trackView_) {
+          this.trackView_.viewport.removeEventListener(
+              'change', this.onViewportChanged_);
+          this.trackView_.removeEventListener(
+              'selectionChange', this.onSelectionChanged_);
+          this.trackView_.detach();
+          this.trackView_ = undefined;
+          this.findCtl_.controller.timeline = undefined;
+          this.scriptingCtl_.controller.timeline = undefined;
+        }
+        this.sidePanelContainer_.model = undefined;
+      }
+
+      // Create new trackView if needed.
+      if (modelValid && !this.trackView_) {
+        this.trackView_ = new tv.c.TimelineTrackView();
+        this.trackView_.focusElement =
+            this.focusElement_ ? this.focusElement_ : this.parentElement;
+        this.trackViewContainer_.appendChild(this.trackView_);
+        this.findCtl_.controller.timeline = this.trackView_;
+        this.scriptingCtl_.controller.timeline = this.trackView_;
+        this.trackView_.addEventListener(
+            'selectionChange', this.onSelectionChanged_);
+        this.trackView_.viewport.addEventListener(
+            'change', this.onViewportChanged_);
+      }
+
+      // Set the model.
+      if (modelValid) {
+        this.trackView_.model = model;
+        this.sidePanelContainer_.model = model;
+        this.trackView_.viewport.showFlowEvents = this.showFlowEvents;
+        this.trackView_.viewport.highlightVSync = this.highlightVSync;
+        this.clearSelectionHistory_();
+      }
+      tv.b.dispatchSimpleEvent(this, 'modelChange');
+
+      // Do things that are selection specific
+      if (modelInstanceChanged) {
+        this.onSelectionChanged_();
+        this.onViewportChanged_();
+      }
+    },
+
+    get timeline() {
+      return this.trackView_;
+    },
+
+    get settings() {
+      if (!this.settings_)
+        this.settings_ = new tv.b.Settings();
+      return this.settings_;
+    },
+
+    /**
+     * Sets the element whose focus state will determine whether
+     * to respond to keybaord input.
+     */
+    set focusElement(value) {
+      this.focusElement_ = value;
+      if (this.trackView_)
+        this.trackView_.focusElement = value;
+    },
+
+    /**
+     * @return {Element} The element whose focused state determines
+     * whether to respond to keyboard inputs.
+     * Defaults to the parent element.
+     */
+    get focusElement() {
+      if (this.focusElement_)
+        return this.focusElement_;
+      return this.parentElement;
+    },
+
+    get listenToKeys_() {
+      if (!tv.b.ui.isElementAttachedToDocument(this))
+        return;
+      if (!this.focusElement_)
+        return true;
+      if (this.focusElement.tabIndex >= 0)
+        return document.activeElement == this.focusElement;
+      return true;
+    },
+
+    onKeyDown_: function(e) {
+      if (!this.listenToKeys_)
+        return;
+
+      if (e.keyCode === 27) { // ESC
+        this.focus();
+        e.preventDefault();
+      }
+    },
+
+    onKeypress_: function(e) {
+      if (!this.listenToKeys_)
+        return;
+
+      // Shortcuts that *can* steal focus from the console and the filter text
+      // box.
+      switch (e.keyCode) {
+        case '`'.charCodeAt(0):
+          this.scriptingCtl_.toggleVisibility();
+          if (!this.scriptingCtl_.hasFocus)
+            this.focus();
+          e.preventDefault();
+          break;
+      }
+
+      if (this.scriptingCtl_.hasFocus)
+        return;
+
+      // Shortcuts that *can* steal focus from the filter text box.
+      switch (e.keyCode) {
+        case '/'.charCodeAt(0):
+          if (this.findCtl_.hasFocus)
+            this.focus();
+          else
+            this.findCtl_.focus();
+          e.preventDefault();
+          break;
+        case '?'.charCodeAt(0):
+          this.querySelector('.view-help-button').click();
+          e.preventDefault();
+          break;
+      }
+
+      if (this.findCtl_.hasFocus)
+        return;
+
+      // Shortcuts that *can't* steal focus from the filter text box or the
+      // console.
+      switch (e.keyCode) {
+        case 'v'.charCodeAt(0):
+          this.toggleHighlightVSync_();
+          e.preventDefault();
+          break;
+      }
+    },
+
+    onSelectionChanged_: function(e) {
+      var oldScrollTop = this.trackViewContainer_.scrollTop;
+
+      var selection = this.trackView_ ?
+          this.trackView_.selectionOfInterest :
+          new tv.c.Selection();
+      this.analysisEl_.selection = selection;
+      this.trackViewContainer_.scrollTop = oldScrollTop;
+      this.sidePanelContainer_.selection = selection;
+    },
+
+    onRequestSelectionChange_: function(e) {
+      // Save the selection so that when back button is pressed,
+      // it could be retrieved.
+      this.selections_[e.selection.guid] = e.selection;
+      var state = {
+        selection_guid: e.selection.guid
+      };
+      window.history.pushState(state, '');
+
+      this.trackView_.selection = e.selection;
+      e.stopPropagation();
+    },
+
+    onPopState_: function(e) {
+      if (e.state === null)
+        return;
+
+      var selection = this.selections_[e.state.selection_guid];
+      if (selection)
+        this.trackView_.selection = selection;
+      e.stopPropagation();
+    },
+
+    clearSelectionHistory_: function() {
+      this.selections_ = {};
+    },
+
+    onViewportChanged_: function(e) {
+      var spc = this.sidePanelContainer_;
+      if (!this.trackView_) {
+        spc.rangeOfInterest.reset();
+        return;
+      }
+
+      var vr = this.trackView_.viewport.interestRange.asRangeObject();
+      if (!spc.rangeOfInterest.equals(vr))
+        spc.rangeOfInterest = vr;
+    },
+
+    toggleHighlightVSync_: function() {
+      this.highlightVSyncCheckbox_.checked =
+          !this.highlightVSyncCheckbox_.checked;
+    }
+  };
+
+  return {
+    TimelineView: TimelineView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_view_test.html b/trace-viewer/trace_viewer/core/timeline_view_test.html
new file mode 100644
index 0000000..747b404
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_view_test.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_view.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+  var Task = tv.b.Task;
+
+  var createFullyPopulatedModel = function(opt_withError, opt_withMetadata) {
+    var withError = opt_withError !== undefined ? opt_withError : true;
+    var withMetadata = opt_withMetadata !== undefined ?
+        opt_withMetadata : true;
+
+    var num_tests = 50;
+    var testIndex = 0;
+    var startTime = 0;
+
+    var model = new tv.c.TraceModel();
+    for (testIndex = 0; testIndex < num_tests; ++testIndex) {
+      var process = model.getOrCreateProcess(10000 + testIndex);
+      if (testIndex % 2 == 0) {
+        var thread = process.getOrCreateThread('Thread Name Here');
+        thread.sliceGroup.pushSlice(new tv.c.trace_model.Slice(
+            'foo', 'a', 0, startTime, {}, 1));
+        thread.sliceGroup.pushSlice(new tv.c.trace_model.Slice(
+            'bar', 'b', 0, startTime + 23, {}, 10));
+      } else {
+        var thread = process.getOrCreateThread('Name');
+        thread.sliceGroup.pushSlice(new tv.c.trace_model.Slice(
+            'foo', 'a', 0, startTime + 4, {}, 11));
+        thread.sliceGroup.pushSlice(new tv.c.trace_model.Slice(
+            'bar', 'b', 0, startTime + 22, {}, 14));
+      }
+    }
+    var p1000 = model.getOrCreateProcess(1000);
+    var objects = p1000.objects;
+    objects.idWasCreated('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 10);
+    objects.addSnapshot('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 10,
+                        'snapshot-1');
+    objects.addSnapshot('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 25,
+                        'snapshot-2');
+    objects.addSnapshot('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 40,
+                        'snapshot-3');
+    objects.idWasDeleted('0x1000', 'tv.e.cc', 'LayerTreeHostImpl', 45);
+    model.updateCategories_();
+
+    // Add a known problematic piece of data to test the import errors UI.
+    model.importWarning({
+      type: 'test_error',
+      message: 'Synthetic Import Error'
+    });
+    model.updateBounds();
+
+    // Add data with metadata information stored
+    model.metadata.push({name: 'a', value: 'testA'});
+    model.metadata.push({name: 'b', value: 'testB'});
+    model.metadata.push({name: 'c', value: 'testC'});
+
+    return model;
+  };
+
+  var visibleTracks = function(trackButtons) {
+    return trackButtons.reduce(function(numVisible, button) {
+      var style = button.parentElement.style;
+      var visible = (style.display.indexOf('none') === -1);
+      return visible ? numVisible + 1 : numVisible;
+    }, 0);
+  };
+
+  var modelsEquivalent = function(lhs, rhs) {
+    if (lhs.length !== rhs.length)
+      return false;
+    return lhs.every(function(lhsItem, index) {
+      var rhsItem = rhs[index];
+      return rhsItem.regexpText === lhsItem.regexpText &&
+          rhsItem.isOn === lhsItem.isOn;
+    });
+  };
+
+  test('instantiate', function() {
+    var model11 = createFullyPopulatedModel(true, true);
+
+    var view = new tv.c.TimelineView();
+    view.style.height = '400px';
+    view.style.border = '1px solid black';
+    view.model = model11;
+    this.addHTMLOutput(view);
+  });
+
+  test('changeModelToSomethingDifferent', function() {
+    var model00 = createFullyPopulatedModel(false, false);
+    var model11 = createFullyPopulatedModel(true, true);
+
+    var view = new tv.c.TimelineView();
+    view.style.height = '400px';
+    view.model = model00;
+    view.model = undefined;
+    view.model = model11;
+    view.model = model00;
+  });
+
+  test('setModelToSameThingAgain', function() {
+    var model = createFullyPopulatedModel(false, false);
+
+    // Create a view with am model.
+    var view = new tv.c.TimelineView();
+    view.style.height = '400px';
+    view.model = model;
+
+    // Mutate the model and update the view.
+    var t123 = model.getOrCreateProcess(123).getOrCreateThread(123);
+    t123.sliceGroup.pushSlice(newSliceNamed('somethingUnusual', 0, 5));
+    view.model = model;
+
+    // Verify that the new bits of the model show up in the view.
+    var selection = new tv.c.Selection();
+    var filter = new tv.c.TitleOrCategoryFilter('somethingUnusual');
+    var filterTask = view.timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+        filter, selection);
+    Task.RunSynchronously(filterTask);
+    assert.equal(selection.length, 1);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/timeline_viewport.html b/trace-viewer/trace_viewer/core/timeline_viewport.html
new file mode 100644
index 0000000..d843b1d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_viewport.html
@@ -0,0 +1,424 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/draw_helpers.html">
+<link rel="import" href="/core/timeline_interest_range.html">
+<link rel="import" href="/core/timeline_display_transform.html">
+<link rel="import" href="/base/events.html">
+<link rel="import" href="/base/ui/animation.html">
+<link rel="import" href="/base/ui/animation_controller.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Code for the viewport.
+ */
+tv.exportTo('tv.c', function() {
+  var TimelineDisplayTransform = tv.c.TimelineDisplayTransform;
+  var TimelineInterestRange = tv.c.TimelineInterestRange;
+
+  /**
+   * ContainerToTrackObj is a class to handle building and accessing a map
+   * between an EventContainer's stableId and its handling track.
+   *
+   * @constructor
+   */
+  function ContainerToTrackObj() {
+    this.stableIdToTrackMap_ = {};
+  }
+
+  ContainerToTrackObj.prototype = {
+    addContainer: function(container, track) {
+      if (!track)
+        throw new Error('Must provide a track.');
+      this.stableIdToTrackMap_[container.stableId] = track;
+    },
+
+    clearMap: function() {
+      this.stableIdToTrackMap_ = {};
+    },
+
+    getTrackByStableId: function(stableId) {
+      return this.stableIdToTrackMap_[stableId];
+    }
+  };
+
+  /**
+   * The TimelineViewport manages the transform used for navigating
+   * within the timeline. It is a simple transform:
+   *   x' = (x+pan) * scale
+   *
+   * The timeline code tries to avoid directly accessing this transform,
+   * instead using this class to do conversion between world and viewspace,
+   * as well as the math for centering the viewport in various interesting
+   * ways.
+   *
+   * @constructor
+   * @extends {tv.b.EventTarget}
+   */
+  function TimelineViewport(parentEl) {
+    this.parentEl_ = parentEl;
+    this.modelTrackContainer_ = undefined;
+    this.currentDisplayTransform_ = new TimelineDisplayTransform();
+    this.initAnimationController_();
+
+    // Flow events
+    this.showFlowEvents_ = false;
+
+    // Highlights.
+    this.highlightVSync_ = false;
+
+    // High details.
+    this.highDetails_ = false;
+
+    // Grid system.
+    this.gridTimebase_ = 0;
+    this.gridStep_ = 1000 / 60;
+    this.gridEnabled_ = false;
+
+    // Init logic.
+    this.hasCalledSetupFunction_ = false;
+
+    this.onResize_ = this.onResize_.bind(this);
+    this.onModelTrackControllerScroll_ =
+        this.onModelTrackControllerScroll_.bind(this);
+
+    // The following code uses an interval to detect when the parent element
+    // is attached to the document. That is a trigger to run the setup function
+    // and install a resize listener.
+    this.checkForAttachInterval_ = setInterval(
+        this.checkForAttach_.bind(this), 250);
+
+    this.majorMarkPositions = [];
+    this.interestRange_ = new TimelineInterestRange(this);
+
+    this.eventToTrackMap_ = {};
+    this.containerToTrackObj = new ContainerToTrackObj();
+  }
+
+  TimelineViewport.prototype = {
+    __proto__: tv.b.EventTarget.prototype,
+
+    /**
+     * Allows initialization of the viewport when the viewport's parent element
+     * has been attached to the document and given a size.
+     * @param {Function} fn Function to call when the viewport can be safely
+     * initialized.
+     */
+    setWhenPossible: function(fn) {
+      this.pendingSetFunction_ = fn;
+    },
+
+    /**
+     * @return {boolean} Whether the current timeline is attached to the
+     * document.
+     */
+    get isAttachedToDocumentOrInTestMode() {
+      // Allow not providing a parent element, used by tests.
+      if (this.parentEl_ === undefined)
+        return;
+      return tv.b.ui.isElementAttachedToDocument(this.parentEl_);
+    },
+
+    onResize_: function() {
+      this.dispatchChangeEvent();
+    },
+
+    /**
+     * Checks whether the parentNode is attached to the document.
+     * When it is, it installs the iframe-based resize detection hook
+     * and then runs the pendingSetFunction_, if present.
+     */
+    checkForAttach_: function() {
+      if (!this.isAttachedToDocumentOrInTestMode || this.clientWidth == 0)
+        return;
+
+      if (!this.iframe_) {
+        this.iframe_ = document.createElement('iframe');
+        this.iframe_.style.cssText =
+            'position:absolute;width:100%;height:0;border:0;visibility:hidden;';
+        this.parentEl_.appendChild(this.iframe_);
+
+        this.iframe_.contentWindow.addEventListener('resize', this.onResize_);
+      }
+
+      var curSize = this.parentEl_.clientWidth + 'x' +
+          this.parentEl_.clientHeight;
+      if (this.pendingSetFunction_) {
+        this.lastSize_ = curSize;
+        try {
+          this.pendingSetFunction_();
+        } catch (ex) {
+          console.log('While running setWhenPossible:',
+              ex.message ? ex.message + '\n' + ex.stack : ex.stack);
+        }
+        this.pendingSetFunction_ = undefined;
+      }
+
+      window.clearInterval(this.checkForAttachInterval_);
+      this.checkForAttachInterval_ = undefined;
+    },
+
+    /**
+     * Fires the change event on this viewport. Used to notify listeners
+     * to redraw when the underlying model has been mutated.
+     */
+    dispatchChangeEvent: function() {
+      tv.b.dispatchSimpleEvent(this, 'change');
+    },
+
+    detach: function() {
+      if (this.checkForAttachInterval_) {
+        window.clearInterval(this.checkForAttachInterval_);
+        this.checkForAttachInterval_ = undefined;
+      }
+      if (this.iframe_) {
+        this.iframe_.removeEventListener('resize', this.onResize_);
+        this.parentEl_.removeChild(this.iframe_);
+      }
+    },
+
+    initAnimationController_: function() {
+      this.dtAnimationController_ = new tv.b.ui.AnimationController();
+      this.dtAnimationController_.addEventListener(
+          'didtick', function(e) {
+            this.onCurentDisplayTransformChange_(e.oldTargetState);
+          }.bind(this));
+
+      var that = this;
+      this.dtAnimationController_.target = {
+        get panX() {
+          return that.currentDisplayTransform_.panX;
+        },
+
+        set panX(panX) {
+          that.currentDisplayTransform_.panX = panX;
+        },
+
+        get panY() {
+          return that.currentDisplayTransform_.panY;
+        },
+
+        set panY(panY) {
+          that.currentDisplayTransform_.panY = panY;
+        },
+
+        get scaleX() {
+          return that.currentDisplayTransform_.scaleX;
+        },
+
+        set scaleX(scaleX) {
+          that.currentDisplayTransform_.scaleX = scaleX;
+        },
+
+        cloneAnimationState: function() {
+          return that.currentDisplayTransform_.clone();
+        },
+
+        xPanWorldPosToViewPos: function(xWorld, xView) {
+          that.currentDisplayTransform_.xPanWorldPosToViewPos(
+              xWorld, xView, that.modelTrackContainer_.canvas.clientWidth);
+        }
+      };
+    },
+
+    get currentDisplayTransform() {
+      return this.currentDisplayTransform_;
+    },
+
+    setDisplayTransformImmediately: function(displayTransform) {
+      this.dtAnimationController_.cancelActiveAnimation();
+
+      var oldDisplayTransform =
+          this.dtAnimationController_.target.cloneAnimationState();
+      this.currentDisplayTransform_.set(displayTransform);
+      this.onCurentDisplayTransformChange_(oldDisplayTransform);
+    },
+
+    queueDisplayTransformAnimation: function(animation) {
+      if (!(animation instanceof tv.b.ui.Animation))
+        throw new Error('animation must be instanceof tv.b.ui.Animation');
+      this.dtAnimationController_.queueAnimation(animation);
+    },
+
+    onCurentDisplayTransformChange_: function(oldDisplayTransform) {
+      // Ensure panY stays clamped in the track container's scroll range.
+      if (this.modelTrackContainer_) {
+        this.currentDisplayTransform.panY = tv.b.clamp(
+            this.currentDisplayTransform.panY,
+            0,
+            this.modelTrackContainer_.scrollHeight -
+                this.modelTrackContainer_.clientHeight);
+      }
+
+      var changed = !this.currentDisplayTransform.equals(oldDisplayTransform);
+      var yChanged = this.currentDisplayTransform.panY !==
+          oldDisplayTransform.panY;
+      if (yChanged)
+        this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY;
+      if (changed)
+        this.dispatchChangeEvent();
+    },
+
+    onModelTrackControllerScroll_: function(e) {
+      if (this.dtAnimationController_.activeAnimation &&
+          this.dtAnimationController_.activeAnimation.affectsPanY)
+        this.dtAnimationController_.cancelActiveAnimation();
+      var panY = this.modelTrackContainer_.scrollTop;
+      this.currentDisplayTransform_.panY = panY;
+    },
+
+    get modelTrackContainer() {
+      return this.modelTrackContainer_;
+    },
+
+    set modelTrackContainer(m) {
+      if (this.modelTrackContainer_)
+        this.modelTrackContainer_.removeEventListener('scroll',
+            this.onModelTrackControllerScroll_);
+
+      this.modelTrackContainer_ = m;
+      this.modelTrackContainer_.addEventListener('scroll',
+          this.onModelTrackControllerScroll_);
+    },
+
+    get showFlowEvents() {
+      return this.showFlowEvents_;
+    },
+
+    set showFlowEvents(showFlowEvents) {
+      this.showFlowEvents_ = showFlowEvents;
+      this.dispatchChangeEvent();
+    },
+
+    get highlightVSync() {
+      return this.highlightVSync_;
+    },
+
+    set highlightVSync(highlightVSync) {
+      this.highlightVSync_ = highlightVSync;
+      this.dispatchChangeEvent();
+    },
+
+    get highDetails() {
+      return this.highDetails_;
+    },
+
+    set highDetails(highDetails) {
+      this.highDetails_ = highDetails;
+      this.dispatchChangeEvent();
+    },
+
+    get gridEnabled() {
+      return this.gridEnabled_;
+    },
+
+    set gridEnabled(enabled) {
+      if (this.gridEnabled_ == enabled)
+        return;
+
+      this.gridEnabled_ = enabled && true;
+      this.dispatchChangeEvent();
+    },
+
+    get gridTimebase() {
+      return this.gridTimebase_;
+    },
+
+    set gridTimebase(timebase) {
+      if (this.gridTimebase_ == timebase)
+        return;
+      this.gridTimebase_ = timebase;
+      this.dispatchChangeEvent();
+    },
+
+    get gridStep() {
+      return this.gridStep_;
+    },
+
+    get interestRange() {
+      return this.interestRange_;
+    },
+
+    drawMajorMarkLines: function(ctx) {
+      // Apply subpixel translate to get crisp lines.
+      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+      ctx.save();
+      ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+      ctx.beginPath();
+      for (var idx in this.majorMarkPositions) {
+        var x = Math.floor(this.majorMarkPositions[idx]);
+        tv.c.drawLine(ctx, x, 0, x, ctx.canvas.height);
+      }
+      ctx.strokeStyle = '#ddd';
+      ctx.stroke();
+
+      ctx.restore();
+    },
+
+    drawGridLines: function(ctx, viewLWorld, viewRWorld) {
+      if (!this.gridEnabled)
+        return;
+
+      var dt = this.currentDisplayTransform;
+      var x = this.gridTimebase;
+
+      // Apply subpixel translate to get crisp lines.
+      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+      ctx.save();
+      ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0);
+
+      ctx.beginPath();
+      while (x < viewRWorld) {
+        if (x >= viewLWorld) {
+          // Do conversion to viewspace here rather than on
+          // x to avoid precision issues.
+          var vx = Math.floor(dt.xWorldToView(x));
+          tv.c.drawLine(ctx, vx, 0, vx, ctx.canvas.height);
+        }
+
+        x += this.gridStep;
+      }
+      ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)';
+      ctx.stroke();
+
+      ctx.restore();
+    },
+
+    rebuildEventToTrackMap: function() {
+      this.eventToTrackMap_ = undefined;
+
+      var eventToTrackMap = {};
+      eventToTrackMap.addEvent = function(event, track) {
+        if (!track)
+          throw new Error('Must provide a track.');
+        this[event.guid] = track;
+      };
+      this.modelTrackContainer_.addEventsToTrackMap(eventToTrackMap);
+      this.eventToTrackMap_ = eventToTrackMap;
+    },
+
+    rebuildContainerToTrackMap: function() {
+      this.containerToTrackObj.clearMap();
+      this.modelTrackContainer_.addContainersToTrackMap(
+          this.containerToTrackObj);
+    },
+
+    trackForEvent: function(event) {
+      return this.eventToTrackMap_[event.guid];
+    }
+  };
+
+  return {
+    ContainerToTrackObj: ContainerToTrackObj,
+    TimelineViewport: TimelineViewport
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timeline_viewport_test.html b/trace-viewer/trace_viewer/core/timeline_viewport_test.html
new file mode 100644
index 0000000..ca8826e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timeline_viewport_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/timeline_viewport.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('memoization', function() {
+
+    var vp = new tv.c.TimelineViewport(document.createElement('div'));
+
+    var slice = { guid: 1 };
+
+    vp.modelTrackContainer = {
+      addEventsToTrackMap: function(eventToTrackMap) {
+        eventToTrackMap.addEvent(slice, 'track');
+      },
+      addEventListener: function() {}
+    };
+
+    assert.isUndefined(vp.trackForEvent(slice));
+    vp.rebuildEventToTrackMap();
+
+    assert.equal(vp.trackForEvent(slice), 'track');
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/timing_tool.html b/trace-viewer/trace_viewer/core/timing_tool.html
new file mode 100644
index 0000000..3156fbb
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timing_tool.html
@@ -0,0 +1,326 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/constants.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/core/trace_model/slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the TimingTool class.
+ */
+tv.exportTo('tv.c', function() {
+
+  var constants = tv.c.constants;
+
+  /**
+   * Tool for taking time measurements in the TimelineTrackView using
+   * Viewportmarkers.
+   * @constructor
+   */
+  function TimingTool(viewport, targetElement) {
+    this.viewport_ = viewport;
+
+    // Prepare the event handlers to be added and removed repeatedly.
+    this.onMouseMove_ = this.onMouseMove_.bind(this);
+    this.onDblClick_ = this.onDblClick_.bind(this);
+    this.targetElement_ = targetElement;
+
+    // Valid only during mousedown.
+    this.isMovingLeftEdge_ = false;
+  };
+
+  TimingTool.prototype = {
+
+    onEnterTiming: function(e) {
+      this.targetElement_.addEventListener('mousemove', this.onMouseMove_);
+      this.targetElement_.addEventListener('dblclick', this.onDblClick_);
+    },
+
+    onBeginTiming: function(e) {
+      if (!this.isTouchPointInsideTrackBounds_(e.clientX, e.clientY))
+        return;
+
+      var pt = this.getSnappedToEventPosition_(e);
+      this.mouseDownAt_(pt.x, pt.y);
+
+      this.updateSnapIndicators_(pt);
+    },
+
+    updateSnapIndicators_: function(pt) {
+      if (!pt.snapped)
+        return;
+      var ir = this.viewport_.interestRange;
+      if (ir.min === pt.x)
+        ir.leftSnapIndicator = new tv.c.SnapIndicator(pt.y, pt.height);
+      if (ir.max === pt.x)
+        ir.rightSnapIndicator = new tv.c.SnapIndicator(pt.y, pt.height);
+    },
+
+    onUpdateTiming: function(e) {
+      var pt = this.getSnappedToEventPosition_(e);
+      this.mouseMoveAt_(pt.x, pt.y, true);
+      this.updateSnapIndicators_(pt);
+    },
+
+    onEndTiming: function(e) {
+      this.mouseUp_();
+    },
+
+    onExitTiming: function(e) {
+      this.targetElement_.removeEventListener('mousemove', this.onMouseMove_);
+      this.targetElement_.removeEventListener('dblclick', this.onDblClick_);
+    },
+
+    onMouseMove_: function(e) {
+      if (e.button)
+        return;
+      var worldX = this.getWorldXFromEvent_(e);
+      this.mouseMoveAt_(worldX, e.clientY, false);
+    },
+
+    onDblClick_: function(e) {
+      // TODO(nduca): Implement dobuleclicking.
+      console.error('not implemented');
+    },
+
+    ////////////////////////////////////////////////////////////////////////////
+
+    isTouchPointInsideTrackBounds_: function(clientX, clientY) {
+      if (!this.viewport_ ||
+          !this.viewport_.modelTrackContainer ||
+          !this.viewport_.modelTrackContainer.canvas)
+        return false;
+
+      var canvas = this.viewport_.modelTrackContainer.canvas;
+      var canvasRect = canvas.getBoundingClientRect();
+      if (clientX >= canvasRect.left && clientX <= canvasRect.right &&
+          clientY >= canvasRect.top && clientY <= canvasRect.bottom)
+        return true;
+
+      return false;
+    },
+
+    mouseDownAt_: function(worldX, y) {
+      var ir = this.viewport_.interestRange;
+      var dt = this.viewport_.currentDisplayTransform;
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
+
+      if (ir.isEmpty) {
+        ir.setMinAndMax(worldX, worldX);
+        ir.rightSelected = true;
+        this.isMovingLeftEdge_ = false;
+        return;
+      }
+
+
+      // Left edge test.
+      if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
+        ir.leftSelected = true;
+        ir.min = worldX;
+        this.isMovingLeftEdge_ = true;
+        return;
+      }
+
+      // Right edge test.
+      if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
+        ir.rightSelected = true;
+        ir.max = worldX;
+        this.isMovingLeftEdge_ = false;
+        return;
+      }
+
+      ir.setMinAndMax(worldX, worldX);
+      ir.rightSelected = true;
+      this.isMovingLeftEdge_ = false;
+    },
+
+    mouseMoveAt_: function(worldX, y, mouseDown) {
+      var ir = this.viewport_.interestRange;
+
+      if (mouseDown) {
+        this.updateMovingEdge_(worldX);
+        return;
+      }
+
+      var ir = this.viewport_.interestRange;
+      var dt = this.viewport_.currentDisplayTransform;
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var nearnessThresholdWorld = dt.xViewVectorToWorld(6 * pixelRatio);
+
+      // Left edge test.
+      if (Math.abs(worldX - ir.min) < nearnessThresholdWorld) {
+        ir.leftSelected = true;
+        ir.rightSelected = false;
+        return;
+      }
+
+      // Right edge test.
+      if (Math.abs(worldX - ir.max) < nearnessThresholdWorld) {
+        ir.leftSelected = false;
+        ir.rightSelected = true;
+        return;
+      }
+
+      ir.leftSelected = false;
+      ir.rightSelected = false;
+      return;
+    },
+
+    updateMovingEdge_: function(newWorldX) {
+      var ir = this.viewport_.interestRange;
+      var a = ir.min;
+      var b = ir.max;
+      if (this.isMovingLeftEdge_)
+        a = newWorldX;
+      else
+        b = newWorldX;
+
+      if (a <= b)
+        ir.setMinAndMax(a, b);
+      else
+        ir.setMinAndMax(b, a);
+
+      if (ir.min == newWorldX) {
+        this.isMovingLeftEdge_ = true;
+        ir.leftSelected = true;
+        ir.rightSelected = false;
+      } else {
+        this.isMovingLeftEdge_ = false;
+        ir.leftSelected = false;
+        ir.rightSelected = true;
+      }
+    },
+
+    mouseUp_: function() {
+      var dt = this.viewport_.currentDisplayTransform;
+      var ir = this.viewport_.interestRange;
+
+      ir.leftSelected = false;
+      ir.rightSelected = false;
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var minWidthValue = dt.xViewVectorToWorld(2 * pixelRatio);
+      if (ir.range < minWidthValue)
+        ir.reset();
+    },
+
+    getWorldXFromEvent_: function(e) {
+      var pixelRatio = window.devicePixelRatio || 1;
+      var canvas = this.viewport_.modelTrackContainer.canvas;
+      var worldOffset = canvas.getBoundingClientRect().left;
+      var viewX = (e.clientX - worldOffset) * pixelRatio;
+      return this.viewport_.currentDisplayTransform.xViewToWorld(viewX);
+    },
+
+
+    /**
+     * Get the closest position of an event within a vertical range of the mouse
+     * position if possible, otherwise use the position of the mouse pointer.
+     * @param {MouseEvent} e Mouse event with the current mouse coordinates.
+     * @return {
+     *   {Number} x, The x coordinate in world space.
+     *   {Number} y, The y coordinate in world space.
+     *   {Number} height, The height of the event.
+     *   {boolean} snapped Whether the coordinates are from a snapped event or
+     *     the mouse position.
+     * }
+     */
+    getSnappedToEventPosition_: function(e) {
+      var pixelRatio = window.devicePixelRatio || 1;
+      var EVENT_SNAP_RANGE = 16 * pixelRatio;
+
+      var modelTrackContainer = this.viewport_.modelTrackContainer;
+      var modelTrackContainerRect = modelTrackContainer.getBoundingClientRect();
+
+      var viewport = this.viewport_;
+      var dt = viewport.currentDisplayTransform;
+      var worldMaxDist = dt.xViewVectorToWorld(EVENT_SNAP_RANGE);
+
+      var worldX = this.getWorldXFromEvent_(e);
+      var mouseY = e.clientY;
+
+      var selection = new tv.c.Selection();
+
+      // Look at the track under mouse position first for better performance.
+      modelTrackContainer.addClosestEventToSelection(
+          worldX, worldMaxDist, mouseY, mouseY, selection);
+
+      // Look at all tracks visible on screen.
+      if (!selection.length) {
+        modelTrackContainer.addClosestEventToSelection(
+            worldX, worldMaxDist,
+            modelTrackContainerRect.top, modelTrackContainerRect.bottom,
+            selection);
+      }
+
+      var minDistX = worldMaxDist;
+      var minDistY = Infinity;
+      var pixWidth = dt.xViewVectorToWorld(1);
+
+      // Create result object with the mouse coordinates.
+      var result = {
+        x: worldX,
+        y: mouseY - modelTrackContainerRect.top,
+        height: 0,
+        snapped: false
+      };
+
+      var eventBounds = new tv.b.Range();
+      for (var i = 0; i < selection.length; i++) {
+        var event = selection[i];
+        var track = viewport.trackForEvent(event);
+        var trackRect = track.getBoundingClientRect();
+
+        eventBounds.reset();
+        event.addBoundsToRange(eventBounds);
+        var eventX;
+        if (Math.abs(eventBounds.min - worldX) <
+            Math.abs(eventBounds.max - worldX)) {
+          eventX = eventBounds.min;
+        } else {
+          eventX = eventBounds.max;
+        }
+
+        var distX = eventX - worldX;
+
+        var eventY = trackRect.top;
+        var eventHeight = trackRect.height;
+        var distY = Math.abs(eventY + eventHeight / 2 - mouseY);
+
+        // Prefer events with a closer y position if their x difference is below
+        // the width of a pixel.
+        if ((distX <= minDistX || Math.abs(distX - minDistX) < pixWidth) &&
+            distY < minDistY) {
+          minDistX = distX;
+          minDistY = distY;
+
+          // Retrieve the event position from the hit.
+          result.x = eventX;
+          result.y = eventY +
+              modelTrackContainer.scrollTop - modelTrackContainerRect.top;
+          result.height = eventHeight;
+          result.snapped = true;
+        }
+      }
+
+      return result;
+    }
+  };
+
+  return {
+    TimingTool: TimingTool
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/timing_tool_test.html b/trace-viewer/trace_viewer/core/timing_tool_test.html
new file mode 100644
index 0000000..81177f7
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/timing_tool_test.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/timing_tool.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function create100PxWideViewportInto10WideWorld() {
+    var vp = new tv.c.TimelineViewport(document.createElement('div'));
+    var tempDisplayTransform = new tv.c.TimelineDisplayTransform();
+    tempDisplayTransform.xSetWorldBounds(0, 10, 100);
+    vp.setDisplayTransformImmediately(tempDisplayTransform);
+
+    assert.equal(vp.currentDisplayTransform.xViewToWorld(0), 0);
+    assert.equal(vp.currentDisplayTransform.xViewToWorld(100), 10);
+
+    return vp;
+  }
+
+  test('dragLeftInterestRegion', function() {
+    var vp = create100PxWideViewportInto10WideWorld();
+    vp.interestRange.min = 1;
+    vp.interestRange.max = 9;
+    var tool = new tv.c.TimingTool(vp);
+
+    tool.mouseDownAt_(1.1, 0);
+    assert.isTrue(vp.interestRange.leftSelected);
+    tool.mouseMoveAt_(1.5, 0, true);
+    assert.equal(vp.interestRange.min, 1.5);
+    tool.mouseUp_();
+    assert.equal(vp.interestRange.min, 1.5);
+    assert.isFalse(vp.interestRange.leftSelected);
+  });
+
+  test('dragRightInterestRegion', function() {
+    var vp = create100PxWideViewportInto10WideWorld();
+    vp.interestRange.min = 1;
+    vp.interestRange.max = 9;
+    var tool = new tv.c.TimingTool(vp);
+
+    tool.mouseDownAt_(9.1, 0);
+    assert.isTrue(vp.interestRange.rightSelected);
+    tool.mouseMoveAt_(8, 0, true);
+    assert.equal(vp.interestRange.max, 8);
+    tool.mouseUp_();
+    assert.equal(vp.interestRange.max, 8);
+    assert.isFalse(vp.interestRange.leftSelected);
+  });
+
+  test('dragInNewSpace', function() {
+    var vp = create100PxWideViewportInto10WideWorld();
+    vp.interestRange.min = 1;
+    vp.interestRange.max = 9;
+    var tool = new tv.c.TimingTool(vp);
+
+    tool.mouseDownAt_(5, 0);
+    assert.isTrue(vp.interestRange.rightSelected);
+    assert.equal(vp.interestRange.min, 5);
+    assert.equal(vp.interestRange.max, 5);
+    tool.mouseMoveAt_(4, 0, true);
+    assert.equal(vp.interestRange.min, 4);
+    assert.equal(vp.interestRange.max, 5);
+    assert.isTrue(vp.interestRange.leftSelected);
+    tool.mouseUp_();
+    assert.equal(vp.interestRange.min, 4);
+    assert.isFalse(vp.interestRange.leftSelected);
+    assert.isFalse(vp.interestRange.rightSelected);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/alert.html b/trace-viewer/trace_viewer/core/trace_model/alert.html
new file mode 100644
index 0000000..aef76ad
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/alert.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/trace_model/alert_type.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+
+  function Alert(type, start, opt_args) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+    this.type = type;
+    if (opt_args !== undefined)
+      this.args = opt_args;
+    else
+      this.args = {};
+    this.duration = 0;
+  }
+  Alert.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+    get title() {
+      return this.type.title;
+    },
+
+    get colorId() {
+      return this.type.colorId;
+    },
+
+    get userFriendlyName() {
+      return 'Alert ' + this.title + ' at ' +
+          tv.c.analysis.tsString(this.start);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      Alert,
+      {
+        name: 'alert',
+        pluralName: 'alerts',
+        singleViewElementName: 'tv-c-single-alert-sub-view',
+        multiViewElementName: 'tv-c-multi-alert-sub-view'
+      });
+
+  return {
+    Alert: Alert
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/trace_model/alert_type.html b/trace-viewer/trace_viewer/core/trace_model/alert_type.html
new file mode 100644
index 0000000..28dac61
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/alert_type.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/ui/color_scheme.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+
+  var ALERT_SEVERITY = {
+    CRITICAL: 'critical',
+    WARNING: 'warning'
+  };
+
+  function AlertType(title, description, severity, opt_colorId) {
+    this.title = title;
+    this.description = description;
+    this.severity = severity;
+    if (opt_colorId !== undefined)
+      this.colorId = opt_colorId;
+    else
+      this.colorId = tv.b.ui.getColorIdForGeneralPurposeString(title);
+  }
+
+  return {
+    AlertType: AlertType,
+    ALERT_SEVERITY: ALERT_SEVERITY
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/trace_model/annotation.html b/trace-viewer/trace_viewer/core/trace_model/annotation.html
new file mode 100644
index 0000000..61887a1
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/annotation.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/base/guid.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * Annotation is a base class that represents all annotation objects that
+   * can be drawn on the timeline.
+   *
+   * @constructor
+   */
+  function Annotation() {
+    this.guid_ = tv.b.GUID.allocate();
+    this.view_ = undefined;
+  };
+
+  Annotation.fromDictIfPossible = function(args) {
+    if (args.typeName === undefined)
+      throw new Error('Missing typeName argument');
+
+    var typeInfo = Annotation.findTypeInfoMatching(function(typeInfo) {
+      return typeInfo.metadata.typeName === args.typeName;
+    });
+
+    if (typeInfo === undefined)
+      return undefined;
+
+    return typeInfo.constructor.fromDict(args);
+  };
+
+  Annotation.fromDict = function() {
+    throw new Error('Not implemented');
+  }
+
+  Annotation.prototype = {
+    get guid() {
+      return this.guid_;
+    },
+
+    toDict: function() {
+      throw new Error('Not implemented');
+    },
+
+    getOrCreateView: function(viewport) {
+      if (!this.view_)
+        this.view_ = this.createView_(viewport);
+      return this.view_;
+    },
+
+    createView_: function() {
+      throw new Error('Not implemented');
+    }
+  };
+
+  var options = new tv.b.ExtensionRegistryOptions(tv.b. BASIC_REGISTRY_MODE);
+  options.mandatoryBaseType = Annotation;
+  tv.b.decorateExtensionRegistry(Annotation, options);
+
+  Annotation.addEventListener('will-register', function(e) {
+    if (!e.typeInfo.constructor.hasOwnProperty('fromDict'))
+      throw new Error('Must have fromDict method');
+
+    if (!e.typeInfo.metadata.typeName)
+      throw new Error('Registered Annotations must provide typeName');
+  });
+
+  return {
+    Annotation: Annotation
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/annotation_test.html b/trace-viewer/trace_viewer/core/trace_model/annotation_test.html
new file mode 100644
index 0000000..09f700b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/annotation_test.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/location.html">
+<link rel="import" href="/core/trace_model/rect_annotation.html">
+<link rel="import" href="/core/trace_model/x_marker_annotation.html">
+<link rel="import" href="/core/test_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+  test('rectAnnotation', function() {
+    var fakeYComponents = [{stableId: '1.2', yPercentOffset: 0.5}];
+    var start = new tv.c.Location(50, fakeYComponents);
+    var end = new tv.c.Location(70, fakeYComponents);
+    var rectAnnotation = new tv.c.trace_model.RectAnnotation(start, end);
+    assert.equal(rectAnnotation.startLocation, start);
+    assert.equal(rectAnnotation.endLocation, end);
+  });
+
+  test('xMarkerAnnotation', function() {
+    var xMarkerAnnotation = new tv.c.trace_model.XMarkerAnnotation(2000);
+    assert.equal(xMarkerAnnotation.timestamp, 2000);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/async_slice.html b/trace-viewer/trace_viewer/core/trace_model/async_slice.html
new file mode 100644
index 0000000..1d03653
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/async_slice.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/trace_model/slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the AsyncSlice class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A AsyncSlice represents an interval of time during which an
+   * asynchronous operation is in progress. An AsyncSlice consumes no CPU time
+   * itself and so is only associated with Threads at its start and end point.
+   *
+   * @constructor
+   */
+  function AsyncSlice(category, title, colorId, start, args, duration,
+                      opt_isTopLevel) {
+    tv.c.trace_model.Slice.call(this, category, title, colorId, start, args,
+                                duration);
+
+    // TODO(nduca): Forgive me for what I must do.
+    this.subSlices = undefined;
+    this.isTopLevel = (opt_isTopLevel === true);
+  };
+
+  AsyncSlice.prototype = {
+    __proto__: tv.c.trace_model.Slice.prototype,
+
+    id: undefined,
+
+    startThread: undefined,
+
+    endThread: undefined,
+
+    subSlices: undefined,
+
+    get viewSubGroupTitle() {
+      return this.title;
+    },
+
+    get userFriendlyName() {
+      return 'Async slice ' + this.title + ' at ' +
+          tv.c.analysis.tsString(this.start);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      AsyncSlice,
+      {
+        name: 'asyncSlice',
+        pluralName: 'asyncSlices',
+        singleViewElementName: 'tv-c-single-slice-sub-view',
+        multiViewElementName: 'tv-c-multi-slice-sub-view'
+      });
+
+
+  var options = new tv.b.ExtensionRegistryOptions(
+      tv.b.TYPE_BASED_REGISTRY_MODE);
+  options.mandatoryBaseClass = AsyncSlice;
+  options.defaultConstructor = AsyncSlice;
+  tv.b.decorateExtensionRegistry(AsyncSlice, options);
+
+  return {
+    AsyncSlice: AsyncSlice
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/async_slice_group.html b/trace-viewer/trace_viewer/core/trace_model/async_slice_group.html
new file mode 100644
index 0000000..2daf497
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/async_slice_group.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/async_slice.html">
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/range.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the AsyncSliceGroup class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A group of AsyncSlices associated with a thread.
+   * @constructor
+   * @extends {tv.c.trace_model.EventContainer}
+   */
+  function AsyncSliceGroup(parentThread, opt_name) {
+    this.parentThread_ = parentThread;
+    this.guid_ = tv.b.GUID.allocate();
+    this.slices = [];
+    this.bounds = new tv.b.Range();
+    this.name_ = opt_name;
+    this.viewSubGroups_ = undefined;
+  }
+
+  AsyncSliceGroup.prototype = {
+    __proto__: tv.c.trace_model.EventContainer.prototype,
+
+    get guid() {
+      return this.guid_;
+    },
+
+    get parentThread() {
+      return this.parentThread_;
+    },
+
+    get model() {
+      return this.parentThread_.parent.model;
+    },
+
+    get stableId() {
+      return this.parentThread_.stableId + '.AsyncSliceGroup';
+    },
+
+    getSettingsKey: function() {
+      if (!this.name_)
+        return undefined;
+      var parentKey = this.parentThread_.getSettingsKey();
+      if (!parentKey)
+        return undefined;
+      return parentKey + '.' + this.name_;
+    },
+
+    /**
+     * Helper function that pushes the provided slice onto the slices array.
+     */
+    push: function(slice) {
+      this.slices.push(slice);
+    },
+
+    /**
+     * @return {Number} The number of slices in this group.
+     */
+    get length() {
+      return this.slices.length;
+    },
+
+    /**
+     * Shifts all the timestamps inside this group forward by the amount
+     * specified, including all nested subSlices if there are any.
+     */
+    shiftTimestampsForward: function(amount) {
+      for (var sI = 0; sI < this.slices.length; sI++) {
+        var slice = this.slices[sI];
+        slice.start = (slice.start + amount);
+        // Shift all nested subSlices recursively.
+        var shiftSubSlices = function(subSlices) {
+          if (subSlices === undefined || subSlices.length === 0)
+            return;
+          for (var sJ = 0; sJ < subSlices.length; sJ++) {
+            subSlices[sJ].start += amount;
+            shiftSubSlices(subSlices[sJ].subSlices);
+          }
+        };
+        shiftSubSlices(slice.subSlices);
+      }
+    },
+
+    /**
+     * Updates the bounds for this group based on the slices it contains.
+     */
+    updateBounds: function() {
+      this.bounds.reset();
+      for (var i = 0; i < this.slices.length; i++) {
+        this.bounds.addValue(this.slices[i].start);
+        this.bounds.addValue(this.slices[i].end);
+      }
+    },
+
+    /**
+     * Gets the sub-groups in this A-S-G defined by the group titles.
+     *
+     * @return {Array} An array of AsyncSliceGroups where each group has
+     * slices that started on the same thread.
+     */
+    get viewSubGroups() {
+      if (this.viewSubGroups_ === undefined) {
+        var prefix = '';
+        if (this.name !== undefined)
+          prefix = this.name + '.';
+        else
+          prefix = '';
+
+        var subGroupsByTitle = {};
+        for (var i = 0; i < this.slices.length; ++i) {
+          var slice = this.slices[i];
+          var subGroupTitle = slice.viewSubGroupTitle;
+          if (!subGroupsByTitle[subGroupTitle]) {
+            subGroupsByTitle[subGroupTitle] = new AsyncSliceGroup(
+                this.parentThread_, prefix + subGroupTitle);
+          }
+          subGroupsByTitle[subGroupTitle].slices.push(slice);
+        }
+        this.viewSubGroups_ = tv.b.dictionaryValues(subGroupsByTitle);
+      }
+      return this.viewSubGroups_;
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      for (var i = 0; i < this.slices.length; i++) {
+        var slice = this.slices[i];
+        callback.call(opt_this, slice);
+        if (slice.subSlices)
+          slice.subSlices.forEach(callback, opt_this);
+      }
+    },
+
+    iterateAllEventContainers: function(callback) {
+      callback(this);
+    }
+  };
+
+  return {
+    AsyncSliceGroup: AsyncSliceGroup
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/async_slice_group_test.html b/trace-viewer/trace_viewer/core/trace_model/async_slice_group_test.html
new file mode 100644
index 0000000..9e2df29
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/async_slice_group_test.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var Process = tv.c.trace_model.Process;
+  var Thread = tv.c.trace_model.Thread;
+  var AsyncSlice = tv.c.trace_model.AsyncSlice;
+  var AsyncSliceGroup = tv.c.trace_model.AsyncSliceGroup;
+  var newAsyncSlice = tv.c.test_utils.newAsyncSlice;
+
+  test('asyncSliceGroupBounds_Empty', function() {
+    var thread = {};
+    var g = new AsyncSliceGroup(thread);
+    g.updateBounds();
+    assert.isTrue(g.bounds.isEmpty);
+  });
+
+  test('asyncSliceGroupBounds_Basic', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+    g.push(newAsyncSlice(0, 1, t1, t1));
+    g.push(newAsyncSlice(1, 1.5, t1, t1));
+    assert.equal(g.length, 2);
+    g.updateBounds();
+    assert.equal(g.bounds.min, 0);
+    assert.equal(g.bounds.max, 2.5);
+  });
+
+  test('asyncSliceGroupStableId', function() {
+    var model = new tv.c.TraceModel();
+    var process = model.getOrCreateProcess(123);
+    var thread = process.getOrCreateThread(456);
+    var group = new AsyncSliceGroup(thread);
+
+    assert.equal(process.stableId, 123);
+    assert.equal(thread.stableId, '123.456');
+    assert.equal(group.stableId, '123.456.AsyncSliceGroup');
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/counter.html b/trace-viewer/trace_viewer/core/trace_model/counter.html
new file mode 100644
index 0000000..dcde5aa
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/counter.html
@@ -0,0 +1,181 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/counter_series.html">
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/range.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Counter class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+
+  /**
+   * Stores all the samples for a given counter.
+   * @constructor
+   */
+  function Counter(parent, id, category, name) {
+    this.guid_ = tv.b.GUID.allocate();
+
+    this.parent = parent;
+    this.id = id;
+    this.category = category || '';
+    this.name = name;
+
+    this.series_ = [];
+    this.totals = [];
+    this.bounds = new tv.b.Range();
+  }
+
+  Counter.prototype = {
+    __proto__: Object.prototype,
+
+    /*
+     * @return {Number} A globally unique identifier for this counter.
+     */
+    get guid() {
+      return this.guid_;
+    },
+
+    set timestamps(arg) {
+      throw new Error('Bad counter API. No cookie.');
+    },
+
+    set seriesNames(arg) {
+      throw new Error('Bad counter API. No cookie.');
+    },
+
+    set seriesColors(arg) {
+      throw new Error('Bad counter API. No cookie.');
+    },
+
+    set samples(arg) {
+      throw new Error('Bad counter API. No cookie.');
+    },
+
+    addSeries: function(series) {
+      series.counter = this;
+      series.seriesIndex = this.series_.length;
+      this.series_.push(series);
+      return series;
+    },
+
+    getSeries: function(idx) {
+      return this.series_[idx];
+    },
+
+    get series() {
+      return this.series_;
+    },
+
+    get numSeries() {
+      return this.series_.length;
+    },
+
+    get numSamples() {
+      if (this.series_.length === 0)
+        return 0;
+      return this.series_[0].length;
+    },
+
+    get timestamps() {
+      if (this.series_.length === 0)
+        return [];
+      return this.series_[0].timestamps;
+    },
+
+    /**
+     * Obtains min, max, avg, values, start, and end for different series for
+     * a given counter
+     *     getSampleStatistics([0,1])
+     * The statistics objects that this returns are an array of objects, one
+     * object for each series for the counter in the form:
+     * {min: minVal, max: maxVal, avg: avgVal, start: startVal, end: endVal}
+     *
+     * @param {Array.<Number>} Indices to summarize.
+     * @return {Object} An array of statistics. Each element in the array
+     * has data for one of the series in the selected counter.
+     */
+    getSampleStatistics: function(sampleIndices) {
+      sampleIndices.sort();
+
+      var ret = [];
+      this.series_.forEach(function(series) {
+        ret.push(series.getStatistics(sampleIndices));
+      });
+      return ret;
+    },
+
+    /**
+     * Shifts all the timestamps inside this counter forward by the amount
+     * specified.
+     */
+    shiftTimestampsForward: function(amount) {
+      for (var i = 0; i < this.series_.length; ++i)
+        this.series_[i].shiftTimestampsForward(amount);
+    },
+
+    /**
+     * Updates the bounds for this counter based on the samples it contains.
+     */
+    updateBounds: function() {
+      this.totals = [];
+      this.maxTotal = 0;
+      this.bounds.reset();
+
+      if (this.series_.length === 0)
+        return;
+
+      var firstSeries = this.series_[0];
+      var lastSeries = this.series_[this.series_.length - 1];
+
+      this.bounds.addValue(firstSeries.getTimestamp(0));
+      this.bounds.addValue(lastSeries.getTimestamp(lastSeries.length - 1));
+
+      var numSeries = this.numSeries;
+      this.maxTotal = -Infinity;
+
+      // Sum the samples at each timestamp.
+      // Note, this assumes that all series have all timestamps.
+      for (var i = 0; i < firstSeries.length; ++i) {
+        var total = 0;
+        this.series_.forEach(function(series) {
+          total += series.getSample(i).value;
+          this.totals.push(total);
+        }.bind(this));
+
+        this.maxTotal = Math.max(total, this.maxTotal);
+      }
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      for (var i = 0; i < this.series_.length; i++)
+        this.series_[i].iterateAllEvents(callback, opt_this);
+    }
+  };
+
+  /**
+   * Comparison between counters that orders by parent.compareTo, then name.
+   */
+  Counter.compare = function(x, y) {
+    var tmp = x.parent.compareTo(y);
+    if (tmp != 0)
+      return tmp;
+    var tmp = x.name.localeCompare(y.name);
+    if (tmp == 0)
+      return x.tid - y.tid;
+    return tmp;
+  };
+
+  return {
+    Counter: Counter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/counter_sample.html b/trace-viewer/trace_viewer/core/trace_model/counter_sample.html
new file mode 100644
index 0000000..66b827f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/counter_sample.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+  function CounterSample(series, timestamp, value) {
+    tv.c.trace_model.Event.call(this);
+    this.series_ = series;
+    this.timestamp_ = timestamp;
+    this.value_ = value;
+  }
+
+  CounterSample.groupByTimestamp = function(samples) {
+    var samplesByTimestamp = {};
+    for (var i = 0; i < samples.length; i++) {
+      var sample = samples[i];
+      var ts = sample.timestamp;
+      if (!samplesByTimestamp[ts])
+        samplesByTimestamp[ts] = [];
+      samplesByTimestamp[ts].push(sample);
+    }
+    var timestamps = tv.b.dictionaryKeys(samplesByTimestamp);
+    timestamps.sort();
+    var groups = [];
+    for (var i = 0; i < timestamps.length; i++) {
+      var ts = timestamps[i];
+      var group = samplesByTimestamp[ts];
+      group.sort(function(x, y) {
+        return x.series.seriesIndex - y.series.seriesIndex;
+      });
+      groups.push(group);
+    }
+    return groups;
+  }
+
+  CounterSample.prototype = {
+    __proto__: tv.c.trace_model.Event.prototype,
+
+    get series() {
+      return this.series_;
+    },
+
+    get timestamp() {
+      return this.timestamp_;
+    },
+
+    get value() {
+      return this.value_;
+    },
+
+    set timestamp(timestamp) {
+      this.timestamp_ = timestamp;
+    },
+
+    addBoundsToRange: function(range) {
+      range.addValue(this.timestamp);
+    },
+
+    getSampleIndex: function() {
+      return tv.b.findLowIndexInSortedArray(
+          this.series.timestamps,
+          function(x) { return x; },
+          this.timestamp_);
+    },
+
+    get userFriendlyName() {
+      return 'Counter sample from ' + this.series_.title + ' at ' +
+          tv.c.analysis.tsString(this.timestamp);
+    }
+  };
+
+
+  tv.c.trace_model.EventRegistry.register(
+      CounterSample,
+      {
+        name: 'counterSample',
+        pluralName: 'counterSamples',
+        singleViewElementName: 'tv-c-counter-sample-sub-view',
+        multiViewElementName: 'tv-c-counter-sample-sub-view'
+      });
+
+  return {
+    CounterSample: CounterSample
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/counter_sample_test.html b/trace-viewer/trace_viewer/core/trace_model/counter_sample_test.html
new file mode 100644
index 0000000..1584c0f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/counter_sample_test.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/counter.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Counter = tv.c.trace_model.Counter;
+  var CounterSeries = tv.c.trace_model.CounterSeries;
+  var CounterSample = tv.c.trace_model.CounterSample;
+
+  test('groupByTimestamp', function() {
+    var counter = new Counter();
+    var s0 = counter.addSeries(new CounterSeries('x', 0));
+    var s1 = counter.addSeries(new CounterSeries('y', 1));
+
+    var s0_0 = s0.addCounterSample(0, 100);
+    var s0_1 = s1.addCounterSample(0, 200);
+    var s1_0 = s0.addCounterSample(1, 100);
+    var s1_1 = s1.addCounterSample(1, 200);
+
+    var groups = CounterSample.groupByTimestamp([s0_1, s0_0,
+                                                 s1_1, s1_0]);
+    assert.equal(groups.length, 2);
+    assert.deepEqual(groups[0], [s0_0, s0_1]);
+    assert.deepEqual(groups[1], [s1_0, s1_1]);
+  });
+
+  test('getSampleIndex', function() {
+    var ctr = new Counter(null, 0, '', 'myCounter');
+    var s0 = new CounterSeries('a', 0);
+    ctr.addSeries(s0);
+
+    var s0_0 = s0.addCounterSample(0, 0);
+    var s0_1 = s0.addCounterSample(1, 100);
+    assert.equal(s0_0.getSampleIndex(), 0);
+    assert.equal(s0_1.getSampleIndex(), 1);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/counter_series.html b/trace-viewer/trace_viewer/core/trace_model/counter_series.html
new file mode 100644
index 0000000..8967708
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/counter_series.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/counter_sample.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the CounterSeries class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var CounterSample = tv.c.trace_model.CounterSample;
+
+  function CounterSeries(name, color) {
+    this.guid_ = tv.b.GUID.allocate();
+
+    this.name_ = name;
+    this.color_ = color;
+
+    this.timestamps_ = [];
+    this.samples_ = [];
+
+    // Set by counter.addSeries
+    this.counter = undefined;
+    this.seriesIndex = undefined;
+  }
+
+  CounterSeries.prototype = {
+    __proto__: Object.prototype,
+
+    get length() {
+      return this.timestamps_.length;
+    },
+
+    get name() {
+      return this.name_;
+    },
+
+    get color() {
+      return this.color_;
+    },
+
+    get samples() {
+      return this.samples_;
+    },
+
+    get timestamps() {
+      return this.timestamps_;
+    },
+
+    getSample: function(idx) {
+      return this.samples_[idx];
+    },
+
+    getTimestamp: function(idx) {
+      return this.timestamps_[idx];
+    },
+
+    addCounterSample: function(ts, val) {
+      this.timestamps_.push(ts);
+      var sample = new CounterSample(this, ts, val);
+      this.samples_.push(sample);
+      return sample;
+    },
+
+    getStatistics: function(sampleIndices) {
+      var sum = 0;
+      var min = Number.MAX_VALUE;
+      var max = -Number.MAX_VALUE;
+
+      for (var i = 0; i < sampleIndices.length; ++i) {
+        var sample = this.getSample(sampleIndices[i]).value;
+
+        sum += sample;
+        min = Math.min(sample, min);
+        max = Math.max(sample, max);
+      }
+
+      return {
+        min: min,
+        max: max,
+        avg: (sum / sampleIndices.length),
+        start: this.getSample(sampleIndices[0]).value,
+        end: this.getSample(sampleIndices.length - 1).value
+      };
+    },
+
+    shiftTimestampsForward: function(amount) {
+      for (var i = 0; i < this.timestamps_.length; ++i) {
+        this.timestamps_[i] += amount;
+        this.samples_[i].timestamp = this.timestamps_[i];
+      }
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      this.samples_.forEach(callback, opt_this);
+    }
+  };
+
+  return {
+    CounterSeries: CounterSeries
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/counter_test.html b/trace-viewer/trace_viewer/core/trace_model/counter_test.html
new file mode 100644
index 0000000..4f8b22c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/counter_test.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/counter.html">
+<link rel="import" href="/core/trace_model/counter_series.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Counter = tv.c.trace_model.Counter;
+  var CounterSeries = tv.c.trace_model.CounterSeries;
+  var CounterSample = tv.c.trace_model.CounterSample;
+
+  var createCounterWithTwoSeries = function() {
+    var ctr = new Counter(null, 0, '', 'myCounter');
+    var aSeries = new CounterSeries('a', 0);
+    var bSeries = new CounterSeries('b', 0);
+    ctr.addSeries(aSeries);
+    ctr.addSeries(bSeries);
+
+    aSeries.addCounterSample(0, 5);
+    aSeries.addCounterSample(1, 6);
+    aSeries.addCounterSample(2, 5);
+    aSeries.addCounterSample(3, 7);
+
+    bSeries.addCounterSample(0, 10);
+    bSeries.addCounterSample(1, 15);
+    bSeries.addCounterSample(2, 12);
+    bSeries.addCounterSample(3, 16);
+
+    return ctr;
+  };
+
+  test('getSampleStatisticsWithSingleSelection', function() {
+    var ctr = createCounterWithTwoSeries();
+    var ret = ctr.getSampleStatistics([0]);
+
+    assert.equal(ret[0].min, 5);
+    assert.equal(ret[0].max, 5);
+    assert.equal(ret[0].avg, 5);
+    assert.equal(ret[0].start, 5);
+    assert.equal(ret[0].end, 5);
+
+    assert.equal(ret[1].min, 10);
+    assert.equal(ret[1].max, 10);
+    assert.equal(ret[1].avg, 10);
+    assert.equal(ret[1].start, 10);
+    assert.equal(ret[1].end, 10);
+  });
+
+  test('getSampleStatisticsWithMultipleSelections', function() {
+    var ctr = createCounterWithTwoSeries();
+    var ret = ctr.getSampleStatistics([0, 1]);
+
+    assert.equal(ret[0].min, 5);
+    assert.equal(ret[0].max, 6);
+    assert.equal(ret[0].avg, (5 + 6) / 2);
+    assert.equal(ret[0].start, 5);
+    assert.equal(ret[0].end, 6);
+
+    assert.equal(ret[1].min, 10);
+    assert.equal(ret[1].max, 15);
+    assert.equal(ret[1].avg, (10 + 15) / 2);
+    assert.equal(ret[1].start, 10);
+    assert.equal(ret[1].end, 15);
+  });
+
+  test('getSampleStatisticsWithOutofOrderIndices', function() {
+    var ctr = createCounterWithTwoSeries();
+    var ret = ctr.getSampleStatistics([1, 0]);
+
+    assert.equal(ret[0].min, 5);
+    assert.equal(ret[0].max, 6);
+    assert.equal(ret[0].avg, (5 + 6) / 2);
+    assert.equal(ret[0].start, 5);
+    assert.equal(ret[0].end, 6);
+
+    assert.equal(ret[1].min, 10);
+    assert.equal(ret[1].max, 15);
+    assert.equal(ret[1].avg, (10 + 15) / 2);
+    assert.equal(ret[1].start, 10);
+    assert.equal(ret[1].end, 15);
+  });
+
+  test('getSampleStatisticsWithAllSelections', function() {
+    var ctr = createCounterWithTwoSeries();
+    var ret = ctr.getSampleStatistics([1, 0, 2, 3]);
+
+    assert.equal(ret[0].min, 5);
+    assert.equal(ret[0].max, 7);
+    assert.equal(ret[0].avg, (5 + 6 + 5 + 7) / 4);
+    assert.equal(ret[0].start, 5);
+    assert.equal(ret[0].end, 7);
+
+    assert.equal(ret[1].min, 10);
+    assert.equal(ret[1].max, 16);
+    assert.equal(ret[1].avg, (10 + 15 + 12 + 16) / 4);
+    assert.equal(ret[1].start, 10);
+    assert.equal(ret[1].end, 16);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/cpu.html b/trace-viewer/trace_viewer/core/trace_model/cpu.html
new file mode 100644
index 0000000..29c86a7
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/cpu.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/core/trace_model/counter.html">
+<link rel="import" href="/core/trace_model/cpu_slice.html">
+<link rel="import" href="/core/trace_model/thread_time_slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Cpu class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+
+  var Counter = tv.c.trace_model.Counter;
+  var Slice = tv.c.trace_model.Slice;
+  var ThreadTimeSlice = tv.c.trace_model.ThreadTimeSlice;
+  var CpuSlice = tv.c.trace_model.CpuSlice;
+
+  /**
+   * The Cpu represents a Cpu from the kernel's point of view.
+   * @constructor
+   */
+  function Cpu(kernel, number) {
+    if (kernel === undefined || number === undefined)
+      throw new Error('Missing arguments');
+    this.kernel = kernel;
+    this.cpuNumber = number;
+    this.slices = [];
+    this.counters = {};
+    this.bounds = new tv.b.Range();
+    this.samples_ = undefined; // Set during createSubSlices
+
+    // Start timestamp of the last active thread.
+    this.lastActiveTimestamp_ = undefined;
+
+    // Identifier of the last active thread. On Linux, it's a pid while on
+    // Windows it's a thread id.
+    this.lastActiveThread_ = undefined;
+
+    // Name and arguments of the last active thread.
+    this.lastActiveName_ = undefined;
+    this.lastActiveArgs_ = undefined;
+  };
+
+  Cpu.prototype = {
+    /**
+     * @return {TimelineCounter} The counter on this process named 'name',
+     * creating it if it doesn't exist.
+     */
+    getOrCreateCounter: function(cat, name) {
+      var id;
+      if (cat.length)
+        id = cat + '.' + name;
+      else
+        id = name;
+      if (!this.counters[id])
+        this.counters[id] = new Counter(this, id, cat, name);
+      return this.counters[id];
+    },
+
+    /**
+     * Shifts all the timestamps inside this CPU forward by the amount
+     * specified.
+     */
+    shiftTimestampsForward: function(amount) {
+      for (var sI = 0; sI < this.slices.length; sI++)
+        this.slices[sI].start = (this.slices[sI].start + amount);
+      for (var id in this.counters)
+        this.counters[id].shiftTimestampsForward(amount);
+    },
+
+    /**
+     * Updates the range based on the current slices attached to the cpu.
+     */
+    updateBounds: function() {
+      this.bounds.reset();
+      if (this.slices.length) {
+        this.bounds.addValue(this.slices[0].start);
+        this.bounds.addValue(this.slices[this.slices.length - 1].end);
+      }
+      for (var id in this.counters) {
+        this.counters[id].updateBounds();
+        this.bounds.addRange(this.counters[id].bounds);
+      }
+      if (this.samples_ && this.samples_.length) {
+        this.bounds.addValue(this.samples_[0].start);
+        this.bounds.addValue(
+            this.samples_[this.samples_.length - 1].end);
+      }
+    },
+
+    createSubSlices: function() {
+      this.samples_ = this.kernel.model.samples.filter(function(sample) {
+        return sample.cpu == this;
+      }, this);
+    },
+
+    addCategoriesToDict: function(categoriesDict) {
+      for (var i = 0; i < this.slices.length; i++)
+        categoriesDict[this.slices[i].category] = true;
+      for (var id in this.counters)
+        categoriesDict[this.counters[id].category] = true;
+      for (var i = 0; i < this.samples_.length; i++)
+        categoriesDict[this.samples_[i].category] = true;
+    },
+
+    get userFriendlyName() {
+      return 'CPU ' + this.cpuNumber;
+    },
+
+    /*
+     * Returns the index of the slice in the CPU's slices, or undefined.
+     */
+    indexOf: function(cpuSlice) {
+      var i = tv.b.findLowIndexInSortedArray(
+          this.slices,
+          function(slice) { return slice.start; },
+          cpuSlice.start);
+      if (this.slices[i] !== cpuSlice)
+        return undefined;
+      return i;
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      this.slices.forEach(callback, opt_this);
+
+      for (var id in this.counters)
+        this.counters[id].iterateAllEvents(callback, opt_this);
+    },
+
+    /**
+     * Closes the thread running on the CPU. |end_timestamp| is the timestamp
+     * at which the thread was unscheduled. |args| is merged with the arguments
+     * specified when the thread was initially scheduled.
+     */
+    closeActiveThread: function(end_timestamp, args) {
+      // Don't generate a slice if the last active thread is the idle task.
+      if (this.lastActiveThread_ == undefined || this.lastActiveThread_ == 0)
+        return;
+
+      if (end_timestamp < this.lastActiveTimestamp_) {
+        throw new Error('The end timestamp of a thread running on CPU ' +
+                        this.cpuNumber + ' is before its start timestamp.');
+      }
+
+      // Merge |args| with |this.lastActiveArgs_|. If a key is in both
+      // dictionaries, the value from |args| is used.
+      for (var key in args) {
+        this.lastActiveArgs_[key] = args[key];
+      }
+
+      var duration = end_timestamp - this.lastActiveTimestamp_;
+      var slice = new tv.c.trace_model.CpuSlice(
+          '', this.lastActiveName_,
+          tv.b.ui.getColorIdForGeneralPurposeString(this.lastActiveName_),
+          this.lastActiveTimestamp_,
+          this.lastActiveArgs_,
+          duration);
+      slice.cpu = this;
+      this.slices.push(slice);
+
+      // Clear the last state.
+      this.lastActiveTimestamp_ = undefined;
+      this.lastActiveThread_ = undefined;
+      this.lastActiveName_ = undefined;
+      this.lastActiveArgs_ = undefined;
+    },
+
+    switchActiveThread: function(timestamp, old_thread_args, new_thread_id,
+                                 new_thread_name, new_thread_args) {
+      // Close the previous active thread and generate a slice.
+      this.closeActiveThread(timestamp, old_thread_args);
+
+      // Keep track of the new thread.
+      this.lastActiveTimestamp_ = timestamp;
+      this.lastActiveThread_ = new_thread_id;
+      this.lastActiveName_ = new_thread_name;
+      this.lastActiveArgs_ = new_thread_args;
+    },
+
+    get samples() {
+      return this.samples_;
+    }
+  };
+
+  /**
+   * Comparison between processes that orders by cpuNumber.
+   */
+  Cpu.compare = function(x, y) {
+    return x.cpuNumber - y.cpuNumber;
+  };
+
+
+  return {
+    Cpu: Cpu
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/cpu_slice.html b/trace-viewer/trace_viewer/core/trace_model/cpu_slice.html
new file mode 100644
index 0000000..4b1aef9
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/cpu_slice.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/core/trace_model/thread_time_slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Cpu class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+
+  var Slice = tv.c.trace_model.Slice;
+
+  /**
+   * A CpuSlice represents a slice of time on a CPU.
+   *
+   * @constructor
+   */
+  function CpuSlice(cat, title, colorId, start, args, opt_duration) {
+    Slice.apply(this, arguments);
+    this.threadThatWasRunning = undefined;
+    this.cpu = undefined;
+  }
+
+  CpuSlice.prototype = {
+    __proto__: Slice.prototype,
+
+    get analysisTypeName() {
+      return 'tv.c.analysis.CpuSlice';
+    },
+
+    getAssociatedTimeslice: function() {
+      if (!this.threadThatWasRunning)
+        return undefined;
+      var timeSlices = this.threadThatWasRunning.timeSlices;
+      for (var i = 0; i < timeSlices.length; i++) {
+        var timeSlice = timeSlices[i];
+        if (timeSlice.start !== this.start)
+          continue;
+        if (timeSlice.duration !== this.duration)
+          continue;
+        return timeSlice;
+      }
+      return undefined;
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      CpuSlice,
+      {
+        name: 'cpuSlice',
+        pluralName: 'cpuSlices',
+        singleViewElementName: 'tv-c-single-cpu-slice-sub-view',
+        multiViewElementName: 'tv-c-multi-slice-sub-view'
+      });
+
+  return {
+    CpuSlice: CpuSlice
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/cpu_test.html b/trace-viewer/trace_viewer/core/trace_model/cpu_test.html
new file mode 100644
index 0000000..f45476d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/cpu_test.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Cpu = tv.c.trace_model.Cpu;
+
+  test('cpuBounds_Empty', function() {
+    var cpu = new Cpu({}, 1);
+    cpu.updateBounds();
+    assert.isUndefined(cpu.bounds.min);
+    assert.isUndefined(cpu.bounds.max);
+  });
+
+  test('cpuBounds_OneSlice', function() {
+    var cpu = new Cpu({}, 1);
+    cpu.slices.push(tv.c.test_utils.newSlice(1, 3));
+    cpu.updateBounds();
+    assert.equal(cpu.bounds.min, 1);
+    assert.equal(cpu.bounds.max, 4);
+  });
+
+  test('getOrCreateCounter', function() {
+    var cpu = new Cpu({}, 1);
+    var ctrBar = cpu.getOrCreateCounter('foo', 'bar');
+    var ctrBar2 = cpu.getOrCreateCounter('foo', 'bar');
+    assert.equal(ctrBar, ctrBar2);
+  });
+
+  test('shiftTimestampsForward', function() {
+    var cpu = new Cpu({}, 1);
+    var ctr = cpu.getOrCreateCounter('foo', 'bar');
+    cpu.slices.push(tv.c.test_utils.newSlice(1, 3));
+    var shiftCount = 0;
+    ctr.shiftTimestampsForward = function(ts) {
+      if (ts == 0.32)
+        shiftCount++;
+    };
+    cpu.slices.push(tv.c.test_utils.newSlice(1, 3));
+    cpu.shiftTimestampsForward(0.32);
+    assert.equal(1, shiftCount);
+    assert.equal(cpu.slices[0].start, 1.32);
+  });
+
+
+  function newCpuSliceNamed(cpu, name, start, duration, opt_thread) {
+    var s = new tv.c.trace_model.CpuSlice(
+        'cat', name, 0, start, {}, duration);
+    s.cpu = cpu;
+    if (opt_thread)
+      s.threadThatWasRunning = opt_thread;
+    return s;
+  }
+
+  function newTimeSliceNamed(thread, name, start, duration, opt_cpu) {
+    var s = new tv.c.trace_model.ThreadTimeSlice(
+        thread, 'cat', name, 0, start, {}, duration);
+    if (opt_cpu)
+      s.cpuOnWhichThreadWasRunning = opt_cpu;
+    return s;
+  }
+
+  test('getTimesliceForCpuSlice', function() {
+    var m = new tv.c.TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+    var t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    t2.timeSlices = [newTimeSliceNamed(t2, 'Running', 0, 10, cpu),
+                     newTimeSliceNamed(t2, 'Sleeping', 10, 10),
+                     newTimeSliceNamed(t2, 'Running', 20, 10, cpu)];
+    cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2),
+                  newCpuSliceNamed(cpu, 'x', 20, 10, t2)];
+    assert.equal(cpu.slices[0].getAssociatedTimeslice(), t2.timeSlices[0]);
+    assert.equal(cpu.slices[1].getAssociatedTimeslice(), t2.timeSlices[2]);
+
+    assert.equal(t2.timeSlices[0].getAssociatedCpuSlice(), cpu.slices[0]);
+    assert.isUndefined(t2.timeSlices[1].getAssociatedCpuSlice());
+    assert.equal(t2.timeSlices[2].getAssociatedCpuSlice(), cpu.slices[1]);
+
+    assert.equal(cpu.indexOf(cpu.slices[0]), 0);
+    assert.equal(cpu.indexOf(cpu.slices[1]), 1);
+
+    assert.equal(t2.indexOfTimeSlice(t2.timeSlices[0]), 0);
+    assert.equal(t2.indexOfTimeSlice(t2.timeSlices[1]), 1);
+    assert.equal(t2.indexOfTimeSlice(t2.timeSlices[2]), 2);
+  });
+
+  test('putToSleepFor', function() {
+    var m = new tv.c.TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+
+    var t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var t3 = m.getOrCreateProcess(1).getOrCreateThread(3);
+    t2.timeSlices = [newTimeSliceNamed(t2, 'Running', 0, 10, cpu),
+                     newTimeSliceNamed(t2, 'Sleeping', 10, 10),
+                     newTimeSliceNamed(t2, 'Running', 20, 10, cpu)];
+    t3.timeSlices = [newTimeSliceNamed(t3, 'Running', 10, 5, cpu)];
+    cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2),
+                   newCpuSliceNamed(cpu, 'x', 10, 5, t3),
+                   newCpuSliceNamed(cpu, 'x', 20, 10, t2)];
+
+    // At timeslice 0, the thread is running.
+    assert.isUndefined(t2.timeSlices[0].getCpuSliceThatTookCpu());
+
+    // t2 lost the cpu to t3 at t=10
+    assert.equal(
+        cpu.slices[1],
+        t2.timeSlices[1].getCpuSliceThatTookCpu());
+  });
+
+  test('putToSleepForNothing', function() {
+    var m = new tv.c.TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+
+    var t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var t3 = m.getOrCreateProcess(1).getOrCreateThread(3);
+    t2.timeSlices = [newTimeSliceNamed(t2, 'Running', 0, 10, cpu),
+                     newTimeSliceNamed(t2, 'Sleeping', 10, 10),
+                     newTimeSliceNamed(t2, 'Running', 20, 10, cpu)];
+    t3.timeSlices = [newTimeSliceNamed(t3, 'Running', 15, 5, cpu)];
+    cpu.slices = [newCpuSliceNamed(cpu, 'x', 0, 10, t2),
+                   newCpuSliceNamed(cpu, 'x', 15, 5, t3),
+                   newCpuSliceNamed(cpu, 'x', 20, 10, t2)];
+    assert.isUndefined(t2.timeSlices[1].getCpuSliceThatTookCpu());
+  });
+
+  test('switchActiveThread', function() {
+    var m = new tv.c.TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+
+    cpu.switchActiveThread(5, {}, 0, 'idle thread', {});
+    cpu.switchActiveThread(10, {}, 1, 'thread one', {a: 1});
+    cpu.switchActiveThread(15, {b: 2}, 2, 'thread two', {c: 3});
+    cpu.switchActiveThread(30, {c: 4, d: 5}, 3, 'thread three', {e: 6});
+    cpu.closeActiveThread(40, {f: 7});
+    cpu.switchActiveThread(50, {}, 4, 'thread four', {g: 8});
+    cpu.switchActiveThread(60, {}, 1, 'thread one', {});
+    cpu.closeActiveThread(70, {});
+
+    assert.equal(cpu.slices.length, 5);
+
+    assert.equal(cpu.slices[0].title, 'thread one');
+    assert.equal(cpu.slices[0].start, 10);
+    assert.equal(cpu.slices[0].duration, 5);
+    assert.equal(Object.keys(cpu.slices[0].args).length, 2);
+    assert.equal(cpu.slices[0].args.a, 1);
+    assert.equal(cpu.slices[0].args.b, 2);
+
+    assert.equal(cpu.slices[1].title, 'thread two');
+    assert.equal(cpu.slices[1].start, 15);
+    assert.equal(cpu.slices[1].duration, 15);
+    assert.equal(Object.keys(cpu.slices[1].args).length, 2);
+    assert.equal(cpu.slices[1].args.c, 4);
+    assert.equal(cpu.slices[1].args.d, 5);
+
+    assert.equal(cpu.slices[2].title, 'thread three');
+    assert.equal(cpu.slices[2].start, 30);
+    assert.equal(cpu.slices[2].duration, 10);
+    assert.equal(Object.keys(cpu.slices[2].args).length, 2);
+    assert.equal(cpu.slices[2].args.e, 6);
+    assert.equal(cpu.slices[2].args.f, 7);
+
+    assert.equal(cpu.slices[3].title, 'thread four');
+    assert.equal(cpu.slices[3].start, 50);
+    assert.equal(cpu.slices[3].duration, 10);
+    assert.equal(Object.keys(cpu.slices[3].args).length, 1);
+    assert.equal(cpu.slices[3].args.g, 8);
+
+    assert.equal(cpu.slices[4].title, 'thread one');
+    assert.equal(cpu.slices[4].start, 60);
+    assert.equal(cpu.slices[4].duration, 10);
+    assert.equal(Object.keys(cpu.slices[4].args).length, 0);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/event.html b/trace-viewer/trace_viewer/core/trace_model/event.html
new file mode 100644
index 0000000..59f4648
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/event.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/extension_registry.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Event class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+
+  /**
+   * The SelectionState enum defines how Events are displayed in the view.
+   */
+  var SelectionState = {
+    NONE: 0,
+    SELECTED: 1,
+    HIGHLIGHTED: 2,
+    DIMMED: 3
+  };
+
+  // Cached values for getCategoryParts.
+  var categoryPartsFor = {};
+
+  /**
+   * Categories are stored in comma-separated form, e.g: 'a,b' meaning
+   * that the event is part of the a and b category.
+   *
+   * This function returns the category split by string, caching the
+   * array for performance.
+   *
+   * Do not mutate the returned array!!!!
+   */
+  function getCategoryParts(category) {
+    var parts = categoryPartsFor[category];
+    if (parts !== undefined)
+      return parts;
+    parts = category.split(',');
+    categoryPartsFor[category] = parts;
+    return parts;
+  }
+
+  /**
+   * An Event is the base type for any non-container, selectable piece
+   * of data in the trace model.
+   *
+   * @constructor
+   */
+  function Event() {
+    this.guid_ = tv.b.GUID.allocate();
+    this.selectionState = SelectionState.NONE;
+  }
+
+  Event.prototype = {
+    get guid() {
+      return this.guid_;
+    },
+
+    get selected() {
+      return this.selectionState === SelectionState.SELECTED;
+    }
+  };
+
+  // Create the type registry.
+  function EventRegistry() {
+  }
+  var options = new tv.b.ExtensionRegistryOptions(tv.b.BASIC_REGISTRY_MODE);
+  options.mandatoryBaseType = Event;
+  tv.b.decorateExtensionRegistry(EventRegistry, options);
+
+  // Enforce all options objects have the right fields.
+  EventRegistry.addEventListener('will-register', function(e) {
+    var metadata = e.typeInfo.metadata;
+
+    if (metadata.name === undefined)
+      throw new Error('Registered events must provide name metadata');
+    var i = tv.b.findFirstInArray(
+      EventRegistry.getAllRegisteredTypeInfos(),
+      function(x) { return x.metadata.name === metadata.name; });
+    if (i !== undefined)
+      throw new Error('Event type with that name already registered');
+
+    if (metadata.pluralName === undefined)
+      throw new Error('Registered events must provide pluralName metadata');
+    if (metadata.singleViewElementName === undefined) {
+      throw new Error('Registered events must provide ' +
+                      'singleViewElementName metadata');
+    }
+    if (metadata.multiViewElementName === undefined) {
+      throw new Error('Registered events must provide ' +
+                      'multiViewElementName metadata');
+    }
+  });
+
+  // Helper: lookup Events indexed by type name.
+  var eventsByTypeName = undefined;
+  EventRegistry.getEventTypeInfoByTypeName = function(typeName) {
+    if (eventsByTypeName === undefined) {
+      eventsByTypeName = {};
+      EventRegistry.getAllRegisteredTypeInfos().forEach(function(typeInfo) {
+        eventsByTypeName[typeInfo.metadata.name] = typeInfo;
+      });
+    }
+    return eventsByTypeName[typeName];
+  }
+
+  // Ensure eventsByTypeName stays current.
+  EventRegistry.addEventListener('registry-changed', function() {
+    eventsByTypeName = undefined;
+  });
+
+  function convertCamelCaseToTitleCase(name) {
+    var result = name.replace(/[A-Z]/g, ' $&');
+    result = result.charAt(0).toUpperCase() + result.slice(1);
+    return result;
+  }
+
+  EventRegistry.getUserFriendlySingularName = function(typeName) {
+    var typeInfo = EventRegistry.getEventTypeInfoByTypeName(typeName);
+    var str = typeInfo.metadata.name;
+    return convertCamelCaseToTitleCase(str);
+  };
+
+  EventRegistry.getUserFriendlyPluralName = function(typeName) {
+    var typeInfo = EventRegistry.getEventTypeInfoByTypeName(typeName);
+    var str = typeInfo.metadata.pluralName;
+    return convertCamelCaseToTitleCase(str);
+  };
+
+  return {
+    Event: Event,
+    EventRegistry: EventRegistry,
+    SelectionState: SelectionState
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/event_container.html b/trace-viewer/trace_viewer/core/trace_model/event_container.html
new file mode 100644
index 0000000..8b7ba32
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/event_container.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the base class for all container classes.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  function EventContainer() {
+  }
+
+  EventContainer.prototype = {
+    /*
+     * @return {String} An identifier that is made up of this parent's
+     *    stableIdentifier plus this countainer identifier.
+     */
+    get stableId() {
+      throw new Error('Not implemented');
+    }
+  };
+
+  return {
+    EventContainer: EventContainer
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/event_test.html b/trace-viewer/trace_viewer/core/trace_model/event_test.html
new file mode 100644
index 0000000..80ee278
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/event_test.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Event = tv.c.trace_model.Event;
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/flow_event.html b/trace-viewer/trace_viewer/core/trace_model/flow_event.html
new file mode 100644
index 0000000..e222d58
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/flow_event.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Flow class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A Flow represents an interval of time plus parameters associated
+   * with that interval.
+   *
+   * @constructor
+   */
+  function FlowEvent(category, id, title, colorId, start, args, opt_duration) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+
+    this.category = category || '';
+    this.title = title;
+    this.colorId = colorId;
+    this.start = start;
+    this.args = args;
+
+    this.id = id;
+
+    this.startSlice = undefined;
+    this.endSlice = undefined;
+
+    if (opt_duration !== undefined)
+      this.duration = opt_duration;
+  }
+
+  FlowEvent.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+    get userFriendlyName() {
+      return 'Flow event named ' + this.title + ' at ' +
+          tv.c.analysis.tsString(this.timestamp);
+    }
+};
+
+  tv.c.trace_model.EventRegistry.register(
+      FlowEvent,
+      {
+        name: 'flowEvent',
+        pluralName: 'flowEvents',
+        singleViewElementName: 'tv-c-single-flow-event-sub-view',
+        multiViewElementName: 'tv-c-multi-flow-event-sub-view'
+      });
+
+  return {
+    FlowEvent: FlowEvent
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/frame.html b/trace-viewer/trace_viewer/core/trace_model/frame.html
new file mode 100644
index 0000000..3d2ebf8
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/frame.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/statistics.html">
+<link rel="import" href="/core/trace_model/event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class describing rendered frames.
+ *
+ * Because a frame is produced by multiple threads, it does not inherit from
+ * TimedEvent, and has no duration.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var Statistics = tv.b.Statistics;
+
+  /**
+   * @constructor
+   * @param {Array} threadTimeRanges Array of {thread, start, end}
+   *     for each thread, describing the critical path of the frame
+   */
+  function Frame(threadTimeRanges) {
+    tv.c.trace_model.TimedEvent.call(this);
+
+    this.threadTimeRanges = threadTimeRanges;
+
+    this.start = Statistics.min(
+        threadTimeRanges, function(x) { return x.start; });
+    this.end = Statistics.max(
+        threadTimeRanges, function(x) { return x.end; });
+    this.totalDuration = Statistics.sum(
+        threadTimeRanges, function(x) { return x.end - x.start; });
+  };
+
+  Frame.prototype = {
+    __proto__: tv.c.trace_model.Event.prototype,
+
+    shiftTimestampsForward: function(amount) {
+      this.start += amount;
+      this.end += amount;
+
+      for (var i = 1; i < this.threadTimeRanges.length; i++) {
+        this.threadTimeRanges[i].start += amount;
+        this.threadTimeRanges[i].end += amount;
+      }
+    },
+
+    addBoundsToRange: function(range) {
+      range.addValue(this.start);
+      range.addValue(this.end);
+    }
+  };
+
+  // TODO(ccraik): register event, once view exists
+
+  return {
+    Frame: Frame
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/global_memory_dump.html b/trace-viewer/trace_viewer/core/trace_model/global_memory_dump.html
new file mode 100644
index 0000000..9de5f5b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/global_memory_dump.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the GlobalMemoryDump class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * The GlobalMemoryDump represents a simultaneous memory dump of all
+   * processes.
+   * @constructor
+   */
+  function GlobalMemoryDump(model, start, opt_args) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+    this.model = model;
+    this.processMemoryDumps = {};
+    this.args = opt_args;
+  };
+
+  GlobalMemoryDump.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+    shiftTimestampsForward: function(amount) {
+      this.start += amount;
+    },
+
+    get userFriendlyName() {
+      return 'Global memory dump ' + ' at ' +
+          tv.c.analysis.tsString(this.start);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      GlobalMemoryDump,
+      {
+        name: 'globalMemoryDump',
+        pluralName: 'globalMemoryDumps',
+        singleViewElementName: 'tv-c-single-global-memory-dump-sub-view',
+        multiViewElementName: 'tv-c-multi-global-memory-dump-sub-view'
+      });
+
+  return {
+    GlobalMemoryDump: GlobalMemoryDump
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/instant_event.html b/trace-viewer/trace_viewer/core/trace_model/instant_event.html
new file mode 100644
index 0000000..dffc983
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/instant_event.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the InstantEvent class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var InstantEventType = {
+    GLOBAL: 1,
+    PROCESS: 2
+  };
+
+  function InstantEvent(category, title, colorId, start, args) {
+    tv.c.trace_model.TimedEvent.call(this);
+
+    this.category = category || '';
+    this.title = title;
+    this.colorId = colorId;
+    this.start = start;
+    this.args = args;
+
+    this.type = undefined;
+  };
+
+  InstantEvent.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype
+  };
+
+  function GlobalInstantEvent(category, title, colorId, start, args) {
+    InstantEvent.apply(this, arguments);
+    this.type = InstantEventType.GLOBAL;
+  };
+
+  GlobalInstantEvent.prototype = {
+    __proto__: InstantEvent.prototype,
+    get userFriendlyName() {
+      return 'Global instant event ' + this.title + ' @ ' +
+          this.tsString(start);
+    }
+  };
+
+  function ProcessInstantEvent(category, title, colorId, start, args) {
+    InstantEvent.apply(this, arguments);
+    this.type = InstantEventType.PROCESS;
+  };
+
+  ProcessInstantEvent.prototype = {
+    __proto__: InstantEvent.prototype,
+
+    get userFriendlyName() {
+      return 'Process-level instant event ' + this.title + ' @ ' +
+          this.tsString(start);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      InstantEvent,
+      {
+        name: 'instantEvent',
+        pluralName: 'instantEvents',
+        singleViewElementName: 'tv-c-single-instant-event-sub-view',
+        multiViewElementName: 'tv-c-multi-instant-event-sub-view'
+      });
+
+  return {
+    GlobalInstantEvent: GlobalInstantEvent,
+    ProcessInstantEvent: ProcessInstantEvent,
+
+    InstantEventType: InstantEventType,
+    InstantEvent: InstantEvent
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/interaction_record.html b/trace-viewer/trace_viewer/core/trace_model/interaction_record.html
new file mode 100644
index 0000000..d873774
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/interaction_record.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+  function InteractionRecord(title, colorId, start, duration) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+    this.title = title;
+    this.colorId = colorId;
+    this.duration = duration;
+  }
+  InteractionRecord.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+    get subSlices() {
+      return [];
+    },
+
+    get userFriendlyName() {
+      return this.title + ' interaction at ' +
+          tv.c.analysis.tsString(this.start);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      InteractionRecord,
+      {
+        name: 'interaction',
+        pluralName: 'interactions',
+        singleViewElementName: 'tv-c-single-interaction-record-sub-view',
+        multiViewElementName: 'tv-c-multi-interaction-record-sub-view'
+      });
+
+  return {
+    InteractionRecord: InteractionRecord
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/core/trace_model/kernel.html b/trace-viewer/trace_viewer/core/trace_model/kernel.html
new file mode 100644
index 0000000..a298064
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/kernel.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/cpu.html">
+<link rel="import" href="/core/trace_model/process_base.html">
+<link rel="import" href="/base/iteration_helpers.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Process class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var Cpu = tv.c.trace_model.Cpu;
+  var ProcessBase = tv.c.trace_model.ProcessBase;
+
+  /**
+   * The Kernel represents kernel-level objects in the
+   * model.
+   * @constructor
+   */
+  function Kernel(model) {
+    if (model === undefined)
+      throw new Error('model must be provided');
+    ProcessBase.call(this, model);
+    this.cpus = {};
+    this.softwareMeasuredCpuCount_ = undefined;
+  };
+
+  /**
+   * Comparison between kernels is pretty meaningless.
+   */
+  Kernel.compare = function(x, y) {
+    return 0;
+  };
+
+  Kernel.prototype = {
+    __proto__: ProcessBase.prototype,
+
+    compareTo: function(that) {
+      return Kernel.compare(this, that);
+    },
+
+    get userFriendlyName() {
+      return 'Kernel';
+    },
+
+    get userFriendlyDetails() {
+      return 'Kernel';
+    },
+
+    get stableId() {
+      return 'Kernel';
+    },
+
+    /**
+     * @return {Cpu} Gets a specific Cpu or creates one if
+     * it does not exist.
+     */
+    getOrCreateCpu: function(cpuNumber) {
+      if (!this.cpus[cpuNumber])
+        this.cpus[cpuNumber] = new Cpu(this, cpuNumber);
+      return this.cpus[cpuNumber];
+    },
+
+    get softwareMeasuredCpuCount() {
+      return this.softwareMeasuredCpuCount_;
+    },
+
+    set softwareMeasuredCpuCount(softwareMeasuredCpuCount) {
+      if (this.softwareMeasuredCpuCount_ !== undefined &&
+          this.softwareMeasuredCpuCount_ !== softwareMeasuredCpuCount) {
+        throw new Error(
+            'Cannot change the softwareMeasuredCpuCount once it is set');
+      }
+
+      this.softwareMeasuredCpuCount_ = softwareMeasuredCpuCount;
+    },
+
+    /**
+     * Estimates how many cpus are in the system, for use in system load
+     * estimation.
+     *
+     * If kernel trace was provided, uses that data. Otherwise, uses the
+     * software measured cpu count.
+     */
+    get bestGuessAtCpuCount() {
+      var realCpuCount = tv.b.dictionaryLength(this.cpus);
+      if (realCpuCount !== 0)
+        return realCpuCount;
+      return this.softwareMeasuredCpuCount;
+    },
+
+    shiftTimestampsForward: function(amount) {
+      ProcessBase.prototype.shiftTimestampsForward.call(this, amount);
+      for (var cpuNumber in this.cpus)
+        this.cpus[cpuNumber].shiftTimestampsForward(amount);
+    },
+
+    updateBounds: function() {
+      ProcessBase.prototype.updateBounds.call(this);
+      for (var cpuNumber in this.cpus) {
+        var cpu = this.cpus[cpuNumber];
+        cpu.updateBounds();
+        this.bounds.addRange(cpu.bounds);
+      }
+    },
+
+    createSubSlices: function() {
+      ProcessBase.prototype.createSubSlices.call(this);
+      for (var cpuNumber in this.cpus) {
+        var cpu = this.cpus[cpuNumber];
+        cpu.createSubSlices();
+      }
+    },
+
+    addCategoriesToDict: function(categoriesDict) {
+      ProcessBase.prototype.addCategoriesToDict.call(this, categoriesDict);
+      for (var cpuNumber in this.cpus)
+        this.cpus[cpuNumber].addCategoriesToDict(categoriesDict);
+    },
+
+    getSettingsKey: function() {
+      return 'kernel';
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      for (var cpuNumber in this.cpus)
+        this.cpus[cpuNumber].iterateAllEvents(callback, opt_this);
+
+      ProcessBase.prototype.iterateAllEvents.call(this, callback, opt_this);
+    },
+
+    iterateAllEventContainers: function(callback) {
+      callback(this);
+    }
+  };
+
+  return {
+    Kernel: Kernel
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/kernel_test.html b/trace-viewer/trace_viewer/core/trace_model/kernel_test.html
new file mode 100644
index 0000000..7455128
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/kernel_test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/trace_model/kernel.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('bestGuessAtCpuCountWithNoData', function() {
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+    });
+    assert.isUndefined(m.kernel.bestGuessAtCpuCount);
+  });
+
+  test('bestGuessAtCpuCountWithCpuData', function() {
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+      var c1 = m.kernel.getOrCreateCpu(1);
+      var c2 = m.kernel.getOrCreateCpu(2);
+    });
+    assert.equal(m.kernel.bestGuessAtCpuCount, 2);
+  });
+
+  test('bestGuessAtCpuCountWithSoftwareCpuCount', function() {
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+      m.kernel.softwareMeasuredCpuCount = 2;
+    });
+    assert.equal(m.kernel.bestGuessAtCpuCount, 2);
+  });
+
+  test('kernelStableId', function() {
+    var model = new tv.c.TraceModel();
+
+    assert.equal(model.kernel.stableId, 'Kernel');
+  });
+
+  test('kernelTimeShifting', function() {
+    var importOptions = new tv.c.ImportOptions();
+    importOptions.shiftWorldToZero = true;
+    importOptions.pruneEmptyContainers = false;
+    importOptions.customizeModelCallback = function(m) {
+      var ctr = m.kernel.getOrCreateCounter('cat', 'ctr');
+      var c0 = new tv.c.trace_model.CounterSeries('a', 0);
+      ctr.addSeries(c0);
+      c0.addCounterSample(100, 5);
+      c0.addCounterSample(200, 5);
+    };
+    var m = new tv.c.TraceModel([], importOptions);
+    var ctr = m.kernel.counters['cat.ctr'];
+    assert.equal(ctr.series[0].samples[0].timestamp, 0);
+    ctr.series[0].samples[0].ts == 100;
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/object_collection.html b/trace-viewer/trace_viewer/core/trace_model/object_collection.html
new file mode 100644
index 0000000..bf7d1b9
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/object_collection.html
@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/object_instance.html">
+<link rel="import" href="/core/trace_model/time_to_object_instance_map.html">
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ObjectCollection class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var ObjectInstance = tv.c.trace_model.ObjectInstance;
+
+  /**
+   * A collection of object instances and their snapshots, accessible by id and
+   * time, or by object name.
+   *
+   * @constructor
+   */
+  function ObjectCollection(parent) {
+    this.parent = parent;
+    this.bounds = new tv.b.Range();
+    this.instanceMapsById_ = {}; // id -> TimeToObjectInstanceMap
+    this.instancesByTypeName_ = {};
+    this.createObjectInstance_ = this.createObjectInstance_.bind(this);
+  }
+
+  ObjectCollection.prototype = {
+    __proto__: Object.prototype,
+
+    createObjectInstance_: function(
+        parent, id, category, name, creationTs, opt_baseTypeName) {
+      var constructor = tv.c.trace_model.ObjectInstance.getConstructor(
+          category, name);
+      var instance = new constructor(
+          parent, id, category, name, creationTs, opt_baseTypeName);
+      var typeName = instance.typeName;
+      var instancesOfTypeName = this.instancesByTypeName_[typeName];
+      if (!instancesOfTypeName) {
+        instancesOfTypeName = [];
+        this.instancesByTypeName_[typeName] = instancesOfTypeName;
+      }
+      instancesOfTypeName.push(instance);
+      return instance;
+    },
+
+    getOrCreateInstanceMap_: function(id) {
+      var instanceMap = this.instanceMapsById_[id];
+      if (instanceMap)
+        return instanceMap;
+      instanceMap = new tv.c.trace_model.TimeToObjectInstanceMap(
+          this.createObjectInstance_, this.parent, id);
+      this.instanceMapsById_[id] = instanceMap;
+      return instanceMap;
+    },
+
+    idWasCreated: function(id, category, name, ts) {
+      var instanceMap = this.getOrCreateInstanceMap_(id);
+      return instanceMap.idWasCreated(category, name, ts);
+    },
+
+    addSnapshot: function(id, category, name, ts, args, opt_baseTypeName) {
+      var instanceMap = this.getOrCreateInstanceMap_(id);
+      var snapshot = instanceMap.addSnapshot(
+          category, name, ts, args, opt_baseTypeName);
+      if (snapshot.objectInstance.category != category) {
+        var msg = 'Added snapshot name=' + name + ' with cat=' + category +
+            ' impossible. It instance was created/snapshotted with cat=' +
+            snapshot.objectInstance.category + ' name=' +
+            snapshot.objectInstance.name;
+        throw new Error(msg);
+      }
+      if (opt_baseTypeName &&
+          snapshot.objectInstance.baseTypeName != opt_baseTypeName) {
+        throw new Error('Could not add snapshot with baseTypeName=' +
+                        opt_baseTypeName + '. It ' +
+                        'was previously created with name=' +
+                        snapshot.objectInstance.baseTypeName);
+      }
+      if (snapshot.objectInstance.name != name) {
+        throw new Error('Could not add snapshot with name=' + name + '. It ' +
+                        'was previously created with name=' +
+                        snapshot.objectInstance.name);
+      }
+      return snapshot;
+    },
+
+    idWasDeleted: function(id, category, name, ts) {
+      var instanceMap = this.getOrCreateInstanceMap_(id);
+      var deletedInstance = instanceMap.idWasDeleted(category, name, ts);
+      if (!deletedInstance)
+        return;
+      if (deletedInstance.category != category) {
+        var msg = 'Deleting object ' + deletedInstance.name +
+            ' with a different category ' +
+            'than when it was created. It previous had cat=' +
+            deletedInstance.category + ' but the delete command ' +
+            'had cat=' + category;
+        throw new Error(msg);
+      }
+      if (deletedInstance.baseTypeName != name) {
+        throw new Error('Deletion requested for name=' +
+                        name + ' could not proceed: ' +
+                        'An existing object with baseTypeName=' +
+                        deletedInstance.baseTypeName + ' existed.');
+      }
+    },
+
+    autoDeleteObjects: function(maxTimestamp) {
+      tv.b.iterItems(this.instanceMapsById_, function(id, i2imap) {
+        var lastInstance = i2imap.lastInstance;
+        if (lastInstance.deletionTs != Number.MAX_VALUE)
+          return;
+        i2imap.idWasDeleted(
+            lastInstance.category, lastInstance.name, maxTimestamp);
+        // idWasDeleted will cause lastInstance.deletionTsWasExplicit to be set
+        // to true. Unset it here.
+        lastInstance.deletionTsWasExplicit = false;
+      });
+    },
+
+    getObjectInstanceAt: function(id, ts) {
+      var instanceMap = this.instanceMapsById_[id];
+      if (!instanceMap)
+        return undefined;
+      return instanceMap.getInstanceAt(ts);
+    },
+
+    getSnapshotAt: function(id, ts) {
+      var instance = this.getObjectInstanceAt(id, ts);
+      if (!instance)
+        return undefined;
+      return instance.getSnapshotAt(ts);
+    },
+
+    iterObjectInstances: function(iter, opt_this) {
+      opt_this = opt_this || this;
+      tv.b.iterItems(this.instanceMapsById_, function(id, i2imap) {
+        i2imap.instances.forEach(iter, opt_this);
+      });
+    },
+
+    getAllObjectInstances: function() {
+      var instances = [];
+      this.iterObjectInstances(function(i) { instances.push(i); });
+      return instances;
+    },
+
+    getAllInstancesNamed: function(name) {
+      return this.instancesByTypeName_[name];
+    },
+
+    getAllInstancesByTypeName: function() {
+      return this.instancesByTypeName_;
+    },
+
+    preInitializeAllObjects: function() {
+      this.iterObjectInstances(function(instance) {
+        instance.preInitialize();
+      });
+    },
+
+    initializeAllObjects: function() {
+      this.iterObjectInstances(function(instance) {
+        instance.initialize();
+      });
+    },
+
+    initializeInstances: function() {
+      this.iterObjectInstances(function(instance) {
+        instance.initialize();
+      });
+    },
+
+    updateBounds: function() {
+      this.bounds.reset();
+      this.iterObjectInstances(function(instance) {
+        instance.updateBounds();
+        this.bounds.addRange(instance.bounds);
+      }, this);
+    },
+
+    shiftTimestampsForward: function(amount) {
+      this.iterObjectInstances(function(instance) {
+        instance.shiftTimestampsForward(amount);
+      });
+    },
+
+    addCategoriesToDict: function(categoriesDict) {
+      this.iterObjectInstances(function(instance) {
+        categoriesDict[instance.category] = true;
+      });
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      this.iterObjectInstances(function(instance) {
+        callback.call(this, instance);
+        instance.snapshots.forEach(callback);
+      }, opt_this);
+    }
+  };
+
+  return {
+    ObjectCollection: ObjectCollection
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/object_collection_test.html b/trace-viewer/trace_viewer/core/trace_model/object_collection_test.html
new file mode 100644
index 0000000..51ce02f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/object_collection_test.html
@@ -0,0 +1,200 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/object_collection.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var TestObjectInstance = function(parent, id, category, name, creationTs) {
+    tv.c.trace_model.ObjectInstance.call(
+        this, parent, id, category, name, creationTs);
+  };
+
+  TestObjectInstance.prototype = {
+    __proto__: tv.c.trace_model.ObjectInstance.prototype
+  };
+
+  test('objectInstanceSubtype', function() {
+    // Register that TestObjects are bound to TestObjectInstance.
+    tv.c.trace_model.ObjectInstance.register(
+        TestObjectInstance,
+        {typeName: 'TestObject'});
+
+    try {
+      var collection = new tv.c.trace_model.ObjectCollection({ });
+      collection.idWasCreated(
+          '0x1000', 'tv.e.cc', 'Frame', 10);
+      collection.idWasDeleted(
+          '0x1000', 'tv.e.cc', 'Frame', 15);
+      collection.idWasCreated(
+          '0x1000', 'skia', 'TestObject', 20);
+      collection.idWasDeleted(
+          '0x1000', 'skia', 'TestObject', 25);
+
+      var testFrame = collection.getObjectInstanceAt('0x1000', 10);
+      assert.instanceOf(testFrame, tv.c.trace_model.ObjectInstance);
+      assert.notInstanceOf(testFrame, TestObjectInstance);
+
+      var testObject = collection.getObjectInstanceAt('0x1000', 20);
+      assert.instanceOf(testObject, tv.c.trace_model.ObjectInstance);
+      assert.instanceOf(testObject, TestObjectInstance);
+    } finally {
+      tv.c.trace_model.ObjectInstance.unregister(TestObjectInstance);
+    }
+  });
+
+  test('twoSnapshots', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    collection.idWasCreated(
+        '0x1000', 'cat', 'Frame', 10);
+    collection.addSnapshot(
+        '0x1000', 'cat', 'Frame', 10, {foo: 1});
+    collection.addSnapshot(
+        '0x1000', 'cat', 'Frame', 20, {foo: 2});
+
+    collection.updateBounds();
+    assert.equal(collection.bounds.min, 10);
+    assert.equal(collection.bounds.max, 20);
+
+    var s0 = collection.getSnapshotAt('0x1000', 1);
+    assert.isUndefined(s0);
+
+    var s1 = collection.getSnapshotAt('0x1000', 10);
+    assert.equal(s1.args.foo, 1);
+
+    var s2 = collection.getSnapshotAt('0x1000', 15);
+    assert.equal(s2.args.foo, 1);
+    assert.equal(s1, s2);
+
+    var s3 = collection.getSnapshotAt('0x1000', 20);
+    assert.equal(s3.args.foo, 2);
+    assert.equal(s1.object, s3.object);
+
+    var s4 = collection.getSnapshotAt('0x1000', 25);
+    assert.equal(s4, s3);
+  });
+
+  test('twoObjectsSharingOneID', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    collection.idWasCreated(
+        '0x1000', 'tv.e.cc', 'Frame', 10);
+    collection.idWasDeleted(
+        '0x1000', 'tv.e.cc', 'Frame', 15);
+    collection.idWasCreated(
+        '0x1000', 'skia', 'Picture', 20);
+    collection.idWasDeleted(
+        '0x1000', 'skia', 'Picture', 25);
+
+    var frame = collection.getObjectInstanceAt('0x1000', 10);
+    assert.equal(frame.category, 'tv.e.cc');
+    assert.equal(frame.name, 'Frame');
+
+    var picture = collection.getObjectInstanceAt('0x1000', 20);
+    assert.equal(picture.category, 'skia');
+    assert.equal(picture.name, 'Picture');
+
+    var typeNames = tv.b.dictionaryKeys(collection.getAllInstancesByTypeName());
+    typeNames.sort();
+    assert.deepEqual(
+        ['Frame', 'Picture'],
+        typeNames);
+    assert.deepEqual(
+        [frame],
+        collection.getAllInstancesByTypeName()['Frame']);
+    assert.deepEqual(
+        [picture],
+        collection.getAllInstancesByTypeName()['Picture']);
+  });
+
+  test('createSnapDelete', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    collection.idWasCreated(
+        '0x1000', 'cat', 'Frame', 10);
+    collection.addSnapshot(
+        '0x1000', 'cat', 'Frame', 10, {foo: 1});
+    collection.idWasDeleted(
+        '0x1000', 'cat', 'Frame', 15);
+
+    collection.updateBounds();
+    assert.equal(collection.bounds.min, 10);
+    assert.equal(collection.bounds.max, 15);
+
+    var s10 = collection.getSnapshotAt('0x1000', 10);
+    var i10 = s10.objectInstance;
+    assert.equal(i10.creationTs, 10);
+    assert.equal(i10.deletionTs, 15);
+  });
+
+  test('boundsOnUndeletedObject', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    collection.idWasCreated(
+        '0x1000', 'cat', 'Frame', 10);
+    collection.addSnapshot(
+        '0x1000', 'cat', 'Frame', 15, {foo: 1});
+
+    collection.updateBounds();
+    assert.equal(10, collection.bounds.min);
+    assert.equal(15, collection.bounds.max);
+  });
+
+  test('snapshotWithCustomBaseTypeThenDelete', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    var s10 = collection.addSnapshot(
+        '0x1000', 'cat', 'cc::PictureLayerImpl', 10, {}, 'cc::LayerImpl');
+    collection.idWasDeleted(
+        '0x1000', 'cat', 'cc::LayerImpl', 15);
+    collection.updateBounds();
+    assert.equal(10, collection.bounds.min);
+    assert.equal(15, collection.bounds.max);
+    assert.equal(s10.objectInstance.name, 'cc::PictureLayerImpl');
+    assert.equal(s10.objectInstance.baseTypeName, 'cc::LayerImpl');
+  });
+
+  test('newWithSnapshotThatChangesBaseType', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    var i10 = collection.idWasCreated(
+        '0x1000', 'cat', 'cc::LayerImpl', 10);
+    var s15 = collection.addSnapshot(
+        '0x1000', 'cat', 'cc::PictureLayerImpl', 15, {}, 'cc::LayerImpl');
+    collection.updateBounds();
+    assert.equal(10, collection.bounds.min);
+    assert.equal(15, collection.bounds.max);
+    assert.equal(s15.objectInstance, i10);
+    assert.equal(i10.name, 'cc::PictureLayerImpl');
+    assert.equal(i10.baseTypeName, 'cc::LayerImpl');
+  });
+
+  test('deleteThenSnapshotWithCustomBase', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    collection.idWasDeleted(
+        '0x1000', 'cat', 'cc::LayerImpl', 10);
+    var s15 = collection.addSnapshot(
+        '0x1000', 'cat', 'cc::PictureLayerImpl', 15, {}, 'cc::LayerImpl');
+    collection.updateBounds();
+    assert.equal(10, collection.bounds.min);
+    assert.equal(15, collection.bounds.max);
+    assert.equal(s15.objectInstance.name, 'cc::PictureLayerImpl');
+  });
+
+  test('autoDelete', function() {
+    var collection = new tv.c.trace_model.ObjectCollection({});
+    collection.idWasCreated(
+        '0x1000', 'cat', 'Frame', 10);
+    collection.addSnapshot(
+        '0x1000', 'cat', 'Frame', 10, {foo: 1});
+    collection.autoDeleteObjects(15);
+
+    var s10 = collection.getSnapshotAt('0x1000', 10);
+    var i10 = s10.objectInstance;
+    assert.equal(15, i10.deletionTs);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/object_instance.html b/trace-viewer/trace_viewer/core/trace_model/object_instance.html
new file mode 100644
index 0000000..9ef06b3
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/object_instance.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/trace_model/object_snapshot.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ObjectSnapshot and ObjectHistory classes.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * An object with a specific id, whose state has been snapshotted several
+   * times.
+   *
+   * @constructor
+   */
+  function ObjectInstance(
+      parent, id, category, name, creationTs, opt_baseTypeName) {
+    tv.c.trace_model.Event.call(this);
+    this.parent = parent;
+    this.id = id;
+    this.category = category;
+    this.baseTypeName = opt_baseTypeName ? opt_baseTypeName : name;
+    this.name = name;
+    this.creationTs = creationTs;
+    this.creationTsWasExplicit = false;
+    this.deletionTs = Number.MAX_VALUE;
+    this.deletionTsWasExplicit = false;
+    this.colorId = 0;
+    this.bounds = new tv.b.Range();
+    this.snapshots = [];
+    this.hasImplicitSnapshots = false;
+  }
+
+  ObjectInstance.prototype = {
+    __proto__: tv.c.trace_model.Event.prototype,
+
+    get typeName() {
+      return this.name;
+    },
+
+    addBoundsToRange: function(range) {
+      range.addRange(this.bounds);
+    },
+
+    addSnapshot: function(ts, args, opt_name, opt_baseTypeName) {
+      if (ts < this.creationTs)
+        throw new Error('Snapshots must be >= instance.creationTs');
+      if (ts >= this.deletionTs)
+        throw new Error('Snapshots cannot be added after ' +
+                        'an objects deletion timestamp.');
+
+      var lastSnapshot;
+      if (this.snapshots.length > 0) {
+        lastSnapshot = this.snapshots[this.snapshots.length - 1];
+        if (lastSnapshot.ts == ts)
+          throw new Error('Snapshots already exists at this time!');
+        if (ts < lastSnapshot.ts) {
+          throw new Error(
+              'Snapshots must be added in increasing timestamp order');
+        }
+      }
+
+      // Update baseTypeName if needed.
+      if (opt_name &&
+          (this.name != opt_name)) {
+        if (!opt_baseTypeName)
+          throw new Error('Must provide base type name for name update');
+        if (this.baseTypeName != opt_baseTypeName)
+          throw new Error('Cannot update type name: base types dont match');
+        this.name = opt_name;
+      }
+
+      var snapshotConstructor =
+          tv.c.trace_model.ObjectSnapshot.getConstructor(
+              this.category, this.name);
+      var snapshot = new snapshotConstructor(this, ts, args);
+      this.snapshots.push(snapshot);
+      return snapshot;
+    },
+
+    wasDeleted: function(ts) {
+      var lastSnapshot;
+      if (this.snapshots.length > 0) {
+        lastSnapshot = this.snapshots[this.snapshots.length - 1];
+        if (lastSnapshot.ts > ts)
+          throw new Error(
+              'Instance cannot be deleted at ts=' +
+              ts + '. A snapshot exists that is older.');
+      }
+      this.deletionTs = ts;
+      this.deletionTsWasExplicit = true;
+    },
+
+    /**
+     * See ObjectSnapshot constructor notes on object initialization.
+     */
+    preInitialize: function() {
+      for (var i = 0; i < this.snapshots.length; i++)
+        this.snapshots[i].preInitialize();
+    },
+
+    /**
+     * See ObjectSnapshot constructor notes on object initialization.
+     */
+    initialize: function() {
+      for (var i = 0; i < this.snapshots.length; i++)
+        this.snapshots[i].initialize();
+    },
+
+    getSnapshotAt: function(ts) {
+      if (ts < this.creationTs) {
+        if (this.creationTsWasExplicit)
+          throw new Error('ts must be within lifetime of this instance');
+        return this.snapshots[0];
+      }
+      if (ts > this.deletionTs)
+        throw new Error('ts must be within lifetime of this instance');
+
+      var snapshots = this.snapshots;
+      var i = tv.b.findLowIndexInSortedIntervals(
+          snapshots,
+          function(snapshot) { return snapshot.ts; },
+          function(snapshot, i) {
+            if (i == snapshots.length - 1)
+              return snapshots[i].objectInstance.deletionTs;
+            return snapshots[i + 1].ts - snapshots[i].ts;
+          },
+          ts);
+      if (i < 0) {
+        // Note, this is a little bit sketchy: this lets early ts point at the
+        // first snapshot, even before it is taken. We do this because raster
+        // tasks usually post before their tile snapshots are dumped. This may
+        // be a good line of code to re-visit if we start seeing strange and
+        // confusing object references showing up in the traces.
+        return this.snapshots[0];
+      }
+      if (i >= this.snapshots.length)
+        return this.snapshots[this.snapshots.length - 1];
+      return this.snapshots[i];
+    },
+
+    updateBounds: function() {
+      this.bounds.reset();
+      this.bounds.addValue(this.creationTs);
+      if (this.deletionTs != Number.MAX_VALUE)
+        this.bounds.addValue(this.deletionTs);
+      else if (this.snapshots.length > 0)
+        this.bounds.addValue(this.snapshots[this.snapshots.length - 1].ts);
+    },
+
+    shiftTimestampsForward: function(amount) {
+      this.creationTs += amount;
+      if (this.deletionTs != Number.MAX_VALUE)
+        this.deletionTs += amount;
+      this.snapshots.forEach(function(snapshot) {
+        snapshot.ts += amount;
+      });
+    },
+
+    get userFriendlyName() {
+      return this.typeName + ' object ' + this.id;
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+    ObjectInstance,
+    {
+      name: 'objectInstance',
+      pluralName: 'objectInstances',
+      singleViewElementName: 'tv-c-single-object-instance-sub-view',
+      multiViewElementName: 'tv-c-multi-object-sub-view'
+    });
+
+  var options = new tv.b.ExtensionRegistryOptions(
+      tv.b.TYPE_BASED_REGISTRY_MODE);
+  options.mandatoryBaseClass = ObjectInstance;
+  options.defaultConstructor = ObjectInstance;
+  tv.b.decorateExtensionRegistry(ObjectInstance, options);
+
+  return {
+    ObjectInstance: ObjectInstance
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/object_instance_test.html b/trace-viewer/trace_viewer/core/trace_model/object_instance_test.html
new file mode 100644
index 0000000..5bb622e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/object_instance_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('getSnapshotAtWithImplicitCreation', function() {
+    var instance = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'n', 10);
+    var s10 = instance.addSnapshot(10, 'a');
+    instance.addSnapshot(40, 'b');
+    instance.wasDeleted(60);
+
+    var s1 = instance.getSnapshotAt(1);
+    assert.equal(s1, s10);
+
+    var s10 = instance.getSnapshotAt(10);
+    assert.equal(s10.args, 'a');
+    assert.equal(instance.getSnapshotAt(15), s10);
+
+    var s40 = instance.getSnapshotAt(40);
+    assert.equal(s40.args, 'b');
+    assert.equal(instance.getSnapshotAt(50), s40);
+    assert.equal(instance.getSnapshotAt(59.9), s40);
+  });
+
+  test('getSnapshotAtWithExplicitCreation', function() {
+    var instance = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'n', 10);
+    instance.creationTsWasExplicit = true;
+    instance.addSnapshot(10, 'a');
+    instance.wasDeleted(60);
+
+    assert.throws(function() {
+      instance.getSnapshotAt(1);
+    });
+
+    var s10 = instance.getSnapshotAt(10);
+    assert.equal(s10.args, 'a');
+    assert.equal(instance.getSnapshotAt(15), s10);
+  });
+
+  test('getSnapshotBeforeFirstSnapshot', function() {
+    var instance = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'n', 10);
+    var s15 = instance.addSnapshot(15, 'a');
+    instance.wasDeleted(40);
+
+    assert.equal(instance.getSnapshotAt(10), s15);
+  });
+
+  test('getSnapshotAfterLastSnapshot', function() {
+    var instance = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'n', 10);
+    var s15 = instance.addSnapshot(15, 'a');
+    instance.wasDeleted(40);
+
+    assert.equal(instance.getSnapshotAt(20), s15);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/object_snapshot.html b/trace-viewer/trace_viewer/core/trace_model/object_snapshot.html
new file mode 100644
index 0000000..8ed7433
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/object_snapshot.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/core/analysis/util.html">
+<link rel="import" href="/core/trace_model/event.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A snapshot of an object instance, at a given moment in time.
+   *
+   * Initialization of snapshots and instances is three phased:
+   *
+   * 1. Instances and snapshots are constructed. This happens during event
+   *    importing. Little should be done here, because the object's data
+   *    are still being used by the importer to reconstruct object references.
+   *
+   * 2. Instances and snapshtos are preinitialized. This happens after implicit
+   *    objects have been found, but before any references have been found and
+   *    switched to direct references. Thus, every snapshot stands on its own.
+   *    This is a good time to do global field renaming and type conversion,
+   *    e.g. recognizing domain-specific types and converting from C++ naming
+   *    convention to JS.
+   *
+   * 3. Instances and snapshtos are initialized. At this point, {id_ref:
+   *    '0x1000'} fields have been converted to snapshot references. This is a
+   *    good time to generic initialization steps and argument verification.
+   *
+   * @constructor
+   */
+  function ObjectSnapshot(objectInstance, ts, args) {
+    tv.c.trace_model.Event.call(this);
+    this.objectInstance = objectInstance;
+    this.ts = ts;
+    this.args = args;
+  }
+
+  ObjectSnapshot.prototype = {
+    __proto__: tv.c.trace_model.Event.prototype,
+
+    /**
+     * See ObjectSnapshot constructor notes on object initialization.
+     */
+    preInitialize: function() {
+    },
+
+    /**
+     * See ObjectSnapshot constructor notes on object initialization.
+     */
+    initialize: function() {
+    },
+
+    addBoundsToRange: function(range) {
+      range.addValue(this.ts);
+    },
+
+    get userFriendlyName() {
+      return 'Snapshot of ' +
+             this.objectInstance.typeName + ' ' +
+             this.objectInstance.id + ' @ ' +
+             tv.c.analysis.tsString(this.ts);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      ObjectSnapshot,
+      {
+        name: 'objectSnapshot',
+        pluralName: 'objectSnapshots',
+        singleViewElementName: 'tv-c-single-object-snapshot-sub-view',
+        multiViewElementName: 'tv-c-multi-object-sub-view'
+      });
+
+  var options = new tv.b.ExtensionRegistryOptions(
+      tv.b.TYPE_BASED_REGISTRY_MODE);
+  options.mandatoryBaseClass = ObjectSnapshot;
+  options.defaultConstructor = ObjectSnapshot;
+  tv.b.decorateExtensionRegistry(ObjectSnapshot, options);
+
+  return {
+    ObjectSnapshot: ObjectSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/object_snapshot_test.html b/trace-viewer/trace_viewer/core/trace_model/object_snapshot_test.html
new file mode 100644
index 0000000..ef4c063
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/object_snapshot_test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/object_instance.html">
+<link rel="import" href="/core/trace_model/object_snapshot.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('snapshotTypeRegistry', function() {
+    function MySnapshot() {
+      tv.c.trace_model.ObjectSnapshot.apply(this, arguments);
+      this.myFoo = this.args.foo;
+    }
+
+    MySnapshot.prototype = {
+      __proto__: tv.c.trace_model.ObjectSnapshot.prototype
+    };
+
+    var instance = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'MySnapshot', 10);
+    try {
+      tv.c.trace_model.ObjectSnapshot.register(
+          MySnapshot,
+          {typeName: 'MySnapshot'});
+      var snapshot = instance.addSnapshot(15, {foo: 'bar'});
+      assert.instanceOf(snapshot, MySnapshot);
+      assert.equal(snapshot.myFoo, 'bar');
+    } finally {
+      tv.c.trace_model.ObjectSnapshot.unregister(MySnapshot);
+    }
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/process.html b/trace-viewer/trace_viewer/core/trace_model/process.html
new file mode 100644
index 0000000..db34c70
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/process.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/process_base.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Process class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var ProcessBase = tv.c.trace_model.ProcessBase;
+
+  /**
+   * The Process represents a single userland process in the
+   * trace.
+   * @constructor
+   */
+  function Process(model, pid) {
+    if (model === undefined)
+      throw new Error('model must be provided');
+    if (pid === undefined)
+      throw new Error('pid must be provided');
+    tv.c.trace_model.ProcessBase.call(this, model);
+    this.pid = pid;
+    this.name = undefined;
+    this.labels = [];
+    this.instantEvents = [];
+    this.memoryDumps = [];
+    this.frames = [];
+  };
+
+  /**
+   * Comparison between processes that orders by pid.
+   */
+  Process.compare = function(x, y) {
+    var tmp = tv.c.trace_model.ProcessBase.compare(x, y);
+    if (tmp)
+      return tmp;
+
+    tmp = tv.b.comparePossiblyUndefinedValues(
+        x.name, y.name,
+        function(x, y) { return x.localeCompare(y); });
+    if (tmp)
+      return tmp;
+
+    tmp = tv.b.compareArrays(x.labels, y.labels,
+        function(x, y) { return x.localeCompare(y); });
+    if (tmp)
+      return tmp;
+
+    return x.pid - y.pid;
+  };
+
+  Process.prototype = {
+    __proto__: tv.c.trace_model.ProcessBase.prototype,
+
+    get stableId() {
+      return this.pid;
+    },
+
+    compareTo: function(that) {
+      return Process.compare(this, that);
+    },
+
+    pushInstantEvent: function(instantEvent) {
+      this.instantEvents.push(instantEvent);
+    },
+
+    addLabelIfNeeded: function(labelName) {
+      for (var i = 0; i < this.labels.length; i++) {
+        if (this.labels[i] === labelName)
+          return;
+      }
+      this.labels.push(labelName);
+    },
+
+    get userFriendlyName() {
+      var res;
+      if (this.name)
+        res = this.name + ' (pid ' + this.pid + ')';
+      else
+        res = 'Process ' + this.pid;
+      if (this.labels.length)
+        res += ': ' + this.labels.join(', ');
+      return res;
+    },
+
+    get userFriendlyDetails() {
+      if (this.name)
+        return this.name + ' (pid ' + this.pid + ')';
+      return 'pid: ' + this.pid;
+    },
+
+    getSettingsKey: function() {
+      if (!this.name)
+        return undefined;
+      if (!this.labels.length)
+        return 'processes.' + this.name;
+      return 'processes.' + this.name + '.' + this.labels.join('.');
+    },
+
+    shiftTimestampsForward: function(amount) {
+      for (var id in this.instantEvents)
+        this.instantEvents[id].start += amount;
+
+      for (var i = 0; i < this.frames.length; i++)
+        this.frames[i].shiftTimestampsForward(amount);
+
+      for (var i = 0; i < this.memoryDumps.length; i++)
+        this.memoryDumps[i].shiftTimestampsForward(amount);
+
+      tv.c.trace_model.ProcessBase.prototype
+          .shiftTimestampsForward.apply(this, arguments);
+    },
+
+    updateBounds: function() {
+      tv.c.trace_model.ProcessBase.prototype.updateBounds.apply(this);
+
+      for (var i = 0; i < this.frames.length; i++)
+        this.frames[i].addBoundsToRange(this.bounds);
+
+      for (var i = 0; i < this.memoryDumps.length; i++)
+        this.memoryDumps[i].addBoundsToRange(this.bounds);
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      this.instantEvents.forEach(callback, opt_this);
+
+      this.frames.forEach(callback, opt_this);
+
+      this.memoryDumps.forEach(callback, opt_this);
+
+      ProcessBase.prototype.iterateAllEvents.call(this, callback, opt_this);
+    },
+
+    iterateAllEventContainers: function(callback) {
+      callback(this);
+      for (var tid in this.threads)
+        this.threads[tid].iterateAllEventContainers(callback);
+    },
+
+    sortMemoryDumps: function() {
+      this.memoryDumps.sort(function(x, y) {
+        return x.start - y.start;
+      });
+    }
+  };
+
+  return {
+    Process: Process
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/process_base.html b/trace-viewer/trace_viewer/core/trace_model/process_base.html
new file mode 100644
index 0000000..84a031f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/process_base.html
@@ -0,0 +1,233 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/counter.html">
+<link rel="import" href="/core/trace_model/object_collection.html">
+<link rel="import" href="/core/trace_model/thread.html">
+<link rel="import" href="/core/trace_model/trace_model_settings.html">
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/range.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ProcessBase class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+
+  var Thread = tv.c.trace_model.Thread;
+  var Counter = tv.c.trace_model.Counter;
+
+  /**
+   * The ProcessBase is a partial base class, upon which Kernel
+   * and Process are built.
+   *
+   * @constructor
+   * @extends {tv.c.trace_model.EventContainer}
+   */
+  function ProcessBase(model) {
+    if (!model)
+      throw new Error('Must provide a model');
+    this.guid_ = tv.b.GUID.allocate();
+    this.model = model;
+    this.threads = {};
+    this.counters = {};
+    this.objects = new tv.c.trace_model.ObjectCollection(this);
+    this.bounds = new tv.b.Range();
+    this.sortIndex = 0;
+  };
+
+  ProcessBase.compare = function(x, y) {
+    return x.sortIndex - y.sortIndex;
+  };
+
+  ProcessBase.prototype = {
+    __proto__: tv.c.trace_model.EventContainer.prototype,
+
+    /*
+     * @return {Number} A globally unique identifier for this counter.
+     */
+    get guid() {
+      return this.guid_;
+    },
+
+    get stableId() {
+      throw new Error('Not implemented');
+    },
+
+    /**
+     * Gets the number of threads in this process.
+     */
+    get numThreads() {
+      var n = 0;
+      for (var p in this.threads) {
+        n++;
+      }
+      return n;
+    },
+
+    /**
+     * Shifts all the timestamps inside this process forward by the amount
+     * specified.
+     */
+    shiftTimestampsForward: function(amount) {
+      for (var tid in this.threads)
+        this.threads[tid].shiftTimestampsForward(amount);
+      for (var id in this.counters)
+        this.counters[id].shiftTimestampsForward(amount);
+      this.objects.shiftTimestampsForward(amount);
+    },
+
+    /**
+     * Closes any open slices.
+     */
+    autoCloseOpenSlices: function(opt_maxTimestamp) {
+      for (var tid in this.threads) {
+        var thread = this.threads[tid];
+        thread.autoCloseOpenSlices(opt_maxTimestamp);
+      }
+    },
+
+    autoDeleteObjects: function(maxTimestamp) {
+      this.objects.autoDeleteObjects(maxTimestamp);
+    },
+
+    /**
+     * Called by the model after finalizing imports,
+     * but before joining refs.
+     */
+    preInitializeObjects: function() {
+      this.objects.preInitializeAllObjects();
+    },
+
+    /**
+     * Called by the model after joining refs.
+     */
+    initializeObjects: function() {
+      this.objects.initializeAllObjects();
+    },
+
+    /**
+     * Merge slices from the kernel with those from userland for each thread.
+     */
+    mergeKernelWithUserland: function() {
+      for (var tid in this.threads) {
+        var thread = this.threads[tid];
+        thread.mergeKernelWithUserland();
+      }
+    },
+
+    updateBounds: function() {
+      this.bounds.reset();
+      for (var tid in this.threads) {
+        this.threads[tid].updateBounds();
+        this.bounds.addRange(this.threads[tid].bounds);
+      }
+      for (var id in this.counters) {
+        this.counters[id].updateBounds();
+        this.bounds.addRange(this.counters[id].bounds);
+      }
+      this.objects.updateBounds();
+      this.bounds.addRange(this.objects.bounds);
+    },
+
+    addCategoriesToDict: function(categoriesDict) {
+      for (var tid in this.threads)
+        this.threads[tid].addCategoriesToDict(categoriesDict);
+      for (var id in this.counters)
+        categoriesDict[this.counters[id].category] = true;
+      this.objects.addCategoriesToDict(categoriesDict);
+    },
+
+    /**
+     * @param {String} The name of the thread to find.
+     * @return {Array} An array of all the matched threads.
+     */
+    findAllThreadsNamed: function(name) {
+      var namedThreads = [];
+      for (var tid in this.threads) {
+        var thread = this.threads[tid];
+        if (thread.name == name)
+          namedThreads.push(thread);
+      }
+      return namedThreads;
+    },
+
+    /**
+     * Removes threads from the process that are fully empty.
+     */
+    pruneEmptyContainers: function() {
+      var threadsToKeep = {};
+      for (var tid in this.threads) {
+        var thread = this.threads[tid];
+        if (!thread.isEmpty)
+          threadsToKeep[tid] = thread;
+      }
+      this.threads = threadsToKeep;
+    },
+
+    /**
+     * @return {TimelineThread} The thread identified by tid on this process,
+     * or undefined if it doesn't exist.
+     */
+    getThread: function(tid) {
+      return this.threads[tid];
+    },
+
+    /**
+     * @return {TimelineThread} The thread identified by tid on this process,
+     * creating it if it doesn't exist.
+     */
+    getOrCreateThread: function(tid) {
+      if (!this.threads[tid])
+        this.threads[tid] = new Thread(this, tid);
+      return this.threads[tid];
+    },
+
+    /**
+     * @return {TimelineCounter} The counter on this process named 'name',
+     * creating it if it doesn't exist.
+     */
+    getOrCreateCounter: function(cat, name) {
+      var id = cat + '.' + name;
+      if (!this.counters[id])
+        this.counters[id] = new Counter(this, id, cat, name);
+      return this.counters[id];
+    },
+
+    getSettingsKey: function() {
+      throw new Error('Not implemented');
+    },
+
+    createSubSlices: function() {
+      for (var tid in this.threads)
+        this.threads[tid].createSubSlices();
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      for (var tid in this.threads)
+        this.threads[tid].iterateAllEvents(callback, opt_this);
+
+      for (var id in this.counters)
+        this.counters[id].iterateAllEvents(callback, opt_this);
+
+      this.objects.iterateAllEvents(callback, opt_this);
+    },
+
+    iterateAllPersistableObjects: function(cb) {
+      cb(this);
+      for (var tid in this.threads)
+        this.threads[tid].iterateAllPersistableObjects(cb);
+    }
+  };
+
+  return {
+    ProcessBase: ProcessBase
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/process_memory_dump.html b/trace-viewer/trace_viewer/core/trace_model/process_memory_dump.html
new file mode 100644
index 0000000..76c3534
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/process_memory_dump.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the ProcessMemoryDump class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * The ProcessMemoryDump represents a memory dump of a single process.
+   * @constructor
+   */
+  function ProcessMemoryDump(globalMemoryDump, process, start) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+    this.process = process;
+    this.globalMemoryDump = globalMemoryDump;
+
+    this.totalResidentBytes = undefined;
+    this.vmRegions = undefined;
+    this.memoryAllocatorDumps = [];
+    this.memoryAllocatorDumpsByFullName = {};
+  };
+
+  ProcessMemoryDump.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+    shiftTimestampsForward: function(amount) {
+      this.start += amount;
+    },
+
+    get userFriendlyName() {
+      return 'Process memory dump ' + ' at ' +
+          tv.c.analysis.tsString(this.start);
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function MemoryAllocatorDump(fullName, physicalSizeInBytes,
+      allocatedObjectsCount, allocatedObjectsSizeInBytes, opt_parent) {
+    this.fullName = fullName;
+    this.parent = opt_parent;
+    this.children = [];
+
+    this.physicalSizeInBytes = physicalSizeInBytes;
+    this.allocatedObjectsCount = allocatedObjectsCount;
+    this.allocatedObjectsSizeInBytes = allocatedObjectsSizeInBytes;
+
+    // TODO(primiano): next CLs add extra_attributes.
+  };
+
+  MemoryAllocatorDump.prototype = {
+    get name() {
+      return this.fullName.substring(this.fullName.lastIndexOf('/') + 1);
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function VMRegion(startAddress, sizeInBytes, protectionFlags,
+      mappedFile, byteStats) {
+    this.startAddress = startAddress;
+    this.sizeInBytes = sizeInBytes;
+    this.protectionFlags = protectionFlags;
+    this.mappedFile = mappedFile;
+    this.byteStats = byteStats;
+  };
+
+  VMRegion.PROTECTION_FLAG_READ = 4;
+  VMRegion.PROTECTION_FLAG_WRITE = 2;
+  VMRegion.PROTECTION_FLAG_EXECUTE = 1;
+
+  VMRegion.prototype = {
+    get protectionFlagsToString() {
+      return (
+          (this.protectionFlags & VMRegion.PROTECTION_FLAG_READ ? 'r' : '-') +
+          (this.protectionFlags & VMRegion.PROTECTION_FLAG_WRITE ? 'w' : '-') +
+          (this.protectionFlags & VMRegion.PROTECTION_FLAG_EXECUTE ? 'x' : '-')
+      );
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function VMRegionByteStats(privateResident, sharedResident,
+      proportionalResident) {
+    this.privateResident = privateResident;
+    this.sharedResident = sharedResident;
+    this.proportionalResident = proportionalResident;
+  };
+
+  VMRegionByteStats.prototype = {
+    get totalResident() {
+      return this.privateResident + this.sharedResident;
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      ProcessMemoryDump,
+      {
+        name: 'processMemoryDump',
+        pluralName: 'processMemoryDumps',
+        singleViewElementName: 'tv-c-single-process-memory-dump-sub-view',
+        multiViewElementName: 'tv-c-multi-process-memory-dump-sub-view'
+      });
+
+  return {
+    ProcessMemoryDump: ProcessMemoryDump,
+    MemoryAllocatorDump: MemoryAllocatorDump,
+    VMRegion: VMRegion,
+    VMRegionByteStats: VMRegionByteStats
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/process_test.html b/trace-viewer/trace_viewer/core/trace_model/process_test.html
new file mode 100644
index 0000000..f3d8e88
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/process_test.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/trace_model/process.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('getOrCreateCounter', function() {
+    var model = new tv.c.TraceModel();
+    var process = new tv.c.trace_model.Process(model, 7);
+    var ctrBar = process.getOrCreateCounter('foo', 'bar');
+    var ctrBar2 = process.getOrCreateCounter('foo', 'bar');
+    assert.equal(ctrBar2, ctrBar);
+  });
+
+  test('shiftTimestampsForward', function() {
+    var model = new tv.c.TraceModel();
+    var process = new tv.c.trace_model.Process(model, 7);
+    var ctr = process.getOrCreateCounter('foo', 'bar');
+    var thread = process.getOrCreateThread(1);
+
+    var shiftCount = 0;
+    thread.shiftTimestampsForward = function(ts) {
+      if (ts == 0.32)
+        shiftCount++;
+    };
+    ctr.shiftTimestampsForward = function(ts) {
+      if (ts == 0.32)
+        shiftCount++;
+    };
+    process.shiftTimestampsForward(0.32);
+    assert.equal(shiftCount, 2);
+  });
+
+  test('compareOnPID', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new tv.c.trace_model.Process(model, 1);
+    p1.name = 'Renderer';
+
+    var model = new tv.c.TraceModel();
+    var p2 = new tv.c.trace_model.Process(model, 2);
+    p2.name = 'Renderer';
+
+    assert.isBelow(p1.compareTo(p2), 0);
+  });
+
+  test('compareOnSortIndex', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new tv.c.trace_model.Process(model, 1);
+    p1.name = 'Renderer';
+    p1.sortIndex = 1;
+
+    var p2 = new tv.c.trace_model.Process(model, 2);
+    p2.name = 'Renderer';
+
+    assert.isAbove(p1.compareTo(p2), 0);
+  });
+
+  test('compareOnName', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new tv.c.trace_model.Process(model, 1);
+    p1.name = 'Browser';
+
+    var p2 = new tv.c.trace_model.Process(model, 2);
+    p2.name = 'Renderer';
+
+    assert.isBelow(p1.compareTo(p2), 0);
+  });
+
+  test('compareOnLabels', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new tv.c.trace_model.Process(model, 1);
+    p1.name = 'Renderer';
+    p1.labels = ['a'];
+
+    var p2 = new tv.c.trace_model.Process(model, 2);
+    p2.name = 'Renderer';
+    p2.labels = ['b'];
+
+    assert.isBelow(p1.compareTo(p2), 0);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/rect_annotation.html b/trace-viewer/trace_viewer/core/trace_model/rect_annotation.html
new file mode 100644
index 0000000..67b8d1e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/rect_annotation.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/annotation.html">
+<link rel="import" href="/core/tracks/rect_annotation_view.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+
+  function RectAnnotation(start, end) {
+    tv.c.trace_model.Annotation.apply(this, arguments);
+
+    this.startLocation_ = start; // Location of top-left corner.
+    this.endLocation_ = end; // Location of bottom-right corner.
+    this.fillStyle = 'rgba(255, 180, 0, 0.3)';
+  }
+
+  RectAnnotation.fromDict = function(dict) {
+    var args = dict.args;
+    var startLoc =
+        new tv.c.Location(args.start.xWorld, args.start.yComponents);
+    var endLoc =
+        new tv.c.Location(args.end.xWorld, args.end.yComponents);
+    return new tv.c.trace_model.RectAnnotation(startLoc, endLoc);
+  }
+
+  RectAnnotation.prototype = {
+    __proto__: tv.c.trace_model.Annotation.prototype,
+
+    get startLocation() {
+      return this.startLocation_;
+    },
+
+    get endLocation() {
+      return this.endLocation_;
+    },
+
+    toDict: function() {
+      return {
+        typeName: 'rect',
+        args: {
+          start: this.startLocation.toDict(),
+          end: this.endLocation.toDict()
+        }
+      };
+    },
+
+    createView_: function(viewport) {
+      return new tv.c.annotations.RectAnnotationView(viewport, this);
+    }
+  };
+
+  tv.c.trace_model.Annotation.register(RectAnnotation, {typeName: 'rect'});
+
+  return {
+    RectAnnotation: RectAnnotation
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/sample.html b/trace-viewer/trace_viewer/core/trace_model/sample.html
new file mode 100644
index 0000000..c4b6571
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/sample.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/timed_event.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Sample class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A Sample represents a sample taken at an instant in time, plus its stack
+   * frame and parameters associated with that sample.
+   *
+   * @constructor
+   */
+  function Sample(cpu, thread, title, start, leafStackFrame,
+                  opt_weight, opt_args) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+
+    this.title = title;
+    this.cpu = cpu;
+    this.thread = thread;
+    this.leafStackFrame = leafStackFrame;
+    this.weight = opt_weight;
+    this.args = opt_args || {};
+  }
+
+  Sample.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+    get colorId() {
+      return this.leafStackFrame.colorId;
+    },
+
+    get stackTrace() {
+      return this.leafStackFrame.stackTrace;
+    },
+
+    getUserFriendlyStackTrace: function() {
+      return this.leafStackFrame.getUserFriendlyStackTrace();
+    },
+
+    get userFriendlyName() {
+      return 'Sample ' + ' at ' +
+          tv.c.analysis.tsString(this.start);
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      Sample,
+      {
+        name: 'sample',
+        pluralName: 'samples',
+        singleViewElementName: 'tv-c-single-sample-sub-view',
+        multiViewElementName: 'tv-c-multi-sample-sub-view'
+      });
+
+  return {
+    Sample: Sample
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/sample_test.html b/trace-viewer/trace_viewer/core/trace_model/sample_test.html
new file mode 100644
index 0000000..5372612
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/sample_test.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Sample = tv.c.trace_model.Sample;
+  var StackFrame = tv.c.trace_model.StackFrame;
+  var Thread = tv.c.trace_model.Thread;
+
+  test('sampleStackTrace', function() {
+    var thread = new Thread({}, 1);
+
+    var model = new tv.c.TraceModel();
+    var fABC = tv.c.test_utils.newStackTrace(model, 'cat', ['a', 'b', 'c']);
+
+    var s = new Sample(undefined, thread, 'instructions_retired',
+                       10, fABC, 10);
+    var stackTrace = s.stackTrace;
+    var stackTraceNames = stackTrace.map(function(f) { return f.title; });
+    assert.deepEqual(
+        stackTraceNames,
+        ['a', 'b', 'c']);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/slice.html b/trace-viewer/trace_viewer/core/trace_model/slice.html
new file mode 100644
index 0000000..ef0b6b2
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/slice.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/timed_event.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Slice class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A Slice represents an interval of time plus parameters associated
+   * with that interval.
+   *
+   * @constructor
+   */
+  function Slice(category, title, colorId, start, args, opt_duration,
+                 opt_cpuStart, opt_cpuDuration) {
+    tv.c.trace_model.TimedEvent.call(this, start);
+
+    this.category = category || '';
+    this.title = title;
+    this.colorId = colorId;
+    this.args = args;
+    this.startStackFrame = undefined;
+    this.endStackFrame = undefined;
+    this.didNotFinish = false;
+    this.inFlowEvents = [];
+    this.outFlowEvents = [];
+    this.subSlices = [];
+    this.selfTime = undefined;
+    this.cpuSelfTime = undefined;
+    this.important = false;
+
+    if (opt_duration !== undefined)
+      this.duration = opt_duration;
+
+    if (opt_cpuStart !== undefined)
+      this.cpuStart = opt_cpuStart;
+
+    if (opt_cpuDuration !== undefined)
+      this.cpuDuration = opt_cpuDuration;
+  }
+
+  Slice.prototype = {
+    __proto__: tv.c.trace_model.TimedEvent.prototype,
+
+
+    get analysisTypeName() {
+      return this.title;
+    },
+
+    get userFriendlyName() {
+      return 'Slice ' + this.title + ' at ' +
+          tv.c.analysis.tsString(this.start);
+    },
+
+    findDescendentSlice: function(targetTitle) {
+      if (!this.subSlices)
+        return undefined;
+
+      for (var i = 0; i < this.subSlices.length; i++) {
+        if (this.subSlices[i].title == targetTitle)
+          return this.subSlices[i];
+        var slice = this.subSlices[i].findDescendentSlice(targetTitle);
+        if (slice) return slice;
+      }
+      return undefined;
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      Slice,
+      {
+        name: 'slice',
+        pluralName: 'slices',
+        singleViewElementName: 'tv-c-single-slice-sub-view',
+        multiViewElementName: 'tv-c-multi-slice-sub-view'
+      });
+
+  return {
+    Slice: Slice
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/slice_group.html b/trace-viewer/trace_viewer/core/trace_model/slice_group.html
new file mode 100644
index 0000000..3c73a44
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/slice_group.html
@@ -0,0 +1,502 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event_container.html">
+<link rel="import" href="/core/trace_model/slice.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/base/ui/color_scheme.html">
+<link rel="import" href="/base/guid.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the SliceGroup class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var Slice = tv.c.trace_model.Slice;
+
+  /**
+   * A group of Slices, plus code to create them from B/E events, as
+   * well as arrange them into subRows.
+   *
+   * Do not mutate the slices array directly. Modify it only by
+   * SliceGroup mutation methods.
+   *
+   * @constructor
+   * @param {function(new:Slice, category, title, colorId, start, args)=}
+   *     opt_sliceConstructor The constructor to use when creating slices.
+   * @extends {tv.c.trace_model.EventContainer}
+   */
+  function SliceGroup(parentThread, opt_sliceConstructor, opt_name) {
+    this.guid_ = tv.b.GUID.allocate();
+
+    this.parentThread_ = parentThread;
+
+    var sliceConstructor = opt_sliceConstructor || Slice;
+    this.sliceConstructor = sliceConstructor;
+
+    this.openPartialSlices_ = [];
+
+    this.slices = [];
+    this.bounds = new tv.b.Range();
+    this.topLevelSlices = [];
+    this.haveTopLevelSlicesBeenBuilt = false;
+    this.name_ = opt_name;
+  }
+
+  SliceGroup.prototype = {
+    __proto__: tv.c.trace_model.EventContainer.prototype,
+
+    get guid() {
+      return this.guid_;
+    },
+
+    get parentThread() {
+      return this.parentThread_;
+    },
+
+    get model() {
+      return this.parentThread_.parent.model;
+    },
+
+    get stableId() {
+      return this.parentThread_.stableId + '.SliceGroup';
+    },
+
+    getSettingsKey: function() {
+      if (!this.name_)
+        return undefined;
+      var parentKey = this.parentThread_.getSettingsKey();
+      if (!parentKey)
+        return undefined;
+      return parentKey + '.' + this.name;
+    },
+
+    /**
+     * @return {Number} The number of slices in this group.
+     */
+    get length() {
+      return this.slices.length;
+    },
+
+    /**
+     * Helper function that pushes the provided slice onto the slices array.
+     * @param {Slice} slice The slice to be added to the slices array.
+     */
+    pushSlice: function(slice) {
+      this.haveTopLevelSlicesBeenBuilt = false;
+      this.slices.push(slice);
+      return slice;
+    },
+
+    /**
+     * Helper function that pushes the provided slices onto the slices array.
+     * @param {Array.<Slice>} slices An array of slices to be added.
+     */
+    pushSlices: function(slices) {
+      this.haveTopLevelSlicesBeenBuilt = false;
+      this.slices.push.apply(this.slices, slices);
+    },
+
+    /**
+     * Opens a new slice in the group's slices.
+     *
+     * Calls to beginSlice and
+     * endSlice must be made with non-monotonically-decreasing timestamps.
+     *
+     * @param {String} category Category name of the slice to add.
+     * @param {String} title Title of the slice to add.
+     * @param {Number} ts The timetsamp of the slice, in milliseconds.
+     * @param {Object.<string, Object>=} opt_args Arguments associated with
+     * the slice.
+     */
+    beginSlice: function(category, title, ts, opt_args, opt_tts) {
+      if (this.openPartialSlices_.length) {
+        var prevSlice = this.openPartialSlices_[
+            this.openPartialSlices_.length - 1];
+        if (ts < prevSlice.start)
+          throw new Error('Slices must be added in increasing timestamp order');
+      }
+
+      var colorId = tv.b.ui.getColorIdForGeneralPurposeString(title);
+      var slice = new this.sliceConstructor(category, title, colorId, ts,
+                                            opt_args ? opt_args : {}, null,
+                                            opt_tts);
+      this.openPartialSlices_.push(slice);
+      slice.didNotFinish = true;
+      this.pushSlice(slice);
+
+      return slice;
+    },
+
+    isTimestampValidForBeginOrEnd: function(ts) {
+      if (!this.openPartialSlices_.length)
+        return true;
+      var top = this.openPartialSlices_[this.openPartialSlices_.length - 1];
+      return ts >= top.start;
+    },
+
+    /**
+     * @return {Number} The number of beginSlices for which an endSlice has not
+     * been issued.
+     */
+    get openSliceCount() {
+      return this.openPartialSlices_.length;
+    },
+
+    get mostRecentlyOpenedPartialSlice() {
+      if (!this.openPartialSlices_.length)
+        return undefined;
+      return this.openPartialSlices_[this.openPartialSlices_.length - 1];
+    },
+
+    /**
+     * Ends the last begun slice in this group and pushes it onto the slice
+     * array.
+     *
+     * @param {Number} ts Timestamp when the slice ended.
+     * @return {Slice} slice.
+     */
+    endSlice: function(ts, opt_tts) {
+      if (!this.openSliceCount)
+        throw new Error('endSlice called without an open slice');
+
+      var slice = this.openPartialSlices_[this.openSliceCount - 1];
+      this.openPartialSlices_.splice(this.openSliceCount - 1, 1);
+      if (ts < slice.start)
+        throw new Error('Slice ' + slice.title +
+                        ' end time is before its start.');
+
+      slice.duration = ts - slice.start;
+      slice.didNotFinish = false;
+
+      if (opt_tts && slice.cpuStart !== undefined)
+        slice.cpuDuration = opt_tts - slice.cpuStart;
+
+      return slice;
+    },
+
+    /**
+     * Push a complete event as a Slice into the slice list.
+     * The timestamp can be in any order.
+     *
+     * @param {String} category Category name of the slice to add.
+     * @param {String} title Title of the slice to add.
+     * @param {Number} ts The timetsamp of the slice, in milliseconds.
+     * @param {Number} duration The duration of the slice, in milliseconds.
+     * @param {Object.<string, Object>=} opt_args Arguments associated with
+     * the slice.
+     */
+    pushCompleteSlice: function(category, title, ts, duration, tts,
+                                cpuDuration, opt_args) {
+      var colorId = tv.b.ui.getColorIdForGeneralPurposeString(title);
+      var slice = new this.sliceConstructor(category, title, colorId, ts,
+                                            opt_args ? opt_args : {},
+                                            duration, tts, cpuDuration);
+      if (duration === undefined)
+        slice.didNotFinish = true;
+      this.pushSlice(slice);
+      return slice;
+    },
+
+    /**
+     * Closes any open slices.
+     * @param {Number=} opt_maxTimestamp The end time to use for the closed
+     * slices. If not provided,
+     * the max timestamp for this slice is provided.
+     */
+    autoCloseOpenSlices: function(opt_maxTimestamp) {
+      if (!opt_maxTimestamp) {
+        this.updateBounds();
+        opt_maxTimestamp = this.bounds.max;
+      }
+      for (var sI = 0; sI < this.slices.length; sI++) {
+        var slice = this.slices[sI];
+        if (slice.didNotFinish)
+          slice.duration = opt_maxTimestamp - slice.start;
+      }
+      this.openPartialSlices_ = [];
+    },
+
+    /**
+     * Shifts all the timestamps inside this group forward by the amount
+     * specified.
+     */
+    shiftTimestampsForward: function(amount) {
+      for (var sI = 0; sI < this.slices.length; sI++) {
+        var slice = this.slices[sI];
+        slice.start = (slice.start + amount);
+      }
+    },
+
+    /**
+     * Updates the bounds for this group based on the slices it contains.
+     */
+    updateBounds: function() {
+      this.bounds.reset();
+      for (var i = 0; i < this.slices.length; i++) {
+        this.bounds.addValue(this.slices[i].start);
+        this.bounds.addValue(this.slices[i].end);
+      }
+    },
+
+    copySlice: function(slice) {
+      var newSlice = new this.sliceConstructor(slice.category, slice.title,
+          slice.colorId, slice.start,
+          slice.args, slice.duration, slice.cpuStart, slice.cpuDuration);
+      newSlice.didNotFinish = slice.didNotFinish;
+      return newSlice;
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      this.slices.forEach(callback, opt_this);
+    },
+
+    iterateAllEventContainers: function(callback) {
+      callback(this);
+    },
+
+    getSlicesOfName: function(title) {
+      var slices = [];
+      for (var i = 0; i < this.slices.length; i++) {
+        if (this.slices[i].title == title) {
+          slices.push(this.slices[i]);
+        }
+      }
+      return slices;
+    },
+
+    /**
+     * Construct subSlices for this group.
+     * Populate the group topLevelSlices, parent slices get a subSlices[],
+     * a selfThreadTime and a selfTime, child slices get a parentSlice
+     * reference.
+     */
+    createSubSlices: function() {
+      this.haveTopLevelSlicesBeenBuilt = true;
+      this.createSubSlicesImpl_();
+
+      this.slices.forEach(function(slice) {
+        var selfTime = slice.duration;
+        for (var i = 0; i < slice.subSlices.length; i++)
+          selfTime -= slice.subSlices[i].duration;
+        slice.selfTime = selfTime;
+
+        if (slice.cpuDuration === undefined)
+          return;
+
+        var cpuSelfTime = slice.cpuDuration;
+        for (var i = 0; i < slice.subSlices.length; i++) {
+          if (slice.subSlices[i].cpuDuration !== undefined)
+            cpuSelfTime -= slice.subSlices[i].cpuDuration;
+        }
+        slice.cpuSelfTime = cpuSelfTime;
+      });
+    },
+    createSubSlicesImpl_: function() {
+      function addSliceIfBounds(root, child) {
+        // Because we know that the start time of child is >= the start time
+        // of all other slices seen so far, we can just check the last slice
+        // of each row for bounding.
+        if (root.bounds(child)) {
+          if (root.subSlices && root.subSlices.length > 0) {
+            if (addSliceIfBounds(root.subSlices[root.subSlices.length - 1],
+                                 child))
+              return true;
+          }
+          child.parentSlice = root;
+          if (root.subSlices === undefined)
+            root.subSlices = [];
+          root.subSlices.push(child);
+          return true;
+        }
+        return false;
+      }
+
+      if (!this.slices.length)
+        return;
+
+      var ops = [];
+      for (var i = 0; i < this.slices.length; i++) {
+        if (this.slices[i].subSlices)
+          this.slices[i].subSlices.splice(0,
+                                          this.slices[i].subSlices.length);
+        ops.push(i);
+      }
+
+      var groupSlices = this.slices;
+      ops.sort(function(ix, iy) {
+        var x = groupSlices[ix];
+        var y = groupSlices[iy];
+        if (x.start != y.start)
+          return x.start - y.start;
+
+        // Elements get inserted into the slices array in order of when the
+        // slices start. Because slices must be properly nested, we break
+        // start-time ties by assuming that the elements appearing earlier
+        // in the slices array (and thus ending earlier) start earlier.
+        return ix - iy;
+      });
+
+      var rootSlice = this.slices[ops[0]];
+      this.topLevelSlices = [];
+      this.topLevelSlices.push(rootSlice);
+      for (var i = 1; i < ops.length; i++) {
+        var slice = this.slices[ops[i]];
+        if (!addSliceIfBounds(rootSlice, slice)) {
+          rootSlice = slice;
+          this.topLevelSlices.push(rootSlice);
+        }
+      }
+    }
+  };
+
+  /**
+   * Merge two slice groups.
+   *
+   * If the two groups do not nest properly some of the slices of groupB will
+   * be split to accomodate the improper nesting.  This is done to accomodate
+   * combined kernel and userland call stacks on Android.  Because userland
+   * tracing is done by writing to the trace_marker file, the kernel calls
+   * that get invoked as part of that write may not be properly nested with
+   * the userland call trace.  For example the following sequence may occur:
+   *
+   *     kernel enter sys_write        (the write to trace_marker)
+   *     user   enter some_function
+   *     kernel exit  sys_write
+   *     ...
+   *     kernel enter sys_write        (the write to trace_marker)
+   *     user   exit  some_function
+   *     kernel exit  sys_write
+   *
+   * This is handled by splitting the sys_write call into two slices as
+   * follows:
+   *
+   *     | sys_write |            some_function            | sys_write (cont.) |
+   *                 | sys_write (cont.) |     | sys_write |
+   *
+   * The colorId of both parts of the split slices are kept the same, and the
+   * " (cont.)" suffix is appended to the later parts of a split slice.
+   *
+   * The two input SliceGroups are not modified by this, and the merged
+   * SliceGroup will contain a copy of each of the input groups' slices (those
+   * copies may be split).
+   */
+  SliceGroup.merge = function(groupA, groupB) {
+    // This is implemented by traversing the two slice groups in reverse
+    // order.  The slices in each group are sorted by ascending end-time, so
+    // we must do the traversal from back to front in order to maintain the
+    // sorting.
+    //
+    // We traverse the two groups simultaneously, merging as we go.  At each
+    // iteration we choose the group from which to take the next slice based
+    // on which group's next slice has the greater end-time.  During this
+    // traversal we maintain a stack of currently "open" slices for each input
+    // group.  A slice is considered "open" from the time it gets reached in
+    // our input group traversal to the time we reach an slice in this
+    // traversal with an end-time before the start time of the "open" slice.
+    //
+    // Each time a slice from groupA is opened or closed (events corresponding
+    // to the end-time and start-time of the input slice, respectively) we
+    // split all of the currently open slices from groupB.
+
+    if (groupA.openPartialSlices_.length > 0) {
+      throw new Error('groupA has open partial slices');
+    }
+    if (groupB.openPartialSlices_.length > 0) {
+      throw new Error('groupB has open partial slices');
+    }
+    if (groupA.parentThread != groupB.parentThread)
+      throw new Error('Different parent threads. Cannot merge');
+
+    var result = new SliceGroup(groupA.parentThread);
+
+    var slicesA = groupA.slices;
+    var slicesB = groupB.slices;
+    var idxA = 0;
+    var idxB = 0;
+    var openA = [];
+    var openB = [];
+
+    var splitOpenSlices = function(when) {
+      for (var i = 0; i < openB.length; i++) {
+        var oldSlice = openB[i];
+        var oldEnd = oldSlice.end;
+        if (when < oldSlice.start || oldEnd < when) {
+          throw new Error('slice should not be split');
+        }
+
+        var newSlice = result.copySlice(oldSlice);
+        newSlice.start = when;
+        newSlice.duration = oldEnd - when;
+        if (newSlice.title.indexOf(' (cont.)') == -1)
+          newSlice.title += ' (cont.)';
+        oldSlice.duration = when - oldSlice.start;
+        openB[i] = newSlice;
+        result.pushSlice(newSlice);
+      }
+    };
+
+    var closeOpenSlices = function(upTo) {
+      while (openA.length > 0 || openB.length > 0) {
+        var nextA = openA[openA.length - 1];
+        var nextB = openB[openB.length - 1];
+        var endA = nextA && nextA.end;
+        var endB = nextB && nextB.end;
+
+        if ((endA === undefined || endA > upTo) &&
+            (endB === undefined || endB > upTo)) {
+          return;
+        }
+
+        if (endB === undefined || endA < endB) {
+          splitOpenSlices(endA);
+          openA.pop();
+        } else {
+          openB.pop();
+        }
+      }
+    };
+
+    while (idxA < slicesA.length || idxB < slicesB.length) {
+      var sA = slicesA[idxA];
+      var sB = slicesB[idxB];
+      var nextSlice, isFromB;
+
+      if (sA === undefined || (sB !== undefined && sA.start > sB.start)) {
+        nextSlice = result.copySlice(sB);
+        isFromB = true;
+        idxB++;
+      } else {
+        nextSlice = result.copySlice(sA);
+        isFromB = false;
+        idxA++;
+      }
+
+      closeOpenSlices(nextSlice.start);
+
+      result.pushSlice(nextSlice);
+
+      if (isFromB) {
+        openB.push(nextSlice);
+      } else {
+        splitOpenSlices(nextSlice.start);
+        openA.push(nextSlice);
+      }
+    }
+
+    closeOpenSlices();
+
+    return result;
+  };
+
+  return {
+    SliceGroup: SliceGroup
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/slice_group_test.html b/trace-viewer/trace_viewer/core/trace_model/slice_group_test.html
new file mode 100644
index 0000000..9e3d681
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/slice_group_test.html
@@ -0,0 +1,652 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/slice_group.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Slice = tv.c.trace_model.Slice;
+  var SliceGroup = tv.c.trace_model.SliceGroup;
+  var newSlice = tv.c.test_utils.newSlice;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  test('basicBeginEnd', function() {
+    var group = new SliceGroup({});
+    assert.equal(group.openSliceCount, 0);
+    var sliceA = group.beginSlice('', 'a', 1, {a: 1});
+    assert.equal(group.openSliceCount, 1);
+    assert.equal(sliceA.title, 'a');
+    assert.equal(sliceA.start, 1);
+    assert.equal(sliceA.args.a, 1);
+
+    var sliceB = group.endSlice(3);
+    assert.equal(sliceA, sliceB);
+    assert.equal(sliceB.duration, 2);
+  });
+
+  test('subSlicesBuilderBasic', function() {
+    var group = new SliceGroup({});
+    var sA = group.pushSlice(newSliceNamed('a', 1, 2));
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 2);
+    assert.deepEqual(group.topLevelSlices, [sA, sB]);
+  });
+
+  test('subSlicesBuilderBasic2', function() {
+    var group = new SliceGroup({});
+    var sA = group.pushSlice(newSliceNamed('a', 1, 4));
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 1);
+    assert.deepEqual(group.topLevelSlices, [sA]);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.deepEqual(sA.subSlices, [sB]);
+    assert.equal(sA.selfTime, 3);
+
+    assert.equal(sA, sB.parentSlice);
+  });
+
+  test('subSlicesBuilderNestedExactly', function() {
+    var group = new SliceGroup({});
+    var sB = group.pushSlice(newSliceNamed('b', 1, 4));
+    var sA = group.pushSlice(newSliceNamed('a', 1, 4));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 1);
+    assert.deepEqual(group.topLevelSlices, [sB]);
+
+    assert.equal(sB.subSlices.length, 1);
+    assert.deepEqual(sB.subSlices, [sA]);
+    assert.equal(sB.selfTime, 0);
+
+    assert.equal(sB, sA.parentSlice);
+  });
+
+  test('subSlicesBuilderInstantEvents', function() {
+    var group = new SliceGroup({});
+    var sA = group.pushSlice(newSliceNamed('a', 1, 0));
+    var sB = group.pushSlice(newSliceNamed('b', 2, 0));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 2);
+    assert.deepEqual(group.topLevelSlices, [sA, sB]);
+  });
+
+  test('subSlicesBuilderTwoInstantEvents', function() {
+    var group = new SliceGroup({});
+    var sA = group.pushSlice(newSliceNamed('a', 1, 0));
+    var sB = group.pushSlice(newSliceNamed('b', 1, 0));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 1);
+    assert.deepEqual(group.topLevelSlices, [sA]);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.deepEqual(sA.subSlices, [sB]);
+    assert.equal(sA.selfTime, 0);
+
+    assert.equal(sA, sB.parentSlice);
+  });
+
+  test('subSlicesBuilderOutOfOrderAddition', function() {
+    var group = new SliceGroup({});
+
+    // Pattern being tested:
+    // [    a     ][   b   ]
+    // Where insertion is done backward.
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+    var sA = group.pushSlice(newSliceNamed('a', 1, 2));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 2);
+    assert.deepEqual(group.topLevelSlices, [sA, sB]);
+  });
+
+  test('subRowBuilderOutOfOrderAddition2', function() {
+    var group = new SliceGroup({});
+
+    // Pattern being tested:
+    // [    a     ]
+    //   [  b   ]
+    // Where insertion is done backward.
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+    var sA = group.pushSlice(newSliceNamed('a', 1, 5));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 1);
+    assert.deepEqual(group.topLevelSlices, [sA]);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.deepEqual(sA.subSlices, [sB]);
+    assert.equal(sA.selfTime, 4);
+
+    assert.equal(sA, sB.parentSlice);
+  });
+
+  test('subSlicesBuilderOnNestedZeroLength', function() {
+    var group = new SliceGroup({});
+
+    // Pattern being tested:
+    // [    a    ]
+    // [  b1 ]  []<- b2 where b2.duration = 0 and b2.end == a.end.
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB1 = group.pushSlice(newSliceNamed('b1', 1, 2));
+    var sB2 = group.pushSlice(newSliceNamed('b2', 4, 0));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 1);
+    assert.deepEqual(group.topLevelSlices, [sA]);
+
+    assert.equal(sA.subSlices.length, 2);
+    assert.deepEqual(sA.subSlices, [sB1, sB2]);
+    assert.equal(sA.selfTime, 1);
+
+    assert.equal(sA, sB1.parentSlice);
+    assert.equal(sA, sB2.parentSlice);
+  });
+
+  test('subSlicesBuilderOnGroup1', function() {
+    var group = new SliceGroup({});
+
+    // Pattern being tested:
+    // [    a     ]   [  c   ]
+    //   [  b   ]
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB = group.pushSlice(newSliceNamed('b', 1.5, 1));
+    var sC = group.pushSlice(newSliceNamed('c', 5, 0));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 2);
+    assert.deepEqual(group.topLevelSlices, [sA, sC]);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.deepEqual(sA.subSlices, [sB]);
+    assert.equal(sA.selfTime, 2);
+
+    assert.equal(sA, sB.parentSlice);
+  });
+
+  test('subSlicesBuilderOnGroup2', function() {
+    var group = new SliceGroup({});
+
+    // Pattern being tested:
+    // [    a     ]   [  d   ]
+    //   [  b   ]
+    //    [ c ]
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB = group.pushSlice(newSliceNamed('b', 1.5, 1));
+    var sC = group.pushSlice(newSliceNamed('c', 1.75, 0.5));
+    var sD = group.pushSlice(newSliceNamed('d', 5, 0.25));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 2);
+    assert.deepEqual(group.topLevelSlices, [sA, sD]);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.deepEqual(sA.subSlices, [sB]);
+    assert.equal(sA.selfTime, 2);
+
+    assert.equal(sA, sB.parentSlice);
+    assert.equal(sB.subSlices.length, 1);
+    assert.deepEqual(sB.subSlices, [sC]);
+    assert.equal(sB.selfTime, 0.5);
+
+    assert.equal(sB, sC.parentSlice);
+  });
+
+  test('subSlicesBuilderTolerateFPInaccuracy', function() {
+    var group = new SliceGroup({});
+
+    // Pattern being tested:
+    // [  a  ]
+    // [  b  ] where b.end contains a tiny FP calculation error.
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB = group.pushSlice(newSliceNamed('b', 1, 3.0000000001));
+
+    group.createSubSlices();
+
+    assert.equal(group.topLevelSlices.length, 1);
+    assert.deepEqual(group.topLevelSlices, [sA]);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.deepEqual(sA.subSlices, [sB]);
+    assert.equal(sA, sB.parentSlice);
+  });
+
+  test('basicMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 1);
+    a.endSlice(2);
+    b.beginSlice('', 'two', 3);
+    b.endSlice(5);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 2);
+
+    assert.equal(m.slices[0].title, 'one');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 1);
+
+    assert.equal(m.slices[1].title, 'two');
+    assert.equal(m.slices[1].start, 3);
+    assert.equal(m.slices[1].duration, 2);
+  });
+
+  test('nestedMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 1);
+    a.endSlice(4);
+    b.beginSlice('', 'two', 2);
+    b.endSlice(3);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 2);
+
+    assert.equal(m.slices[0].title, 'one');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 3);
+
+    assert.equal(m.slices[1].title, 'two');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 1);
+  });
+
+  test('startSplitMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 2);
+    a.endSlice(4);
+    b.beginSlice('', 'two', 1);
+    b.endSlice(3);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 3);
+
+    assert.equal(m.slices[0].title, 'two');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 1);
+
+    assert.equal(m.slices[1].title, 'one');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 2);
+
+    assert.equal(m.slices[2].title, 'two (cont.)');
+    assert.equal(m.slices[2].start, 2);
+    assert.equal(m.slices[2].duration, 1);
+  });
+
+  test('startSplitTwoMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 3);
+    a.endSlice(6);
+    b.beginSlice('', 'two', 1);
+    b.beginSlice('', 'three', 2);
+    b.endSlice(4);
+    b.endSlice(5);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 5);
+
+    assert.equal(m.slices[0].title, 'two');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 2);
+
+    assert.equal(m.slices[1].title, 'three');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 1);
+
+    assert.equal(m.slices[2].title, 'one');
+    assert.equal(m.slices[2].start, 3);
+    assert.equal(m.slices[2].duration, 3);
+
+    assert.equal(m.slices[3].title, 'two (cont.)');
+    assert.equal(m.slices[3].start, 3);
+    assert.equal(m.slices[3].duration, 2);
+
+    assert.equal(m.slices[4].title, 'three (cont.)');
+    assert.equal(m.slices[4].start, 3);
+    assert.equal(m.slices[4].duration, 1);
+  });
+
+  test('startSplitTwiceMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 2);
+    a.beginSlice('', 'two', 3);
+    a.endSlice(5);
+    a.endSlice(6);
+    b.beginSlice('', 'three', 1);
+    b.endSlice(4);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 5);
+
+    assert.equal(m.slices[0].title, 'three');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 1);
+
+    assert.equal(m.slices[1].title, 'one');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 4);
+
+    assert.equal(m.slices[2].title, 'three (cont.)');
+    assert.equal(m.slices[2].start, 2);
+    assert.equal(m.slices[2].duration, 1);
+
+    assert.equal(m.slices[3].title, 'two');
+    assert.equal(m.slices[3].start, 3);
+    assert.equal(m.slices[3].duration, 2);
+
+    assert.equal(m.slices[4].title, 'three (cont.)');
+    assert.equal(m.slices[4].start, 3);
+    assert.equal(m.slices[4].duration, 1);
+  });
+
+  test('endSplitMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 1);
+    a.endSlice(3);
+    b.beginSlice('', 'two', 2);
+    b.endSlice(4);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 3);
+
+    assert.equal(m.slices[0].title, 'one');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 2);
+
+    assert.equal(m.slices[1].title, 'two');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 1);
+
+    assert.equal(m.slices[2].title, 'two (cont.)');
+    assert.equal(m.slices[2].start, 3);
+    assert.equal(m.slices[2].duration, 1);
+  });
+
+  test('endSplitTwoMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 1);
+    a.endSlice(4);
+    b.beginSlice('', 'two', 2);
+    b.beginSlice('', 'three', 3);
+    b.endSlice(5);
+    b.endSlice(6);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 5);
+
+    assert.equal(m.slices[0].title, 'one');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 3);
+
+    assert.equal(m.slices[1].title, 'two');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 2);
+
+    assert.equal(m.slices[2].title, 'three');
+    assert.equal(m.slices[2].start, 3);
+    assert.equal(m.slices[2].duration, 1);
+
+    assert.equal(m.slices[3].title, 'two (cont.)');
+    assert.equal(m.slices[3].start, 4);
+    assert.equal(m.slices[3].duration, 2);
+
+    assert.equal(m.slices[4].title, 'three (cont.)');
+    assert.equal(m.slices[4].start, 4);
+    assert.equal(m.slices[4].duration, 1);
+  });
+
+  test('endSplitTwiceMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 1);
+    a.beginSlice('', 'two', 2);
+    a.endSlice(4);
+    a.endSlice(5);
+    b.beginSlice('', 'three', 3);
+    b.endSlice(6);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 5);
+
+    assert.equal(m.slices[0].title, 'one');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 4);
+
+    assert.equal(m.slices[1].title, 'two');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 2);
+
+    assert.equal(m.slices[2].title, 'three');
+    assert.equal(m.slices[2].start, 3);
+    assert.equal(m.slices[2].duration, 1);
+
+    assert.equal(m.slices[3].title, 'three (cont.)');
+    assert.equal(m.slices[3].start, 4);
+    assert.equal(m.slices[3].duration, 1);
+
+    assert.equal(m.slices[4].title, 'three (cont.)');
+    assert.equal(m.slices[4].start, 5);
+    assert.equal(m.slices[4].duration, 1);
+  });
+
+  // Input:
+  // A:  |    one     |       |     two     |
+  //
+  // B:       |         three         |
+  //
+  // Output:
+  //     |    one     | three |     two     |
+  //          | three |       | three |
+  test('splitTwiceMerge', function() {
+    var thread = {};
+    var a = new SliceGroup(thread);
+    var b = new SliceGroup(thread);
+    a.beginSlice('', 'one', 1);
+    a.endSlice(3);
+    a.beginSlice('', 'two', 4);
+    a.endSlice(6);
+    b.beginSlice('', 'three', 2);
+    b.endSlice(5);
+
+    var m = SliceGroup.merge(a, b);
+    assert.equal(m.slices.length, 5);
+
+    assert.equal(m.slices[0].title, 'one');
+    assert.equal(m.slices[0].start, 1);
+    assert.equal(m.slices[0].duration, 2);
+
+    assert.equal(m.slices[1].title, 'three');
+    assert.equal(m.slices[1].start, 2);
+    assert.equal(m.slices[1].duration, 1);
+
+    assert.equal(m.slices[2].title, 'three (cont.)');
+    assert.equal(m.slices[2].start, 3);
+    assert.equal(m.slices[2].duration, 1);
+
+    assert.equal(m.slices[3].title, 'two');
+    assert.equal(m.slices[3].start, 4);
+    assert.equal(m.slices[3].duration, 2);
+
+    assert.equal(m.slices[4].title, 'three (cont.)');
+    assert.equal(m.slices[4].start, 4);
+    assert.equal(m.slices[4].duration, 1);
+  });
+
+  test('bounds', function() {
+    var group = new SliceGroup({});
+    group.updateBounds();
+    assert.isUndefined(group.bounds.min);
+    assert.isUndefined(group.bounds.max);
+
+    group.pushSlice(newSlice(1, 3));
+    group.pushSlice(newSlice(7, 2));
+    group.updateBounds();
+    assert.equal(group.bounds.min, 1);
+    assert.equal(group.bounds.max, 9);
+  });
+
+  test('boundsWithPartial', function() {
+    var group = new SliceGroup({});
+    group.beginSlice('', 'a', 7);
+    group.updateBounds();
+    assert.equal(group.bounds.min, 7);
+    assert.equal(group.bounds.max, 7);
+  });
+
+  test('boundsWithTwoPartials', function() {
+    var group = new SliceGroup({});
+    group.beginSlice('', 'a', 0);
+    group.beginSlice('', 'a', 1);
+    group.updateBounds();
+    assert.equal(group.bounds.min, 0);
+    assert.equal(group.bounds.max, 1);
+  });
+
+  test('boundsWithBothPartialAndRegular', function() {
+    var group = new SliceGroup({});
+    group.updateBounds();
+    assert.isUndefined(group.bounds.min);
+    assert.isUndefined(group.bounds.max);
+
+    group.pushSlice(newSlice(1, 3));
+    group.beginSlice('', 'a', 7);
+    group.updateBounds();
+    assert.equal(group.bounds.min, 1);
+    assert.equal(group.bounds.max, 7);
+  });
+
+  test('autocloserBasic', function() {
+    var group = new SliceGroup({});
+    assert.equal(0, group.openSliceCount);
+
+    group.pushSlice(newSliceNamed('a', 1, 0.5));
+
+    group.beginSlice('', 'b', 2);
+    group.beginSlice('', 'c', 2.5);
+    group.endSlice(3);
+
+    group.autoCloseOpenSlices();
+    group.updateBounds();
+
+    assert.equal(group.bounds.min, 1);
+    assert.equal(group.bounds.max, 3);
+    assert.equal(group.slices.length, 3);
+
+    assert.equal(group.slices[0].title, 'a');
+    assert.isFalse(group.slices[0].didNotFinish);
+
+    assert.equal(group.slices[1].title, 'b');
+    assert.isTrue(group.slices[1].didNotFinish);
+    assert.equal(group.slices[1].duration, 1);
+
+    assert.equal(group.slices[2].title, 'c');
+    assert.isFalse(group.slices[2].didNotFinish);
+  });
+
+  test('autocloserWithSubTasks', function() {
+    var group = new SliceGroup({});
+    assert.equal(0, group.openSliceCount);
+
+    group.beginSlice('', 'a', 1);
+    group.beginSlice('', 'b1', 2);
+    group.endSlice(3);
+    group.beginSlice('', 'b2', 3);
+
+    group.autoCloseOpenSlices();
+    assert.equal(group.slices.length, 3);
+
+    assert.equal(group.slices[0].title, 'a');
+    assert.isTrue(group.slices[0].didNotFinish);
+    assert.equal(group.slices[0].duration, 2);
+
+    assert.equal(group.slices[1].title, 'b1');
+    assert.isFalse(group.slices[1].didNotFinish);
+    assert.equal(group.slices[1].duration, 1);
+
+    assert.equal(group.slices[2].title, 'b2');
+    assert.isTrue(group.slices[2].didNotFinish);
+    assert.equal(group.slices[2].duration, 0);
+  });
+
+  test('autocloseCompleteSlice', function() {
+    var group = new SliceGroup({});
+
+    group.pushCompleteSlice('', 'a', 1, undefined);
+    group.pushCompleteSlice('', 'b', 2, 3);
+
+    group.autoCloseOpenSlices();
+    assert.equal(group.slices.length, 2);
+
+    assert.equal(group.slices[0].title, 'a');
+    assert.isTrue(group.slices[0].didNotFinish);
+    assert.equal(group.slices[0].duration, 4);
+
+    assert.equal(group.slices[1].title, 'b');
+    assert.isFalse(group.slices[1].didNotFinish);
+    assert.equal(group.slices[1].duration, 3);
+  });
+
+  test('sliceGroupStableId', function() {
+    var model = new tv.c.TraceModel();
+    var process = model.getOrCreateProcess(123);
+    var thread = process.getOrCreateThread(456);
+    var group = new SliceGroup(thread);
+
+    assert.equal(process.stableId, 123);
+    assert.equal(thread.stableId, '123.456');
+    assert.equal(group.stableId, '123.456.SliceGroup');
+  });
+
+  test('getSlicesOfName', function() {
+    var group = new SliceGroup({});
+    var expected = [];
+
+    for (var i = 0; i < 10; i++) {
+      var aSlice = newSliceNamed('a', i, i + 1);
+      group.pushSlice(aSlice);
+      group.pushSlice(newSliceNamed('b', i + 1, i + 2));
+      expected.push(aSlice);
+    }
+
+    assert.deepEqual(group.getSlicesOfName('a'), expected);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/slice_test.html b/trace-viewer/trace_viewer/core/trace_model/slice_test.html
new file mode 100644
index 0000000..f2d2dd3
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/slice_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/slice_group.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Slice = tv.c.trace_model.Slice;
+  var SliceGroup = tv.c.trace_model.SliceGroup;
+  var newSlice = tv.c.test_utils.newSlice;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  test('findDescendentSlice', function() {
+    var group = new SliceGroup({});
+
+    var sA = group.pushSlice(newSliceNamed('a', 1, 10));
+    var sB = group.pushSlice(newSliceNamed('b', 2, 8));
+    var sC = group.pushSlice(newSliceNamed('c', 3, 6));
+
+    group.createSubSlices();
+
+    assert.equal(sB, sA.findDescendentSlice('b'));
+    assert.equal(sC, sA.findDescendentSlice('c'));
+    assert.isUndefined(sA.findDescendentSlice('d'));
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/stack_frame.html b/trace-viewer/trace_viewer/core/trace_model/stack_frame.html
new file mode 100644
index 0000000..40796d1
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/stack_frame.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+  function StackFrame(parentFrame, id, category, title, colorId) {
+    if (id === undefined)
+      throw new Error('id must be given');
+    this.parentFrame_ = parentFrame;
+    this.id = id;
+    this.category = category || '';
+    this.title = title;
+    this.colorId = colorId;
+    this.children = [];
+
+    if (this.parentFrame_)
+      this.parentFrame_.addChild(this);
+  }
+
+  StackFrame.prototype = {
+    get parentFrame() {
+      return this.parentFrame_;
+    },
+
+    set parentFrame(parentFrame) {
+      if (this.parentFrame_)
+        this.parentFrame_.removeChild(this);
+      this.parentFrame_ = parentFrame;
+      if (this.parentFrame_)
+        this.parentFrame_.addChild(this);
+    },
+
+    addChild: function(child) {
+      this.children.push(child);
+    },
+
+    removeChild: function(child) {
+      var i = this.children.indexOf(child.id);
+      if (i == -1)
+        throw new Error('omg');
+      this.children.splice(i, 1);
+    },
+
+    removeAllChildren: function() {
+      for (var i = 0; i < this.children.length; i++)
+        this.children[i].parentFrame_ = undefined;
+      this.children.splice(0, this.children.length);
+    },
+
+    get stackTrace() {
+      var stack = [];
+      var cur = this;
+      while (cur) {
+        stack.push(cur);
+        cur = cur.parentFrame;
+      }
+      stack.reverse();
+      return stack;
+    },
+
+    getUserFriendlyStackTrace: function() {
+      return this.stackTrace.map(function(x) {
+        return x.category + ': ' + x.title;
+      });
+    }
+  };
+
+  return {
+    StackFrame: StackFrame
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/thread.html b/trace-viewer/trace_viewer/core/trace_model/thread.html
new file mode 100644
index 0000000..52ecd76
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/thread.html
@@ -0,0 +1,311 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event_container.html">
+<link rel="import" href="/core/trace_model/thread_slice.html">
+<link rel="import" href="/core/trace_model/slice_group.html">
+<link rel="import" href="/core/trace_model/async_slice_group.html">
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/range.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Thread class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var Slice = tv.c.trace_model.Slice;
+  var SliceGroup = tv.c.trace_model.SliceGroup;
+  var AsyncSlice = tv.c.trace_model.AsyncSlice;
+  var AsyncSliceGroup = tv.c.trace_model.AsyncSliceGroup;
+  var ThreadSlice = tv.c.trace_model.ThreadSlice;
+
+  /**
+   * A ThreadSlice represents an interval of time on a thread resource
+   * with associated nestinged slice information.
+   *
+   * ThreadSlices are typically associated with a specific trace event pair on a
+   * specific thread.
+   * For example,
+   *   TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms
+   *   TRACE_EVENT_END0()                 at time=0.3ms
+   * This results in a single slice from 0.1 with duration 0.2 on a
+   * specific thread.
+   *
+   * @constructor
+   */
+  function ThreadSlice(cat, title, colorId, start, args, opt_duration,
+                       opt_cpuStart, opt_cpuDuration) {
+    Slice.call(this, cat, title, colorId, start, args, opt_duration,
+               opt_cpuStart, opt_cpuDuration);
+    // Do not modify this directly.
+    // subSlices is configured by SliceGroup.rebuildSubRows_.
+    this.subSlices = [];
+  }
+
+  ThreadSlice.prototype = {
+    __proto__: Slice.prototype
+  };
+
+  /**
+   * A Thread stores all the trace events collected for a particular
+   * thread. We organize the synchronous slices on a thread by "subrows," where
+   * subrow 0 has all the root slices, subrow 1 those nested 1 deep, and so on.
+   * The asynchronous slices are stored in an AsyncSliceGroup object.
+   *
+   * The slices stored on a Thread should be instances of
+   * ThreadSlice.
+   *
+   * @constructor
+   * @extends {tv.c.trace_model.EventContainer}
+   */
+  function Thread(parent, tid) {
+    this.guid_ = tv.b.GUID.allocate();
+    if (!parent)
+      throw new Error('Parent must be provided.');
+    this.parent = parent;
+    this.sortIndex = 0;
+    this.tid = tid;
+    this.name = undefined;
+    this.samples_ = undefined; // Set during createSubSlices
+
+    var that = this;
+    function ThreadSliceForThisThread(
+        cat, title, colorId, start, args, opt_duration,
+        opt_cpuStart, opt_cpuDuration) {
+      ThreadSlice.call(this, cat, title, colorId, start, args, opt_duration,
+                       opt_cpuStart, opt_cpuDuration);
+      this.parentThread = that;
+    }
+    ThreadSliceForThisThread.prototype = {
+      __proto__: ThreadSlice.prototype
+    };
+
+    this.sliceGroup = new SliceGroup(this, ThreadSliceForThisThread, 'slices');
+    this.timeSlices = undefined;
+    this.kernelSliceGroup = new SliceGroup(this, undefined, 'kernel-slices');
+    this.asyncSliceGroup = new AsyncSliceGroup(this, 'async-slices');
+    this.bounds = new tv.b.Range();
+  }
+
+  Thread.prototype = {
+    __proto__: tv.c.trace_model.EventContainer.prototype,
+
+    /*
+     * @return {Number} A globally unique identifier for this counter.
+     */
+    get guid() {
+      return this.guid_;
+    },
+
+    get stableId() {
+      return this.parent.stableId + '.' + this.tid;
+    },
+
+    compareTo: function(that) {
+      return Thread.compare(this, that);
+    },
+
+    /**
+     * Shifts all the timestamps inside this thread forward by the amount
+     * specified.
+     */
+    shiftTimestampsForward: function(amount) {
+      this.sliceGroup.shiftTimestampsForward(amount);
+
+      if (this.timeSlices) {
+        for (var i = 0; i < this.timeSlices.length; i++) {
+          var slice = this.timeSlices[i];
+          slice.start += amount;
+        }
+      }
+
+      this.kernelSliceGroup.shiftTimestampsForward(amount);
+      this.asyncSliceGroup.shiftTimestampsForward(amount);
+    },
+
+    /**
+     * Determines whether this thread is empty. If true, it usually implies
+     * that it should be pruned from the model.
+     */
+    get isEmpty() {
+      if (this.sliceGroup.length)
+        return false;
+      if (this.sliceGroup.openSliceCount)
+        return false;
+      if (this.timeSlices && this.timeSlices.length)
+        return false;
+      if (this.kernelSliceGroup.length)
+        return false;
+      if (this.asyncSliceGroup.length)
+        return false;
+      if (this.samples_.length)
+        return false;
+      return true;
+    },
+
+    /**
+     * Updates the bounds based on the
+     * current objects associated with the thread.
+     */
+    updateBounds: function() {
+      this.bounds.reset();
+
+      this.sliceGroup.updateBounds();
+      this.bounds.addRange(this.sliceGroup.bounds);
+
+      this.kernelSliceGroup.updateBounds();
+      this.bounds.addRange(this.kernelSliceGroup.bounds);
+
+      this.asyncSliceGroup.updateBounds();
+      this.bounds.addRange(this.asyncSliceGroup.bounds);
+
+      if (this.timeSlices && this.timeSlices.length) {
+        this.bounds.addValue(this.timeSlices[0].start);
+        this.bounds.addValue(
+            this.timeSlices[this.timeSlices.length - 1].end);
+      }
+
+      if (this.samples_ && this.samples_.length) {
+        this.bounds.addValue(this.samples_[0].start);
+        this.bounds.addValue(
+            this.samples_[this.samples_.length - 1].end);
+      }
+    },
+
+    addCategoriesToDict: function(categoriesDict) {
+      for (var i = 0; i < this.sliceGroup.length; i++)
+        categoriesDict[this.sliceGroup.slices[i].category] = true;
+      for (var i = 0; i < this.kernelSliceGroup.length; i++)
+        categoriesDict[this.kernelSliceGroup.slices[i].category] = true;
+      for (var i = 0; i < this.asyncSliceGroup.length; i++)
+        categoriesDict[this.asyncSliceGroup.slices[i].category] = true;
+      if (this.samples_) {
+        for (var i = 0; i < this.samples_.length; i++)
+          categoriesDict[this.samples_[i].category] = true;
+      }
+    },
+
+    autoCloseOpenSlices: function(opt_maxTimestamp) {
+      this.sliceGroup.autoCloseOpenSlices(opt_maxTimestamp);
+      this.kernelSliceGroup.autoCloseOpenSlices(opt_maxTimestamp);
+    },
+
+    mergeKernelWithUserland: function() {
+      if (this.kernelSliceGroup.length > 0) {
+        var newSlices = SliceGroup.merge(
+            this.sliceGroup, this.kernelSliceGroup);
+        this.sliceGroup.slices = newSlices.slices;
+        this.kernelSliceGroup = new SliceGroup(this);
+        this.updateBounds();
+      }
+    },
+
+    createSubSlices: function() {
+      this.sliceGroup.createSubSlices();
+      this.samples_ = this.parent.model.samples.filter(function(sample) {
+        return sample.thread == this;
+      }, this);
+    },
+
+    /**
+     * @return {String} A user-friendly name for this thread.
+     */
+    get userFriendlyName() {
+      return this.name || this.tid;
+    },
+
+    /**
+     * @return {String} User friendly details about this thread.
+     */
+    get userFriendlyDetails() {
+      return 'tid: ' + this.tid +
+          (this.name ? ', name: ' + this.name : '');
+    },
+
+    getSettingsKey: function() {
+      if (!this.name)
+        return undefined;
+      var parentKey = this.parent.getSettingsKey();
+      if (!parentKey)
+        return undefined;
+      return parentKey + '.' + this.name;
+    },
+
+    /*
+     * Returns the index of the slice in the timeSlices array, or undefined.
+     */
+    indexOfTimeSlice: function(timeSlice) {
+      var i = tv.b.findLowIndexInSortedArray(
+          this.timeSlices,
+          function(slice) { return slice.start; },
+          timeSlice.start);
+      if (this.timeSlices[i] !== timeSlice)
+        return undefined;
+      return i;
+    },
+
+    iterateAllEvents: function(callback, opt_this) {
+      this.sliceGroup.iterateAllEvents(callback, opt_this);
+      this.kernelSliceGroup.iterateAllEvents(callback, opt_this);
+      this.asyncSliceGroup.iterateAllEvents(callback, opt_this);
+
+      if (this.timeSlices && this.timeSlices.length)
+        this.timeSlices.forEach(callback, opt_this);
+    },
+
+    iterateAllPersistableObjects: function(cb) {
+      cb(this);
+      if (this.sliceGroup.length)
+        cb(this.sliceGroup);
+      this.asyncSliceGroup.viewSubGroups.forEach(cb);
+    },
+
+    iterateAllEventContainers: function(callback) {
+      callback(this);
+
+      if (this.sliceGroup.length)
+        this.sliceGroup.iterateAllEventContainers(callback);
+      if (this.kernelSliceGroup.length)
+        this.kernelSliceGroup.iterateAllEventContainers(callback);
+      if (this.asyncSliceGroup.length)
+        this.asyncSliceGroup.iterateAllEventContainers(callback);
+    },
+
+    get samples() {
+      return this.samples_;
+    }
+  };
+
+  /**
+   * Comparison between threads that orders first by parent.compareTo,
+   * then by names, then by tid.
+   */
+  Thread.compare = function(x, y) {
+    var tmp = x.parent.compareTo(y.parent);
+    if (tmp)
+      return tmp;
+
+    tmp = x.sortIndex - y.sortIndex;
+    if (tmp)
+      return tmp;
+
+    tmp = tv.b.comparePossiblyUndefinedValues(
+        x.name, y.name,
+        function(x, y) { return x.localeCompare(y); });
+    if (tmp)
+      return tmp;
+
+    return x.tid - y.tid;
+  };
+
+  return {
+    Thread: Thread
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/thread_slice.html b/trace-viewer/trace_viewer/core/trace_model/thread_slice.html
new file mode 100644
index 0000000..1317c1c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/thread_slice.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/slice.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the Thread class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  var Slice = tv.c.trace_model.Slice;
+
+  /**
+   * A ThreadSlice represents an interval of time on a thread resource
+   * with associated nestinged slice information.
+   *
+   * ThreadSlices are typically associated with a specific trace event pair on a
+   * specific thread.
+   * For example,
+   *   TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms
+   *   TRACE_EVENT_END0()                 at time=0.3ms
+   * This results in a single slice from 0.1 with duration 0.2 on a
+   * specific thread.
+   *
+   * @constructor
+   */
+  function ThreadSlice(cat, title, colorId, start, args, opt_duration,
+                       opt_cpuStart, opt_cpuDuration) {
+    Slice.call(this, cat, title, colorId, start, args, opt_duration,
+               opt_cpuStart, opt_cpuDuration);
+    // Do not modify this directly.
+    // subSlices is configured by SliceGroup.rebuildSubRows_.
+    this.subSlices = [];
+  }
+
+  ThreadSlice.prototype = {
+    __proto__: Slice.prototype
+  };
+  return {
+    ThreadSlice: ThreadSlice
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/thread_test.html b/trace-viewer/trace_viewer/core/trace_model/thread_test.html
new file mode 100644
index 0000000..5715c8d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/thread_test.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ThreadSlice = tv.c.trace_model.ThreadSlice;
+  var Process = tv.c.trace_model.Process;
+  var Thread = tv.c.trace_model.Thread;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+  var newAsyncSlice = tv.c.test_utils.newAsyncSlice;
+
+  test('threadBounds_Empty', function() {
+    var model = new tv.c.TraceModel();
+    var t = new Thread(new Process(model, 7), 1);
+    t.updateBounds();
+    assert.isUndefined(t.bounds.min);
+    assert.isUndefined(t.bounds.max);
+  });
+
+  test('threadBounds_SubRow', function() {
+    var model = new tv.c.TraceModel();
+    var t = new Thread(new Process(model, 7), 1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+    t.updateBounds();
+    assert.equal(t.bounds.min, 1);
+    assert.equal(t.bounds.max, 4);
+  });
+
+  test('threadBounds_AsyncSliceGroup', function() {
+    var model = new tv.c.TraceModel();
+    var t = new Thread(new Process(model, 7), 1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+    t.asyncSliceGroup.push(newAsyncSlice(0.1, 5, t, t));
+    t.updateBounds();
+    assert.equal(t.bounds.min, 0.1);
+    assert.equal(t.bounds.max, 5.1);
+  });
+
+  test('threadBounds_Cpu', function() {
+    var model = new tv.c.TraceModel();
+    var t = new Thread(new Process(model, 7), 1);
+    t.timeSlices = [newSliceNamed('x', 0, 1)];
+    t.updateBounds();
+    assert.equal(t.bounds.min, 0);
+    assert.equal(t.bounds.max, 1);
+  });
+
+  test('shiftTimestampsForwardWithCpu', function() {
+    var model = new tv.c.TraceModel();
+    var t = new Thread(new Process(model, 7), 1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 0, {}, 3));
+    t.asyncSliceGroup.push(newAsyncSlice(0, 5, t, t));
+    t.timeSlices = [newSliceNamed('x', 0, 1)];
+
+    var shiftCount = 0;
+    t.asyncSliceGroup.shiftTimestampsForward = function(ts) {
+      if (ts == 0.32)
+        shiftCount++;
+    };
+
+    t.shiftTimestampsForward(0.32);
+
+    assert.equal(shiftCount, 1);
+    assert.equal(t.sliceGroup.slices[0].start, 0.32);
+    assert.equal(t.timeSlices[0].start, 0.32);
+  });
+
+  test('shiftTimestampsForwardWithoutCpu', function() {
+    var model = new tv.c.TraceModel();
+    var t = new Thread(new Process(model, 7), 1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 0, {}, 3));
+    t.asyncSliceGroup.push(newAsyncSlice(0, 5, t, t));
+
+    var shiftCount = 0;
+    t.asyncSliceGroup.shiftTimestampsForward = function(ts) {
+      if (ts == 0.32)
+        shiftCount++;
+    };
+
+    t.shiftTimestampsForward(0.32);
+
+    assert.equal(shiftCount, 1);
+    assert.equal(t.sliceGroup.slices[0].start, 0.32);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/thread_time_slice.html b/trace-viewer/trace_viewer/core/trace_model/thread_time_slice.html
new file mode 100644
index 0000000..d61578b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/thread_time_slice.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/core/trace_model/slice.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+  var Slice = tv.c.trace_model.Slice;
+
+  /**
+   * A ThreadTimeSlice is a slice of time on a specific thread where that thread
+   * was running on a specific CPU, or in a specific sleep state.
+   *
+   * As a thread switches moves through its life, it sometimes goes to sleep and
+   * can't run. Other times, its runnable but isn't actually assigned to a CPU.
+   * Finally, sometimes it gets put on a CPU to actually execute. Each of these
+   * states is represented by a ThreadTimeSlice:
+   *
+   *   Sleeping or runnable: cpuOnWhichThreadWasRunning is undefined
+   *   Running:  cpuOnWhichThreadWasRunning is set.
+   *
+   * @constructor
+   */
+  function ThreadTimeSlice(
+      thread, cat, title, colorId, start, args, opt_duration) {
+    Slice.call(this, cat, title, colorId, start, args, opt_duration);
+    this.thread = thread;
+    this.cpuOnWhichThreadWasRunning = undefined;
+  }
+
+  ThreadTimeSlice.prototype = {
+    __proto__: Slice.prototype,
+
+    get analysisTypeName() {
+      return 'tv.c.analysis.ThreadTimeSlice';
+    },
+
+    getAssociatedCpuSlice: function() {
+      if (!this.cpuOnWhichThreadWasRunning)
+        return undefined;
+      var cpuSlices = this.cpuOnWhichThreadWasRunning.slices;
+      for (var i = 0; i < cpuSlices.length; i++) {
+        var cpuSlice = cpuSlices[i];
+        if (cpuSlice.start !== this.start)
+          continue;
+        if (cpuSlice.duration !== this.duration)
+          continue;
+        return cpuSlice;
+      }
+      return undefined;
+    },
+
+    getCpuSliceThatTookCpu: function() {
+      if (this.cpuOnWhichThreadWasRunning)
+        return undefined;
+      var curIndex = this.thread.indexOfTimeSlice(this);
+      var cpuSliceWhenLastRunning;
+      while (curIndex >= 0) {
+        var curSlice = this.thread.timeSlices[curIndex];
+        if (!curSlice.cpuOnWhichThreadWasRunning) {
+          curIndex--;
+          continue;
+        }
+        cpuSliceWhenLastRunning = curSlice.getAssociatedCpuSlice();
+        break;
+      }
+      if (!cpuSliceWhenLastRunning)
+        return undefined;
+
+      var cpu = cpuSliceWhenLastRunning.cpu;
+      var indexOfSliceOnCpuWhenLastRunning =
+          cpu.indexOf(cpuSliceWhenLastRunning);
+      var nextRunningSlice = cpu.slices[indexOfSliceOnCpuWhenLastRunning + 1];
+      if (!nextRunningSlice)
+        return undefined;
+      if (Math.abs(nextRunningSlice.start - cpuSliceWhenLastRunning.end) <
+          0.00001)
+        return nextRunningSlice;
+      return undefined;
+    }
+  };
+
+  tv.c.trace_model.EventRegistry.register(
+      ThreadTimeSlice,
+      {
+        name: 'threadTimeSlice',
+        pluralName: 'threadTimeSlices',
+        singleViewElementName: 'tv-c-single-thread-time-slice-sub-view',
+        multiViewElementName: 'tv-c-multi-slice-sub-view'
+      });
+
+
+  return {
+    ThreadTimeSlice: ThreadTimeSlice
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/time_to_object_instance_map.html b/trace-viewer/trace_viewer/core/trace_model/time_to_object_instance_map.html
new file mode 100644
index 0000000..c59c3f1
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/time_to_object_instance_map.html
@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the TimeToObjectInstanceMap class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * Tracks all the instances associated with a given ID over its lifetime.
+   *
+   * An id can be used multiple times throughout a trace, referring to different
+   * objects at different times. This data structure does the bookkeeping to
+   * figure out what ObjectInstance is referred to at a given timestamp.
+   *
+   * @constructor
+   */
+  function TimeToObjectInstanceMap(createObjectInstanceFunction, parent, id) {
+    this.createObjectInstanceFunction_ = createObjectInstanceFunction;
+    this.parent = parent;
+    this.id = id;
+    this.instances = [];
+  }
+
+  TimeToObjectInstanceMap.prototype = {
+    idWasCreated: function(category, name, ts) {
+      if (this.instances.length == 0) {
+        this.instances.push(this.createObjectInstanceFunction_(
+            this.parent, this.id, category, name, ts));
+        this.instances[0].creationTsWasExplicit = true;
+        return this.instances[0];
+      }
+
+      var lastInstance = this.instances[this.instances.length - 1];
+      if (ts < lastInstance.deletionTs) {
+        throw new Error('Mutation of the TimeToObjectInstanceMap must be ' +
+                        'done in ascending timestamp order.');
+      }
+      lastInstance = this.createObjectInstanceFunction_(
+          this.parent, this.id, category, name, ts);
+      lastInstance.creationTsWasExplicit = true;
+      this.instances.push(lastInstance);
+      return lastInstance;
+    },
+
+    addSnapshot: function(category, name, ts, args, opt_baseTypeName) {
+      if (this.instances.length == 0) {
+        this.instances.push(this.createObjectInstanceFunction_(
+            this.parent, this.id, category, name, ts, opt_baseTypeName));
+      }
+
+      var i = tv.b.findLowIndexInSortedIntervals(
+          this.instances,
+          function(inst) { return inst.creationTs; },
+          function(inst) { return inst.deletionTs - inst.creationTs; },
+          ts);
+
+      var instance;
+      if (i < 0) {
+        instance = this.instances[0];
+        if (ts > instance.deletionTs ||
+            instance.creationTsWasExplicit) {
+          throw new Error(
+              'At the provided timestamp, no instance was still alive');
+        }
+
+        if (instance.snapshots.length != 0) {
+          throw new Error(
+              'Cannot shift creationTs forward, ' +
+              'snapshots have been added. First snap was at ts=' +
+              instance.snapshots[0].ts + ' and creationTs was ' +
+              instance.creationTs);
+        }
+        instance.creationTs = ts;
+      } else if (i >= this.instances.length) {
+        instance = this.instances[this.instances.length - 1];
+        if (ts >= instance.deletionTs) {
+          // The snap is added after our oldest and deleted instance. This means
+          // that this is a new implicit instance.
+          instance = this.createObjectInstanceFunction_(
+              this.parent, this.id, category, name, ts, opt_baseTypeName);
+          this.instances.push(instance);
+        } else {
+          // If the ts is before the last objects deletion time, then the caller
+          // is trying to add a snapshot when there may have been an instance
+          // alive. In that case, try to move an instance's creationTs to
+          // include this ts, provided that it has an implicit creationTs.
+
+          // Search backward from the right for an instance that was definitely
+          // deleted before this ts. Any time an instance is found that has a
+          // moveable creationTs
+          var lastValidIndex;
+          for (var i = this.instances.length - 1; i >= 0; i--) {
+            var tmp = this.instances[i];
+            if (ts >= tmp.deletionTs)
+              break;
+            if (tmp.creationTsWasExplicit == false && tmp.snapshots.length == 0)
+              lastValidIndex = i;
+          }
+          if (lastValidIndex === undefined) {
+            throw new Error(
+                'Cannot add snapshot. No instance was alive that was mutable.');
+          }
+          instance = this.instances[lastValidIndex];
+          instance.creationTs = ts;
+        }
+      } else {
+        instance = this.instances[i];
+      }
+
+      return instance.addSnapshot(ts, args, name, opt_baseTypeName);
+    },
+
+    get lastInstance() {
+      if (this.instances.length == 0)
+        return undefined;
+      return this.instances[this.instances.length - 1];
+    },
+
+    idWasDeleted: function(category, name, ts) {
+      if (this.instances.length == 0) {
+        this.instances.push(this.createObjectInstanceFunction_(
+            this.parent, this.id, category, name, ts));
+      }
+      var lastInstance = this.instances[this.instances.length - 1];
+      if (ts < lastInstance.creationTs)
+        throw new Error('Cannot delete a id before it was crated');
+      if (lastInstance.deletionTs == Number.MAX_VALUE) {
+        lastInstance.wasDeleted(ts);
+        return lastInstance;
+      }
+
+      if (ts < lastInstance.deletionTs)
+        throw new Error('id was already deleted earlier.');
+
+      // A new instance was deleted with no snapshots in-between.
+      // Create an instance then kill it.
+      lastInstance = this.createObjectInstanceFunction_(
+          this.parent, this.id, category, name, ts);
+      this.instances.push(lastInstance);
+      lastInstance.wasDeleted(ts);
+      return lastInstance;
+    },
+
+    getInstanceAt: function(ts) {
+      var i = tv.b.findLowIndexInSortedIntervals(
+          this.instances,
+          function(inst) { return inst.creationTs; },
+          function(inst) { return inst.deletionTs - inst.creationTs; },
+          ts);
+      if (i < 0) {
+        if (this.instances[0].creationTsWasExplicit)
+          return undefined;
+        return this.instances[0];
+      } else if (i >= this.instances.length) {
+        return undefined;
+      }
+      return this.instances[i];
+    },
+
+    logToConsole: function() {
+      for (var i = 0; i < this.instances.length; i++) {
+        var instance = this.instances[i];
+        var cEF = '';
+        var dEF = '';
+        if (instance.creationTsWasExplicit)
+          cEF = '(explicitC)';
+        if (instance.deletionTsWasExplicit)
+          dEF = '(explicit)';
+        console.log(instance.creationTs, cEF,
+                    instance.deletionTs, dEF,
+                    instance.category,
+                    instance.name,
+                    instance.snapshots.length + ' snapshots');
+      }
+    }
+  };
+
+  return {
+    TimeToObjectInstanceMap: TimeToObjectInstanceMap
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/time_to_object_instance_map_test.html b/trace-viewer/trace_viewer/core/trace_model/time_to_object_instance_map_test.html
new file mode 100644
index 0000000..0ab0dbe
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/time_to_object_instance_map_test.html
@@ -0,0 +1,163 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/time_to_object_instance_map.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var createObjectInstance = function(parent, id, category, name, creationTs) {
+    return new tv.c.trace_model.ObjectInstance(
+        parent, id, category, name, creationTs);
+  };
+
+  test('timeToObjectInstanceMap', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    m.addSnapshot('cat', 'name', 10, 'a1');
+    m.addSnapshot('cat', 'name', 20, 'a2');
+    m.idWasDeleted('cat', 'name', 30);
+    m.addSnapshot('cat', 'name', 40, 'b');
+
+    assert.equal(m.instances.length, 2);
+
+    var i0 = m.getInstanceAt(0);
+    var i10 = m.getInstanceAt(10);
+    assert.equal(i0, i10);
+
+    assert.isDefined(i10);
+    assert.equal(i10.snapshots.length, 2);
+    assert.equal(i10.snapshots[0].args, 'a1');
+    assert.equal(i10.snapshots[1].args, 'a2');
+
+    assert.equal(i10.deletionTs, 30);
+
+    var i15 = m.getInstanceAt(15);
+    assert.equal(i15, i10);
+
+    var i20 = m.getInstanceAt(20);
+    assert.equal(i20, i10);
+
+    var i30 = m.getInstanceAt(30);
+    assert.isUndefined(i30);
+
+    var i35 = m.getInstanceAt(35);
+    assert.isUndefined(i35);
+
+    var i40 = m.getInstanceAt(40);
+    assert.isDefined(i40);
+    assert.notEqual(i40, i10);
+    assert.equal(i40.snapshots.length, 1);
+    assert.equal(i40.creationTs, 40);
+    assert.equal(i40.deletionTs, Number.MAX_VALUE);
+
+    var i41 = m.getInstanceAt(41);
+    assert.equal(i40, i41);
+  });
+
+  test('timeToObjectInstanceMapsBoundsLogic', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    m.addSnapshot('cat', 'name', 10, 'a1');
+    m.addSnapshot('cat', 'name', 20, 'a2');
+    m.idWasDeleted('cat', 'name', 30);
+    m.addSnapshot('cat', 'name', 40, 'b');
+    m.addSnapshot('cat', 'name', 41, 'b');
+
+    m.instances.forEach(function(i) { i.updateBounds(); });
+
+    var iA = m.getInstanceAt(10);
+    assert.equal(iA.bounds.min, 10);
+    assert.equal(iA.bounds.max, 30);
+
+    var iB = m.getInstanceAt(40);
+    assert.equal(iB.bounds.min, 40);
+    assert.equal(iB.bounds.max, 41);
+  });
+
+  test('earlySnapshot', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    var i10 = m.idWasCreated('cat', 'name', 10, 'a1');
+    m.idWasDeleted('cat', 'name', 20);
+
+    assert.throws(function() {
+      m.addSnapshot('cat', 'name', 5, 'a1');
+    });
+    assert.equal(i10.creationTs, 10);
+    assert.equal(i10.deletionTs, 20);
+  });
+
+  test('earlySnapshotWithImplicitCreate', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    var i10 = m.idWasDeleted('cat', 'name', 20);
+    m.addSnapshot('cat', 'name', 5, 'a1');
+    assert.equal(i10.creationTs, 5);
+    assert.equal(i10.deletionTs, 20);
+  });
+
+  test('getInstanceBeforeCreationImplicitCreate', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    var i10 = m.idWasCreated('cat', 'name', 10, 'a1');
+    m.idWasDeleted('cat', 'name', 20);
+    assert.isUndefined(m.getInstanceAt(5));
+  });
+
+  test('getInstanceBeforeCreationImplicitCreateWithSnapshot', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    var s5 = m.addSnapshot('cat', 'name', 5, 'a1');
+    var i10 = m.idWasDeleted('cat', 'name', 20);
+    assert.equal(m.getInstanceAt(5), i10);
+  });
+
+  test('successiveDeletions', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    var i20 = m.idWasDeleted('cat', 'name', 20);
+    var i30 = m.idWasDeleted('cat', 'name', 30);
+    var i40 = m.idWasDeleted('cat', 'name', 40);
+    assert.equal(i20.creationTs, 20);
+    assert.isFalse(i20.creationTsWasExplicit);
+    assert.equal(i20.deletionTs, 20);
+    assert.isTrue(i20.deletionTsWasExplicit);
+
+    assert.equal(i30.creationTs, 30);
+    assert.isFalse(i30.creationTsWasExplicit);
+    assert.equal(i30.deletionTs, 30);
+    assert.isTrue(i30.deletionTsWasExplicit);
+
+
+    assert.equal(i40.creationTs, 40);
+    assert.isFalse(i40.creationTsWasExplicit);
+    assert.equal(i40.deletionTs, 40);
+    assert.isTrue(i40.deletionTsWasExplicit);
+  });
+
+  test('snapshotAfterDeletion', function() {
+    var m = new tv.c.trace_model.TimeToObjectInstanceMap(
+        createObjectInstance, {}, 7);
+    var i10 = m.idWasCreated('cat', 'name', 10, 'a1');
+    m.idWasDeleted('cat', 'name', 20);
+
+    var s25 = m.addSnapshot('cat', 'name', 25, 'a1');
+    var i25 = s25.objectInstance;
+
+    assert.equal(i10.creationTs, 10);
+    assert.equal(i10.deletionTs, 20);
+    assert.notEqual(i25, i10);
+    assert.equal(i25.creationTs, 25);
+    assert.equal(i25.deletionTs, Number.MAX_VALUE);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/timed_event.html b/trace-viewer/trace_viewer/core/trace_model/timed_event.html
new file mode 100644
index 0000000..fd8ae2e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/timed_event.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/base/guid.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the TimedEvent class.
+ */
+tv.exportTo('tv.c.trace_model', function() {
+  /**
+   * A TimedEvent is the base type for any piece of data in the trace model with
+   * a specific start and duration.
+   *
+   * @constructor
+   */
+  function TimedEvent(start) {
+    tv.c.trace_model.Event.call(this);
+    this.start = start;
+    this.duration = 0;
+    this.cpuStart = undefined;
+    this.cpuDuration = undefined;
+  }
+
+  TimedEvent.prototype = {
+    __proto__: tv.c.trace_model.Event.prototype,
+
+    get end() {
+      return this.start + this.duration;
+    },
+
+    addBoundsToRange: function(range) {
+      range.addValue(this.start);
+      range.addValue(this.end);
+    },
+
+    bounds: function(that) {
+      // Due to inaccuracy of floating-point calculation, the end times of
+      // slices from a B/E pair (whose end = start + original_end - start)
+      // and an X event (whose end = start + duration) at the same time may
+      // become not equal. Round back to micros (which is the source data
+      // precision) to ensure equality below.
+      var this_end_micros = Math.round(this.end * 1000);
+      var that_end_micros = Math.round(that.end * 1000);
+      return this.start <= that.start && this_end_micros >= that_end_micros;
+    }
+  };
+
+  return {
+    TimedEvent: TimedEvent
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/trace_model.html b/trace-viewer/trace_viewer/core/trace_model/trace_model.html
new file mode 100644
index 0000000..ab4657b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/trace_model.html
@@ -0,0 +1,718 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2012 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/events.html">
+<link rel="import" href="/base/interval_tree.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/base/ui/overlay.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/core/auditor.html">
+<link rel="import" href="/core/importer/empty_importer.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/core/trace_model/kernel.html">
+<link rel="import" href="/core/trace_model/process.html">
+<link rel="import" href="/core/trace_model/sample.html">
+<link rel="import" href="/core/trace_model/stack_frame.html">
+<link rel="import" href="/core/trace_model/instant_event.html">
+<link rel="import" href="/core/trace_model/flow_event.html">
+<link rel="import" href="/core/trace_model/global_memory_dump.html">
+<link rel="import" href="/core/trace_model/process_memory_dump.html">
+<link rel="import" href="/core/trace_model/alert.html">
+<link rel="import" href="/core/trace_model/interaction_record.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview TraceModel is a parsed representation of the
+ * TraceEvents obtained from base/trace_event in which the begin-end
+ * tokens are converted into a hierarchy of processes, threads,
+ * subrows, and slices.
+ *
+ * The building block of the model is a slice. A slice is roughly
+ * equivalent to function call executing on a specific thread. As a
+ * result, slices may have one or more subslices.
+ *
+ * A thread contains one or more subrows of slices. Row 0 corresponds to
+ * the "root" slices, e.g. the topmost slices. Row 1 contains slices that
+ * are nested 1 deep in the stack, and so on. We use these subrows to draw
+ * nesting tasks.
+ *
+ */
+tv.exportTo('tv.c', function() {
+  var Importer = tv.c.importer.Importer;
+  var Process = tv.c.trace_model.Process;
+  var Kernel = tv.c.trace_model.Kernel;
+
+  function ImportOptions() {
+    this.shiftWorldToZero = true;
+    this.pruneEmptyContainers = true;
+
+    // Callback called after
+    // importers run in which more data can be added to the model, before it is
+    // finalized.
+    this.customizeModelCallback = undefined;
+
+    var auditorTypes = tv.c.Auditor.getAllRegisteredTypeInfos();
+    this.auditorConstructors = auditorTypes.map(function(typeInfo) {
+      return typeInfo.constructor;
+    });
+  }
+
+  ImportOptions.fromArguments = function(args, argsStartIndex) {
+    var arg0 = args[argsStartIndex + 0];
+    if (typeof arg0 === 'object') {
+      if (!(arg0 instanceof ImportOptions))
+        throw new Error('Unexpected');
+      return arg0;
+    }
+    var options = new ImportOptions();
+    if (args[argsStartIndex] !== undefined)
+      options.shiftWorldToZero = args[argsStartIndex];
+
+    if (args[argsStartIndex + 1] !== undefined)
+      options.pruneEmptyContainers = args[argsStartIndex + 1];
+
+    if (args[argsStartIndex + 2])
+      options.customizeModelCallback = args[argsStartIndex + 2];
+
+    return options;
+  }
+
+  function ClockSyncRecord(name, ts, args) {
+    this.name = name;
+    this.ts = ts;
+    this.args = args;
+  }
+
+  /**
+   * Builds a model from an array of TraceEvent objects.
+   * @param {Object=} opt_eventData Data from a single trace to be imported into
+   *     the new model. See TraceModel.importTraces for details on how to
+   *     import multiple traces at once.
+   * @param {ImportOptions=} opt_options Options for the import.
+   * @constructor
+   */
+  function TraceModel(opt_eventData, opt_options) {
+    this.faviconHue = 'blue'; // Should be a key from favicons.html
+
+    this.kernel = new Kernel(this);
+    this.processes = {};
+    this.metadata = [];
+    this.categories = [];
+    this.bounds = new tv.b.Range();
+    this.instantEvents = [];
+    this.flowEvents = [];
+    this.clockSyncRecords = [];
+
+    this.stackFrames = {};
+    this.samples = [];
+
+    this.alerts = [];
+    this.interaction_records = [];
+
+    this.flowIntervalTree = new tv.b.IntervalTree(
+        function(f) { return f.start; },
+        function(f) { return f.end; });
+
+    this.globalMemoryDumps = [];
+
+    this.annotationsByGuid_ = {};
+
+    this.importWarnings_ = [];
+    this.reportedImportWarnings_ = {};
+
+    var options = ImportOptions.fromArguments(arguments, 1);
+    if (opt_eventData)
+      this.importTraces([opt_eventData], options);
+  }
+
+  TraceModel.prototype = {
+    __proto__: tv.b.EventTarget.prototype,
+
+    get numProcesses() {
+      var n = 0;
+      for (var p in this.processes)
+        n++;
+      return n;
+    },
+
+    /**
+     * @return {Process} Gets a TimelineProcess for a specified pid. Returns
+     * undefined if the process doesn't exist.
+     */
+    getProcess: function(pid) {
+      return this.processes[pid];
+    },
+
+    /**
+     * @return {Process} Gets a TimelineProcess for a specified pid or
+     * creates one if it does not exist.
+     */
+    getOrCreateProcess: function(pid) {
+      if (!this.processes[pid])
+        this.processes[pid] = new Process(this, pid);
+      return this.processes[pid];
+    },
+
+    pushInstantEvent: function(instantEvent) {
+      this.instantEvents.push(instantEvent);
+    },
+
+    addStackFrame: function(stackFrame) {
+      if (this.stackFrames[stackFrame.id])
+        throw new Error('Stack frame already exists');
+      this.stackFrames[stackFrame.id] = stackFrame;
+      return stackFrame;
+    },
+
+    addInteractionRecord: function(ir1) {
+      this.interaction_records.push(ir1);
+    },
+
+    getClockSyncRecordsNamed: function(name) {
+      return this.clockSyncRecords.filter(function(x) {
+        return x.name === name;
+      });
+    },
+
+    /**
+     * Generates the set of categories from the slices and counters.
+     */
+    updateCategories_: function() {
+      var categoriesDict = {};
+      this.kernel.addCategoriesToDict(categoriesDict);
+      for (var pid in this.processes)
+        this.processes[pid].addCategoriesToDict(categoriesDict);
+
+      this.categories = [];
+      for (var category in categoriesDict)
+        if (category != '')
+          this.categories.push(category);
+    },
+
+    updateBounds: function() {
+      this.bounds.reset();
+
+      this.kernel.updateBounds();
+      this.bounds.addRange(this.kernel.bounds);
+
+      for (var pid in this.processes) {
+        this.processes[pid].updateBounds();
+        this.bounds.addRange(this.processes[pid].bounds);
+      }
+
+      for (var i = 0; i < this.globalMemoryDumps.length; i++)
+        this.globalMemoryDumps[i].addBoundsToRange(this.bounds);
+
+      this.flowEvents.forEach(function(flowEvent) {
+        this.bounds.addValue(flowEvent.start);
+        this.bounds.addValue(flowEvent.end);
+      }, this);
+      this.alerts.forEach(function(alert) {
+        this.bounds.addValue(alert.start);
+        this.bounds.addValue(alert.end);
+      }, this);
+      this.interaction_records.forEach(function(ir) {
+        this.bounds.addValue(ir.start);
+        this.bounds.addValue(ir.end);
+      }, this);
+    },
+
+    shiftWorldToZero: function() {
+      if (this.bounds.isEmpty)
+        return;
+      var timeBase = this.bounds.min;
+      this.kernel.shiftTimestampsForward(-timeBase);
+
+      for (var id in this.instantEvents)
+        this.instantEvents[id].start -= timeBase;
+
+      for (var pid in this.processes)
+        this.processes[pid].shiftTimestampsForward(-timeBase);
+
+      for (var i = 0; i < this.samples.length; i++) {
+        var sample = this.samples[i];
+        sample.start -= timeBase;
+      }
+      this.flowEvents.forEach(function(flowEvent) {
+        flowEvent.start -= timeBase;
+      });
+      this.alerts.forEach(function(alert) {
+        alert.start -= timeBase;
+      });
+      this.interaction_records.forEach(function(ir) {
+        ir.start -= timeBase;
+      });
+
+      for (var i = 0; i < this.globalMemoryDumps.length; i++)
+        this.globalMemoryDumps[i].shiftTimestampsForward(-timeBase);
+
+      this.updateBounds();
+    },
+
+    getAllThreads: function() {
+      var threads = [];
+      for (var tid in this.kernel.threads) {
+        threads.push(process.threads[tid]);
+      }
+      for (var pid in this.processes) {
+        var process = this.processes[pid];
+        for (var tid in process.threads) {
+          threads.push(process.threads[tid]);
+        }
+      }
+      return threads;
+    },
+
+    /**
+     * @return {Array} An array of all processes in the model.
+     */
+    getAllProcesses: function() {
+      var processes = [];
+      for (var pid in this.processes)
+        processes.push(this.processes[pid]);
+      return processes;
+    },
+
+    /**
+     * @return {Array} An array of all the counters in the model.
+     */
+    getAllCounters: function() {
+      var counters = [];
+      counters.push.apply(
+          counters, tv.b.dictionaryValues(this.kernel.counters));
+      for (var pid in this.processes) {
+        var process = this.processes[pid];
+        for (var tid in process.counters) {
+          counters.push(process.counters[tid]);
+        }
+      }
+      return counters;
+    },
+
+    getAnnotationByGUID: function(guid) {
+      return this.annotationsByGuid_[guid];
+    },
+
+    addAnnotation: function(annotation) {
+      if (!annotation.guid)
+        throw new Error('Annotation with undefined guid given');
+
+      this.annotationsByGuid_[annotation.guid] = annotation;
+      tv.b.dispatchSimpleEvent(this, 'annotationChange');
+    },
+
+    removeAnnotation: function(annotation) {
+      delete this.annotationsByGuid_[annotation.guid];
+      tv.b.dispatchSimpleEvent(this, 'annotationChange');
+    },
+
+    getAllAnnotations: function() {
+      return tv.b.dictionaryValues(this.annotationsByGuid_);
+    },
+
+    /**
+     * @param {String} The name of the thread to find.
+     * @return {Array} An array of all the matched threads.
+     */
+    findAllThreadsNamed: function(name) {
+      var namedThreads = [];
+      namedThreads.push.apply(
+          namedThreads,
+          this.kernel.findAllThreadsNamed(name));
+      for (var pid in this.processes) {
+        namedThreads.push.apply(
+            namedThreads,
+            this.processes[pid].findAllThreadsNamed(name));
+      }
+      return namedThreads;
+    },
+
+    createImporter_: function(eventData) {
+      var importerConstructor = tv.c.importer.Importer.findImporterFor(
+          eventData);
+
+      // TODO(kphanee): Throwing same Error at 2 places. Lets try to avoid it!
+      if (!importerConstructor)
+        throw new Error(
+            'Could not find an importer for the provided eventData.');
+
+      var importer = new importerConstructor(
+          this, eventData);
+      return importer;
+    },
+
+    /**
+     * Imports the provided traces into the model. The eventData type
+     * is undefined and will be passed to all the importers registered
+     * via Importer.register. The first importer that returns true
+     * for canImport(events) will be used to import the events.
+     *
+     * The primary trace is provided via the eventData variable. If multiple
+     * traces are to be imported, specify the first one as events, and the
+     * remainder in the opt_additionalEventData array.
+     *
+     * @param {Array} traces An array of eventData to be imported. Each
+     * eventData should correspond to a single trace file and will be handled by
+     * a separate importer.
+     * @param {ImportOptions} options Options for the import, or undefined for
+     * default options.
+     */
+    importTraces: function(traces, opt_options) {
+      var progressMeter = {
+        update: function(msg) {}
+      };
+      var options = ImportOptions.fromArguments(arguments, 1);
+      var task = this.createImportTracesTask(
+          progressMeter,
+          traces,
+          options);
+      tv.b.Task.RunSynchronously(task);
+    },
+
+    /**
+     * Imports a trace with the usual options from importTraces, but
+     * does so using idle callbacks, putting up an import dialog
+     * during the import process.
+     */
+    importTracesWithProgressDialog: function(traces, opt_options) {
+      var options = ImportOptions.fromArguments(arguments, 1);
+
+      var overlay = tv.b.ui.Overlay();
+      overlay.title = 'Importing...';
+      overlay.userCanClose = false;
+      overlay.msgEl = document.createElement('div');
+      overlay.appendChild(overlay.msgEl);
+      overlay.msgEl.style.margin = '20px';
+      overlay.update = function(msg) {
+        this.msgEl.textContent = msg;
+      }
+      overlay.visible = true;
+
+      var task = this.createImportTracesTask(
+          overlay,
+          traces,
+          options);
+      var promise = tv.b.Task.RunWhenIdle(task);
+      promise.then(
+          function() {
+            overlay.visible = false;
+          }, function(err) {
+            overlay.visible = false;
+          });
+      return promise;
+    },
+
+    hasEventDataDecoder_: function(importers) {
+      if (importers.length === 0)
+        return false;
+
+      for (var i = 0; i < importers.length; ++i) {
+        if (!importers[i].isTraceDataContainer())
+          return true;
+      }
+      return false;
+    },
+
+    /**
+     * Creates a task that will import the provided traces into the model,
+     * updating the progressMeter as it goes. Parameters are as defined in
+     * importTraces.
+     */
+    createImportTracesTask: function(progressMeter,
+                                     traces,
+                                     opt_options) {
+      var options = ImportOptions.fromArguments(arguments, 2);
+
+      if (this.importing_)
+        throw new Error('Already importing.');
+      this.importing_ = true;
+
+      // Just some simple setup. It is useful to have a nop first
+      // task so that we can set up the lastTask = lastTask.after()
+      // pattern that follows.
+      var importTask = new tv.b.Task(function() {
+        progressMeter.update('I will now import your traces for you...');
+      }, this);
+      var lastTask = importTask;
+
+      var importers = [];
+
+      lastTask = lastTask.after(function() {
+        // Copy the traces array, we may mutate it.
+        traces = traces.slice(0);
+        progressMeter.update('Creating importers...');
+        // Figure out which importers to use.
+        for (var i = 0; i < traces.length; ++i)
+          importers.push(this.createImporter_(traces[i]));
+
+        // Some traces have other traces inside them. Before doing the full
+        // import, ask the importer if it has any subtraces, and if so, create
+        // importers for them, also.
+        for (var i = 0; i < importers.length; i++) {
+          var subtraces = importers[i].extractSubtraces();
+          for (var j = 0; j < subtraces.length; j++) {
+            try {
+              traces.push(subtraces[j]);
+              importers.push(this.createImporter_(subtraces[j]));
+            } catch (error) {
+              // TODO(kphanee): Log the subtrace file which has failed.
+              console.warn(error.name + ': ' + error.message);
+              continue;
+            }
+          }
+        }
+
+        if (traces.length && !this.hasEventDataDecoder_(importers)) {
+          throw new Error('Could not find an importer for ' +
+                          'the provided eventData.');
+        }
+
+        // Sort them on priority. This ensures importing happens in a
+        // predictable order, e.g. linux_perf_importer before
+        // trace_event_importer.
+        importers.sort(function(x, y) {
+          return x.importPriority - y.importPriority;
+        });
+      }, this);
+
+      // Run the import.
+      lastTask = lastTask.after(function(task) {
+        importers.forEach(function(importer, index) {
+          task.subTask(function() {
+            progressMeter.update(
+                'Importing ' + (index + 1) + ' of ' + importers.length);
+            importer.importEvents();
+          }, this);
+        }, this);
+      }, this);
+
+      // Run the cusomizeModelCallback if needed.
+      if (options.customizeModelCallback) {
+        lastTask = lastTask.after(function(task) {
+          options.customizeModelCallback(this);
+        }, this);
+      }
+
+      // Finalize import.
+      lastTask = lastTask.after(function(task) {
+        importers.forEach(function(importer, index) {
+          progressMeter.update(
+              'Importing sample data ' + (index + 1) + '/' + importers.length);
+          importer.importSampleData();
+        }, this);
+      }, this);
+
+      // Autoclose open slices and create subSlices.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Autoclosing open slices...');
+        // Sort the samples.
+        this.samples.sort(function(x, y) {
+          return x.start - y.start;
+        });
+
+        this.updateBounds();
+        this.kernel.autoCloseOpenSlices(this.bounds.max);
+        for (var pid in this.processes)
+          this.processes[pid].autoCloseOpenSlices(this.bounds.max);
+
+        this.kernel.createSubSlices();
+        for (var pid in this.processes)
+          this.processes[pid].createSubSlices();
+      }, this);
+
+      // Finalize import.
+      lastTask = lastTask.after(function(task) {
+        importers.forEach(function(importer, index) {
+          progressMeter.update(
+              'Finalizing import ' + (index + 1) + '/' + importers.length);
+          importer.finalizeImport();
+        }, this);
+      }, this);
+
+      // Run preinit.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Initializing objects (step 1/2)...');
+        for (var pid in this.processes)
+          this.processes[pid].preInitializeObjects();
+      }, this);
+
+      // Prune empty containers.
+      if (options.pruneEmptyContainers) {
+        lastTask = lastTask.after(function() {
+          progressMeter.update('Pruning empty containers...');
+          this.kernel.pruneEmptyContainers();
+          for (var pid in this.processes) {
+            this.processes[pid].pruneEmptyContainers();
+          }
+        }, this);
+      }
+
+      // Merge kernel and userland slices on each thread.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Merging kernel with userland...');
+        for (var pid in this.processes)
+          this.processes[pid].mergeKernelWithUserland();
+      }, this);
+
+      // Create auditors
+      var auditors = [];
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Adding arbitrary data to model...');
+        auditors = options.auditorConstructors.map(
+          function(auditorConstructor) {
+            return new auditorConstructor(this);
+          }, this);
+        auditors.forEach(function(auditor) {
+          auditor.runAnnotate();
+        });
+      }, this);
+
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Computing final world bounds...');
+        this.updateBounds();
+        this.updateCategories_();
+
+        if (options.shiftWorldToZero)
+          this.shiftWorldToZero();
+      }, this);
+
+      // Build the flow event interval tree.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Building flow event map...');
+        for (var i = 0; i < this.flowEvents.length; ++i) {
+          var flowEvent = this.flowEvents[i];
+          this.flowIntervalTree.insert(flowEvent);
+        }
+        this.flowIntervalTree.updateHighValues();
+      }, this);
+
+      // Join refs.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Joining object refs...');
+        for (var i = 0; i < importers.length; i++)
+          importers[i].joinRefs();
+      }, this);
+
+      // Delete any undeleted objects.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Cleaning up undeleted objects...');
+        for (var pid in this.processes)
+          this.processes[pid].autoDeleteObjects(this.bounds.max);
+      }, this);
+
+      // Sort global and process memory dumps.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Sorting memory dumps...');
+        this.globalMemoryDumps.sort(function(x, y) {
+          return x.start - y.start;
+        });
+        for (var pid in this.processes)
+          this.processes[pid].sortMemoryDumps();
+      }, this);
+
+      // Run initializers.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Initializing objects (step 2/2)...');
+        for (var pid in this.processes)
+          this.processes[pid].initializeObjects();
+      }, this);
+
+      // Run audits.
+      lastTask = lastTask.after(function() {
+        progressMeter.update('Running auditors...');
+        auditors.forEach(function(auditor) {
+          auditor.runAudit();
+        });
+
+        this.interaction_records.sort(function(x, y) {
+          return x.start - y.start;
+        });
+        this.alerts.sort(function(x, y) {
+          return x.start - y.start;
+        });
+
+        this.updateBounds();
+      }, this);
+
+      // Cleanup.
+      lastTask.after(function() {
+        this.importing_ = false;
+      }, this);
+      return importTask;
+    },
+
+    /**
+     * @param {Object} data The import warning data. Data must provide two
+     *    accessors: type, message. The types are used to determine if we
+     *    should output the message, we'll only output one message of each type.
+     *    The message is the actual warning content.
+     */
+    importWarning: function(data) {
+      this.importWarnings_.push(data);
+
+      // Only log each warning type once. We may want to add some kind of
+      // flag to allow reporting all importer warnings.
+      if (this.reportedImportWarnings_[data.type] === true)
+        return;
+
+      console.warn(data.message);
+      this.reportedImportWarnings_[data.type] = true;
+    },
+
+    get hasImportWarnings() {
+      return (this.importWarnings_.length > 0);
+    },
+
+    get importWarnings() {
+      return this.importWarnings_;
+    },
+
+    /**
+     * Iterates all events in the model and calls callback on each event.
+     * @param {function(event)} callback The callback called for every event.
+     */
+    iterateAllEvents: function(callback, opt_this) {
+      this.instantEvents.forEach(callback, opt_this);
+
+      this.kernel.iterateAllEvents(callback, opt_this);
+
+      for (var pid in this.processes)
+        this.processes[pid].iterateAllEvents(callback, opt_this);
+
+      this.samples.forEach(callback, opt_this);
+
+      this.globalMemoryDumps.forEach(callback, opt_this);
+    },
+
+    /**
+     * Some objects in the model can persist their state in TraceModelSettings.
+     *
+     * This iterates through them.
+     */
+    iterateAllPersistableObjects: function(cb) {
+      this.kernel.iterateAllPersistableObjects(cb);
+      for (var pid in this.processes)
+        this.processes[pid].iterateAllPersistableObjects(cb);
+    },
+
+    iterateAllEventContainers: function(cb) {
+      this.kernel.iterateAllEventContainers(cb);
+      for (var pid in this.processes)
+        this.processes[pid].iterateAllEventContainers(cb);
+    }
+  };
+
+  return {
+    ImportOptions: ImportOptions,
+    ClockSyncRecord: ClockSyncRecord,
+    TraceModel: TraceModel
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/trace_model_settings.html b/trace-viewer/trace_viewer/core/trace_model/trace_model_settings.html
new file mode 100644
index 0000000..0c04b73
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/trace_model_settings.html
@@ -0,0 +1,140 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="import" href="/base/settings.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  var Settings = tv.b.Settings;
+
+  /**
+   * A way to persist settings specific to parts of a trace model.
+   *
+   * This object should not be persisted because it builds up internal data
+   * structures that map model objects to settings keys. It should thus be
+   * created for the duration of whatever interaction(s) you're going to do with
+   * model settings, and then discarded.
+   *
+   * This system works on a notion of an object key: for an object's key, it
+   * considers all the other keys in the model. If it is unique, then the key is
+   * persisted to tv.b.Settings. However, if it is not unique, then the
+   * setting is stored on the object itself. Thus, objects with unique keys will
+   * be persisted across page reloads, whereas objects with nonunique keys will
+   * not.
+   */
+  function TraceModelSettings(model) {
+    this.model = model;
+    this.objectsByKey_ = [];
+    this.nonuniqueKeys_ = [];
+    this.buildObjectsByKeyMap_();
+    this.removeNonuniqueKeysFromSettings_();
+    this.ephemeralSettingsByGUID_ = {};
+  }
+
+  TraceModelSettings.prototype = {
+    buildObjectsByKeyMap_: function() {
+      var objects = [];
+      this.model.iterateAllPersistableObjects(function(o) {
+        objects.push(o);
+      });
+
+      var objectsByKey = {};
+      var NONUNIQUE_KEY = 'nonuniqueKey';
+      for (var i = 0; i < objects.length; i++) {
+        var object = objects[i];
+        var objectKey = object.getSettingsKey();
+        if (!objectKey)
+          continue;
+        if (objectsByKey[objectKey] === undefined) {
+          objectsByKey[objectKey] = object;
+          continue;
+        }
+        objectsByKey[objectKey] = NONUNIQUE_KEY;
+      }
+
+      var nonuniqueKeys = {};
+      tv.b.dictionaryKeys(objectsByKey).forEach(function(objectKey) {
+        if (objectsByKey[objectKey] !== NONUNIQUE_KEY)
+          return;
+        delete objectsByKey[objectKey];
+        nonuniqueKeys[objectKey] = true;
+      });
+
+      this.nonuniqueKeys = nonuniqueKeys;
+      this.objectsByKey_ = objectsByKey;
+    },
+
+    removeNonuniqueKeysFromSettings_: function() {
+      var settings = Settings.get('trace_model_settings', {});
+      var settingsChanged = false;
+      tv.b.dictionaryKeys(settings).forEach(function(objectKey) {
+        if (!this.nonuniqueKeys[objectKey])
+          return;
+        settingsChanged = true;
+        delete settings[objectKey];
+      }, this);
+      if (settingsChanged)
+        Settings.set('trace_model_settings', settings);
+    },
+
+    hasUniqueSettingKey: function(object) {
+      var objectKey = object.getSettingsKey();
+      if (!objectKey)
+        return false;
+      return this.objectsByKey_[objectKey] !== undefined;
+    },
+
+    getSettingFor: function(object, objectLevelKey, defaultValue) {
+      var objectKey = object.getSettingsKey();
+      if (!objectKey || !this.objectsByKey_[objectKey]) {
+        var settings = this.getEphemeralSettingsFor_(object);
+        var ephemeralValue = settings[objectLevelKey];
+        if (ephemeralValue !== undefined)
+          return ephemeralValue;
+        return defaultValue;
+      }
+
+      var settings = Settings.get('trace_model_settings', {});
+      if (!settings[objectKey])
+        settings[objectKey] = {};
+      var value = settings[objectKey][objectLevelKey];
+      if (value !== undefined)
+        return value;
+      return defaultValue;
+    },
+
+    setSettingFor: function(object, objectLevelKey, value) {
+      var objectKey = object.getSettingsKey();
+      if (!objectKey || !this.objectsByKey_[objectKey]) {
+        this.getEphemeralSettingsFor_(object)[objectLevelKey] = value;
+        return;
+      }
+
+      var settings = Settings.get('trace_model_settings', {});
+      if (!settings[objectKey])
+        settings[objectKey] = {};
+      if (settings[objectKey][objectLevelKey] === value)
+        return;
+      settings[objectKey][objectLevelKey] = value;
+      Settings.set('trace_model_settings', settings);
+    },
+
+    getEphemeralSettingsFor_: function(object) {
+      if (object.guid === undefined)
+        throw new Error('Only objects with GUIDs can be persisted');
+      if (this.ephemeralSettingsByGUID_[object.guid] === undefined)
+        this.ephemeralSettingsByGUID_[object.guid] = {};
+      return this.ephemeralSettingsByGUID_[object.guid];
+    }
+  };
+
+  return {
+    TraceModelSettings: TraceModelSettings
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/trace_model_settings_test.html b/trace-viewer/trace_viewer/core/trace_model/trace_model_settings_test.html
new file mode 100644
index 0000000..3a9ad96
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/trace_model_settings_test.html
@@ -0,0 +1,178 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/trace_model/trace_model_settings.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('process_name_uniqueness_0', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isFalse(settings.hasUniqueSettingKey(p1));
+  });
+
+  test('process_name_uniqueness_1', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    p1.name = 'Browser';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.hasUniqueSettingKey(p1));
+  });
+
+  test('process_name_uniqueness_2', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var p2 = model.getOrCreateProcess(2);
+    p1.name = 'Renderer';
+    p2.name = 'Renderer';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isFalse(settings.hasUniqueSettingKey(p1));
+    assert.isFalse(settings.hasUniqueSettingKey(p2));
+  });
+
+  test('process_name_uniqueness_3', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var p2 = model.getOrCreateProcess(2);
+    p1.name = 'Renderer';
+    p1.labels.push('Google Search');
+    p2.name = 'Renderer';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.hasUniqueSettingKey(p1));
+    assert.isTrue(settings.hasUniqueSettingKey(p2));
+  });
+
+  test('thread_name_uniqueness_0', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var p2 = model.getOrCreateProcess(2);
+    var t1 = p1.getOrCreateThread(1);
+    var t2 = p2.getOrCreateThread(2);
+    p1.name = 'Browser';
+    p2.name = 'Renderer';
+    t1.name = 'Main';
+    t2.name = 'Main';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.hasUniqueSettingKey(t1));
+    assert.isTrue(settings.hasUniqueSettingKey(t2));
+  });
+
+  test('thread_name_uniqueness_1', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var p2 = model.getOrCreateProcess(2);
+    var t1 = p1.getOrCreateThread(1);
+    var t2 = p2.getOrCreateThread(2);
+    p1.name = 'Renderer';
+    p2.name = 'Renderer';
+    t1.name = 'Main';
+    t2.name = 'Main';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isFalse(settings.hasUniqueSettingKey(t1));
+    assert.isFalse(settings.hasUniqueSettingKey(t2));
+  });
+
+  test('process_persistence_when_not_unique', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true));
+
+    settings.setSettingFor(p1, 'true_by_default', false);
+    assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true));
+
+    // Now, clobber the model, and verify that it didn't persist.
+    model = new tv.c.TraceModel();
+    p1 = model.getOrCreateProcess(1);
+    settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true));
+  });
+
+  test('process_persistence_when_not_unique_with_name', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    p1.name = 'Browser';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.getSettingFor(p1, 'true_by_default', true));
+
+    settings.setSettingFor(p1, 'true_by_default', false);
+    assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true));
+
+    // Now, clobber the model, and verify that it persisted.
+    model = new tv.c.TraceModel();
+    p1 = model.getOrCreateProcess(1);
+    p1.name = 'Browser';
+    settings = new tv.c.TraceModelSettings(model);
+    assert.isFalse(settings.getSettingFor(p1, 'true_by_default', true));
+  });
+
+  test('thread_persistence_when_not_unique', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var p2 = model.getOrCreateProcess(2);
+    var t1 = p1.getOrCreateThread(1);
+    var t2 = p2.getOrCreateThread(2);
+    p1.name = 'Renderer';
+    p2.name = 'Renderer';
+    t1.name = 'Main';
+    t2.name = 'Main';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true));
+
+    settings.setSettingFor(t1, 'true_by_default', false);
+    assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true));
+
+    // Now, clobber the model, and verify that it persisted.
+    model = new tv.c.TraceModel();
+    p1 = model.getOrCreateProcess(1);
+    p2 = model.getOrCreateProcess(2);
+    t1 = p1.getOrCreateThread(1);
+    t2 = p2.getOrCreateThread(2);
+    p1.name = 'Renderer';
+    p2.name = 'Renderer';
+    t1.name = 'Main';
+    t2.name = 'Main';
+    settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true));
+  });
+
+  test('thread_persistence_when_unique', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var p2 = model.getOrCreateProcess(2);
+    var t1 = p1.getOrCreateThread(1);
+    var t2 = p2.getOrCreateThread(2);
+    p1.name = 'Browser';
+    p2.name = 'Renderer';
+    t1.name = 'Main';
+    t2.name = 'Main';
+    var settings = new tv.c.TraceModelSettings(model);
+    assert.isTrue(settings.getSettingFor(t1, 'true_by_default', true));
+
+    settings.setSettingFor(t1, 'true_by_default', false);
+    assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true));
+
+    // Now, clobber the model, and verify that it persisted.
+    model = new tv.c.TraceModel();
+    p1 = model.getOrCreateProcess(1);
+    p2 = model.getOrCreateProcess(2);
+    t1 = p1.getOrCreateThread(1);
+    t2 = p2.getOrCreateThread(2);
+    p1.name = 'Browser';
+    p2.name = 'Renderer';
+    t1.name = 'Main';
+    t2.name = 'Main';
+    settings = new tv.c.TraceModelSettings(model);
+    assert.isFalse(settings.getSettingFor(t1, 'true_by_default', true));
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/trace_model/trace_model_test.html b/trace-viewer/trace_viewer/core/trace_model/trace_model_test.html
new file mode 100644
index 0000000..d3e2981
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/trace_model_test.html
@@ -0,0 +1,394 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/annotation.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/extras/full_config.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ThreadSlice = tv.c.trace_model.ThreadSlice;
+  var TraceModel = tv.c.TraceModel;
+  var TitleOrCategoryFilter = tv.c.TitleOrCategoryFilter;
+  var Frame = tv.c.trace_model.Frame;
+
+  var createTraceModelWithOneOfEverything = function() {
+    var m = new TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+    cpu.slices.push(tv.c.test_utils.newSlice(1, 3));
+
+    var p = m.getOrCreateProcess(1);
+    var t = p.getOrCreateThread(1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+    t.asyncSliceGroup.push(tv.c.test_utils.newAsyncSlice(0, 1, t, t));
+
+    var c = p.getOrCreateCounter('', 'ProcessCounter');
+    var aSeries = new tv.c.trace_model.CounterSeries('a', 0);
+    var bSeries = new tv.c.trace_model.CounterSeries('b', 0);
+    c.addSeries(aSeries);
+    c.addSeries(bSeries);
+
+    aSeries.addCounterSample(0, 5);
+    aSeries.addCounterSample(1, 6);
+    aSeries.addCounterSample(2, 5);
+    aSeries.addCounterSample(3, 7);
+
+    bSeries.addCounterSample(0, 10);
+    bSeries.addCounterSample(1, 15);
+    bSeries.addCounterSample(2, 12);
+    bSeries.addCounterSample(3, 16);
+
+    var c1 = cpu.getOrCreateCounter('', 'CpuCounter');
+    var aSeries = new tv.c.trace_model.CounterSeries('a', 0);
+    var bSeries = new tv.c.trace_model.CounterSeries('b', 0);
+    c1.addSeries(aSeries);
+    c1.addSeries(bSeries);
+
+    aSeries.addCounterSample(0, 5);
+    aSeries.addCounterSample(1, 6);
+    aSeries.addCounterSample(2, 5);
+    aSeries.addCounterSample(3, 7);
+
+    bSeries.addCounterSample(0, 10);
+    bSeries.addCounterSample(1, 15);
+    bSeries.addCounterSample(2, 12);
+    bSeries.addCounterSample(3, 16);
+
+    p.frames.push.apply(p.frames, new Frame([t, 1, 5]));
+
+    var gd = new tv.c.trace_model.GlobalMemoryDump(m, 2);
+    var pd = new tv.c.trace_model.ProcessMemoryDump(gd, p, 2);
+    gd.processMemoryDumps[1] = pd;
+    m.globalMemoryDumps.push(gd);
+    p.memoryDumps.push(pd);
+
+    m.updateBounds();
+
+    return m;
+  };
+
+  test('traceModelBounds_EmptyTraceModel', function() {
+    var m = new TraceModel();
+    m.updateBounds();
+    assert.isUndefined(m.bounds.min);
+    assert.isUndefined(m.bounds.max);
+  });
+
+  test('traceModelBounds_OneEmptyThread', function() {
+    var m = new TraceModel();
+    var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+    m.updateBounds();
+    assert.isUndefined(m.bounds.min);
+    assert.isUndefined(m.bounds.max);
+  });
+
+  test('traceModelBounds_OneThread', function() {
+    var m = new TraceModel();
+    var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+    m.updateBounds();
+    assert.equal(m.bounds.min, 1);
+    assert.equal(m.bounds.max, 4);
+  });
+
+  test('traceModelBounds_OneThreadAndOneEmptyThread', function() {
+    var m = new TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+    var t2 = m.getOrCreateProcess(1).getOrCreateThread(1);
+    m.updateBounds();
+    assert.equal(m.bounds.min, 1);
+    assert.equal(m.bounds.max, 4);
+  });
+
+  test('traceModelBounds_OneCpu', function() {
+    var m = new TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+    cpu.slices.push(tv.c.test_utils.newSlice(1, 3));
+    m.updateBounds();
+    assert.equal(m.bounds.min, 1);
+    assert.equal(m.bounds.max, 4);
+  });
+
+  test('traceModelBounds_OneCpuOneThread', function() {
+    var m = new TraceModel();
+    var cpu = m.kernel.getOrCreateCpu(1);
+    cpu.slices.push(tv.c.test_utils.newSlice(1, 3));
+
+    var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+
+    m.updateBounds();
+    assert.equal(m.bounds.min, 1);
+    assert.equal(m.bounds.max, 5);
+  });
+
+  test('traceModelBounds_GlobalMemoryDumps', function() {
+    var m = new TraceModel();
+    m.globalMemoryDumps.push(new tv.c.trace_model.GlobalMemoryDump(m, 1));
+    m.globalMemoryDumps.push(new tv.c.trace_model.GlobalMemoryDump(m, 3));
+    m.globalMemoryDumps.push(new tv.c.trace_model.GlobalMemoryDump(m, 5));
+
+    m.updateBounds();
+    assert.equal(m.bounds.min, 1);
+    assert.equal(m.bounds.max, 5);
+  });
+
+  test('traceModelBounds_ProcessMemoryDumps', function() {
+    var m = new TraceModel();
+    var p = m.getOrCreateProcess(1);
+    var gd = new tv.c.trace_model.GlobalMemoryDump(m, -1);
+    p.memoryDumps.push(new tv.c.trace_model.ProcessMemoryDump(gd, m, 1));
+    p.memoryDumps.push(new tv.c.trace_model.ProcessMemoryDump(gd, m, 3));
+    p.memoryDumps.push(new tv.c.trace_model.ProcessMemoryDump(gd, m, 5));
+
+    m.updateBounds();
+    assert.equal(m.bounds.min, 1);
+    assert.equal(m.bounds.max, 5);
+  });
+
+  test('traceModelCanImportEmpty', function() {
+    var m;
+    m = new TraceModel([]);
+    m = new TraceModel('');
+  });
+
+  test('traceModelCanImportSubtraces', function() {
+    var systraceLines = [
+      'SurfaceFlinger-2  [001] ...1 1000.0: 0: B|1|taskA',
+      'SurfaceFlinger-2  [001] ...1 2000.0: 0: E',
+      '        chrome-3  [001] ...1 2000.0: 0: trace_event_clock_sync: ' +
+          'parent_ts=0'
+    ];
+    var traceEvents = [
+      {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+        my_object: {id_ref: '0x1000'}
+      }},
+      {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+    ];
+
+    var combined = JSON.stringify({
+      traceEvents: traceEvents,
+      systemTraceEvents: systraceLines.join('\n')
+    });
+
+    var m = new TraceModel();
+    m.importTraces([combined]);
+    assert.equal(tv.b.dictionaryValues(m.processes).length, 1);
+
+    var p1 = m.processes[1];
+    assert.isDefined(p1);
+
+    var t2 = p1.threads[2];
+    var t3 = p1.threads[3];
+    assert.isDefined(t2);
+    assert.isDefined(t3);
+
+    assert.equal(1, 1, t2.sliceGroup.length);
+    assert.equal(t2.sliceGroup.slices[0].title, 'taskA');
+
+    assert.equal(t3.sliceGroup.length, 1);
+    assert.equal(t3.sliceGroup.slices[0].title, 'taskB');
+  });
+
+  test('traceModelCanImportCompressedSingleSubtrace', function() {
+    var compressedTrace = atob('H4sIACKfFVUC/wsuLUpLTE51y8nMS08t0jVSUIg2MDCMV' +
+        'dDT0zNUMDQwMNAzsFIAIqcaw5qSxOJsR65gfDqMEDpcATiC61ZbAAAA');
+    var m = new TraceModel();
+    m.importTraces([compressedTrace]);
+    assert.equal(1, tv.b.dictionaryValues(m.processes).length);
+
+    var p1 = m.processes[1];
+    assert.isDefined(p1);
+
+    var t2 = p1.threads[2];
+    assert.isDefined(t2);
+
+    assert.equal(1, t2.sliceGroup.length, 1);
+    assert.equal('taskA', t2.sliceGroup.slices[0].title);
+  });
+
+  test('traceModelCanImportSubtracesRecursively', function() {
+    var systraceLines = [
+      'SurfaceFlinger-2  [001] ...1 1000.0: 0: B|1|taskA',
+      'SurfaceFlinger-2  [001] ...1 2000.0: 0: E',
+      '        chrome-3  [001] ...1 2000.0: 0: trace_event_clock_sync: ' +
+          'parent_ts=0'
+    ];
+    var outerTraceEvents = [
+      {ts: 1000, pid: 1, tid: 3, ph: 'B', cat: 'c', name: 'taskB', args: {
+        my_object: {id_ref: '0x1000'}
+      }}
+    ];
+
+    var innerTraceEvents = [
+      {ts: 2000, pid: 1, tid: 3, ph: 'E', cat: 'c', name: 'taskB', args: {}}
+    ];
+
+    var innerTrace = JSON.stringify({
+      traceEvents: innerTraceEvents,
+      systemTraceEvents: systraceLines.join('\n')
+    });
+
+    var outerTrace = JSON.stringify({
+      traceEvents: outerTraceEvents,
+      systemTraceEvents: innerTrace
+    });
+
+    var m = new TraceModel();
+    m.importTraces([outerTrace]);
+    assert.equal(tv.b.dictionaryValues(m.processes).length, 1);
+
+    var p1 = m.processes[1];
+    assert.isDefined(p1);
+
+    var t2 = p1.threads[2];
+    var t3 = p1.threads[3];
+    assert.isDefined(t2);
+    assert.isDefined(t3);
+
+    assert.equal(1, 1, t2.sliceGroup.length);
+    assert.equal(t2.sliceGroup.slices[0].title, 'taskA');
+
+    assert.equal(t3.sliceGroup.length, 1);
+    assert.equal(t3.sliceGroup.slices[0].title, 'taskB');
+  });
+
+  test('traceModelWithImportFailure', function() {
+    var malformed = '{traceEvents: [{garbage';
+    var m = new TraceModel();
+    assert.throw(function() {
+      m.importTraces([malformed]);
+    });
+  });
+
+  test('TitleOrCategoryFilter', function() {
+    var s0 = tv.c.test_utils.newSlice(1, 3);
+    assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s0));
+    assert.isFalse(new TitleOrCategoryFilter('x').matchSlice(s0));
+
+    var s1 = tv.c.test_utils.newSliceNamed('ba', 1, 3);
+    assert.isTrue(new TitleOrCategoryFilter('a').matchSlice(s1));
+    assert.isTrue(new TitleOrCategoryFilter('ba').matchSlice(s1));
+    assert.isFalse(new TitleOrCategoryFilter('x').matchSlice(s1));
+  });
+
+  test('traceModel_findAllThreadsNamed', function() {
+    var m = new TraceModel();
+    var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+    t.name = 'CrBrowserMain';
+
+    m.updateBounds();
+    var f = m.findAllThreadsNamed('CrBrowserMain');
+    assert.deepEqual([t], f);
+    f = m.findAllThreadsNamed('NoSuchThread');
+    assert.equal(f.length, 0);
+  });
+
+  test('traceModel_updateCategories', function() {
+    var m = new TraceModel();
+    var t = m.getOrCreateProcess(1).getOrCreateThread(1);
+    t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3));
+    t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3));
+    t.sliceGroup.pushSlice(new ThreadSlice('categoryB', 'a', 0, 1, {}, 3));
+    t.sliceGroup.pushSlice(new ThreadSlice('categoryA', 'a', 0, 1, {}, 3));
+    t.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 3));
+    m.updateCategories_();
+    assert.deepEqual(['categoryA', 'categoryB'], m.categories);
+  });
+
+  test('traceModel_iterateAllEvents', function() {
+    var m = createTraceModelWithOneOfEverything();
+    var wasCalled = false;
+    m.iterateAllEvents(function(event) {
+      assert.isTrue(event instanceof tv.c.trace_model.Event);
+      wasCalled = true;
+    });
+    assert.isTrue(wasCalled);
+  });
+
+  test('customizeCallback', function() {
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+      var browserProcess = m.getOrCreateProcess(1);
+      var browserMain = browserProcess.getOrCreateThread(2);
+      browserMain.sliceGroup.beginSlice('cat', 'Task', 0);
+      browserMain.sliceGroup.beginSlice('cat', 'SubTask', 1);
+      browserMain.sliceGroup.endSlice(9);
+      browserMain.sliceGroup.endSlice(10);
+      browserMain.sliceGroup.beginSlice('cat', 'Task', 20);
+      browserMain.sliceGroup.endSlice(30);
+    });
+    var t2 = m.processes[1].threads[2];
+    assert.equal(t2.sliceGroup.length, 3);
+    assert.equal(t2.sliceGroup.topLevelSlices.length, 2);
+  });
+
+  test('traceModel_sortsSamples', function() {
+    var m = new tv.c.TraceModel();
+    // The 184, 0 and 185 are the tick-times
+    // and irrespective of the order
+    // in which the lines appear in the trace,
+    // the samples should always be sorted by sampling time.
+    m.importTraces(['tick,0x9a,184,0,0x0,5',
+                    'tick,0x9b,0,0,0x0,5',
+                    'tick,0x9c,185,0,0x0,5']);
+    assert.equal(m.samples[0].start, 0);
+    assert.equal(m.samples[1].start, 0.184);
+    assert.equal(m.samples[2].start, 0.185);
+  });
+
+  test('traceModel_sortsGlobalMemoryDumps', function() {
+    var m = new tv.c.TraceModel();
+    m.importTraces([], true /* shiftWorldToZero */, false, function() {
+      m.globalMemoryDumps.push(new tv.c.trace_model.GlobalMemoryDump(m, 1));
+      m.globalMemoryDumps.push(new tv.c.trace_model.GlobalMemoryDump(m, 5));
+      m.globalMemoryDumps.push(new tv.c.trace_model.GlobalMemoryDump(m, 3));
+    });
+    assert.equal(m.globalMemoryDumps[0].start, 0);
+    assert.equal(m.globalMemoryDumps[1].start, 2);
+    assert.equal(m.globalMemoryDumps[2].start, 4);
+  });
+
+  test('traceModel_sortsProcessMemoryDumps', function() {
+    var m = new tv.c.TraceModel();
+    var p = m.getOrCreateProcess(1);
+    m.importTraces([], true /* shiftWorldToZero */, false, function() {
+      var g = new tv.c.trace_model.GlobalMemoryDump(m, -1);
+      p.memoryDumps.push(new tv.c.trace_model.ProcessMemoryDump(g, p, 1));
+      p.memoryDumps.push(new tv.c.trace_model.ProcessMemoryDump(g, p, 5));
+      p.memoryDumps.push(new tv.c.trace_model.ProcessMemoryDump(g, p, 3));
+    });
+    assert.equal(p.memoryDumps[0].start, 0);
+    assert.equal(p.memoryDumps[1].start, 2);
+    assert.equal(p.memoryDumps[2].start, 4);
+  });
+
+  test('traceModel_annotationAddRemove', function() {
+    var m = new tv.c.TraceModel();
+    var a1 = new tv.c.trace_model.Annotation();
+    var a2 = new tv.c.trace_model.Annotation();
+
+    assert.equal(m.getAllAnnotations().length, 0);
+    m.addAnnotation(a1);
+    assert.equal(m.getAllAnnotations().length, 1);
+    m.addAnnotation(a2);
+    assert.equal(m.getAllAnnotations().length, 2);
+
+    assert.equal(m.getAnnotationByGUID(a1.guid), a1);
+    assert.equal(m.getAnnotationByGUID(a2.guid), a2);
+
+    m.removeAnnotation(a1);
+    assert.isUndefined(m.getAnnotationByGUID(a1.guid));
+    assert.equal(m.getAnnotationByGUID(a2.guid), a2);
+    assert.equal(m.getAllAnnotations().length, 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/trace_model/x_marker_annotation.html b/trace-viewer/trace_viewer/core/trace_model/x_marker_annotation.html
new file mode 100644
index 0000000..38b6e7c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/trace_model/x_marker_annotation.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/annotation.html">
+<link rel="import" href="/core/tracks/x_marker_annotation_view.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.trace_model', function() {
+
+  function XMarkerAnnotation(timestamp) {
+    tv.c.trace_model.Annotation.apply(this, arguments);
+
+    this.timestamp_ = timestamp; // Location of top-left corner.
+    this.strokeStyle = 'rgba(0, 0, 255, 0.5)';
+  }
+
+  XMarkerAnnotation.fromDict = function(dict) {
+    return new XMarkerAnnotation(dict.args.timestamp);
+  }
+
+  XMarkerAnnotation.prototype = {
+    __proto__: tv.c.trace_model.Annotation.prototype,
+
+    get timestamp() {
+      return this.timestamp_;
+    },
+
+    toDict: function() {
+      return {
+        typeName: 'xmarker',
+        args: {
+          timestamp: this.timestamp
+        }
+      };
+    },
+
+    createView_: function(viewport) {
+      return new tv.c.annotations.XMarkerAnnotationView(viewport, this);
+    }
+  };
+
+  tv.c.trace_model.Annotation.register(
+      XMarkerAnnotation, {typeName: 'xmarker'});
+
+  return {
+    XMarkerAnnotation: XMarkerAnnotation
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/alert_track.html b/trace-viewer/trace_viewer/core/tracks/alert_track.html
new file mode 100644
index 0000000..910fb42
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/alert_track.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/letter_dot_track.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays an array of alert objects.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var AlertTrack = tv.b.ui.define(
+      'alert-track', tv.c.tracks.LetterDotTrack);
+
+  AlertTrack.prototype = {
+    __proto__: tv.c.tracks.LetterDotTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+      this.heading = 'Alerts';
+      this.alerts_ = undefined;
+    },
+
+    get alerts() {
+      return this.alerts_;
+    },
+
+    set alerts(alerts) {
+      this.alerts_ = alerts;
+      if (alerts === undefined) {
+        this.items = undefined;
+        return;
+      }
+      this.items = this.alerts_.map(function(alert) {
+        return {
+          start: alert.start,
+          get selected() {
+            return this.alert.selected;
+          },
+          colorId: alert.colorId,
+          dotLetter: String.fromCharCode(9888),
+          alert: alert
+        };
+      });
+    },
+
+    getModelEventFromItem: function(item) {
+      return item.alert;
+    }
+  };
+
+  return {
+    AlertTrack: AlertTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/alert_track_test.html b/trace-viewer/trace_viewer/core/tracks/alert_track_test.html
new file mode 100644
index 0000000..fa111cc
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/alert_track_test.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/trace_model/global_memory_dump.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+<link rel="import" href="/core/tracks/alert_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var AlertTrack = tv.c.tracks.AlertTrack;
+  var Selection = tv.c.Selection;
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var Viewport = tv.c.TimelineViewport;
+
+  var ALERT_SEVERITY = tv.c.trace_model.ALERT_SEVERITY;
+  var ALERT_TYPE_1 = new tv.c.trace_model.AlertType(
+    'Alert 1', 'Critical alert', ALERT_SEVERITY.CRITICAL);
+  var ALERT_TYPE_2 = new tv.c.trace_model.AlertType(
+    'Alert 2', 'Warning alert', ALERT_SEVERITY.WARNING);
+
+  var createAlerts = function() {
+    var alerts = [
+      new tv.c.trace_model.Alert(ALERT_TYPE_1, 5),
+      new tv.c.trace_model.Alert(ALERT_TYPE_1, 20),
+      new tv.c.trace_model.Alert(ALERT_TYPE_2, 35),
+      new tv.c.trace_model.Alert(ALERT_TYPE_2, 50)
+    ];
+    return alerts;
+  };
+
+  test('instantiate', function() {
+    var alerts = createAlerts();
+    alerts[1].selectionState = SelectionState.SELECTED;
+
+    var div = document.createElement('div');
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = AlertTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.alerts = alerts;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 50, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+
+
+    assert.equal(5, track.items[0].start);
+  });
+
+  test('modelMapping', function() {
+    var alerts = createAlerts();
+
+    var div = document.createElement('div');
+    var viewport = new Viewport(div);
+    var track = AlertTrack(viewport);
+    track.alerts = alerts;
+
+    var a0 = track.getModelEventFromItem(track.items[0]);
+    assert.equal(a0, alerts[0]);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/annotation_view.html b/trace-viewer/trace_viewer/core/tracks/annotation_view.html
new file mode 100644
index 0000000..4f80568
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/annotation_view.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.annotations', function() {
+  /**
+   * A base class for all annotation views.
+   * @constructor
+   */
+  function AnnotationView(viewport, annotation) {
+  }
+
+  AnnotationView.prototype = {
+    draw: function(ctx) {
+      throw new Error('Not implemented');
+    }
+  };
+
+  return {
+    AnnotationView: AnnotationView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/async_slice_group_track.html b/trace-viewer/trace_viewer/core/tracks/async_slice_group_track.html
new file mode 100644
index 0000000..2b50c5a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/async_slice_group_track.html
@@ -0,0 +1,136 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/multi_row_track.html">
+<link rel="import" href="/core/tracks/slice_track.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays a AsyncSliceGroup.
+   * @constructor
+   * @extends {MultiRowTrack}
+   */
+  var AsyncSliceGroupTrack = tv.b.ui.define(
+      'async-slice-group-track',
+      tv.c.tracks.MultiRowTrack);
+
+  AsyncSliceGroupTrack.prototype = {
+
+    __proto__: tv.c.tracks.MultiRowTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('async-slice-group-track');
+      this.group_ = undefined;
+    },
+
+    addSubTrack_: function(slices) {
+      var track = new tv.c.tracks.SliceTrack(this.viewport);
+      track.slices = slices;
+      this.appendChild(track);
+      track.asyncStyle = true;
+      return track;
+    },
+
+    get group() {
+      return this.group_;
+    },
+
+    set group(group) {
+      this.group_ = group;
+      this.setItemsToGroup(this.group_.slices, this.group_);
+    },
+
+    get eventContainer() {
+      return this.group;
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      containerToTrackMap.addContainer(this.group, this);
+    },
+
+    /**
+     * Breaks up the list of slices into N rows, each of which is a list of
+     * slices that are non overlapping.
+     *
+     * It uses a very simple approach: walk through the slices in sorted order
+     * by start time. For each slice, try to fit it in an existing subRow. If
+     * it doesn't fit in any subrow, make another subRow. It then fits nested
+     * subSlices recursively into rows below parent slice according to which
+     * nested level the child is in.
+     */
+    buildSubRows_: function(slices, opt_skipSort) {
+      if (!opt_skipSort) {
+        slices.sort(function(x, y) {
+          return x.start - y.start;
+        });
+      }
+
+      // Helper function that returns true if it can put the slice on row n.
+      var findLevel = function(sliceToPut, rows, n) {
+        if (n >= rows.length)
+          return true; // We always can make empty rows to put the slice.
+        var subRow = rows[n];
+        var lastSliceInSubRow = subRow[subRow.length - 1];
+        if (sliceToPut.start >= lastSliceInSubRow.end) {
+          if (sliceToPut.subSlices === undefined ||
+              sliceToPut.subSlices.length === 0) {
+            return true;
+          }
+          // Make sure nested sub slices can be fitted in as well.
+          for (var i = 0; i < sliceToPut.subSlices.length; i++) {
+            if (!findLevel(sliceToPut.subSlices[i], rows, n + 1))
+              return false;
+          }
+          return true;
+        }
+        return false;
+      }
+
+      var subRows = [];
+      for (var i = 0; i < slices.length; i++) {
+        var slice = slices[i];
+
+        var found = false;
+        var index = subRows.length;
+        for (var j = 0; j < subRows.length; j++) {
+          if (findLevel(slice, subRows, j)) {
+            found = true;
+            index = j;
+            break;
+          }
+        }
+        if (!found)
+          subRows.push([]);
+        subRows[index].push(slice);
+
+        // Fit subSlices recursively into rows below parent.
+        var fitSubSlicesRecursively = function(subSlices, level, rows) {
+          if (subSlices === undefined || subSlices.length === 0)
+            return;
+          if (level === rows.length)
+            rows.push([]);
+          for (var h = 0; h < subSlices.length; h++) {
+             rows[level].push(subSlices[h]);
+             fitSubSlicesRecursively(subSlices[h].subSlices, level + 1, rows);
+          }
+        }
+        fitSubSlicesRecursively(slice.subSlices, index + 1, subRows);
+      }
+      return subRows;
+    }
+  };
+
+  return {
+    AsyncSliceGroupTrack: AsyncSliceGroupTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/async_slice_group_track_test.html b/trace-viewer/trace_viewer/core/tracks/async_slice_group_track_test.html
new file mode 100644
index 0000000..d71b163
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/async_slice_group_track_test.html
@@ -0,0 +1,252 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var AsyncSliceGroup = tv.c.trace_model.AsyncSliceGroup;
+  var AsyncSliceGroupTrack = tv.c.tracks.AsyncSliceGroupTrack;
+  var Process = tv.c.trace_model.Process;
+  var ProcessTrack = tv.c.tracks.ProcessTrack;
+  var Thread = tv.c.trace_model.Thread;
+  var ThreadTrack = tv.c.tracks.ThreadTrack;
+  var newAsyncSlice = tv.c.test_utils.newAsyncSlice;
+  var newAsyncSliceNamed = tv.c.test_utils.newAsyncSliceNamed;
+
+  test('filterSubRows', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+    g.push(newAsyncSlice(0, 1, t1, t1));
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+
+    assert.equal(track.children.length, 1);
+    assert.isTrue(track.hasVisibleContent);
+  });
+
+  test('rebuildSubRows_twoNonOverlappingSlices', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+    var s1 = newAsyncSlice(0, 1, t1, t1);
+    var subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+    s1.subSlices = [subs1];
+    g.push(s1);
+    g.push(newAsyncSlice(1, 1, t1, t1));
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+    var subRows = track.subRows;
+    assert.equal(subRows.length, 2);
+    assert.equal(subRows[0].length, 2);
+    assert.equal(subRows[1].length, 1);
+    assert.equal(subRows[1][0], g.slices[0].subSlices[0]);
+    assert.isUndefined(g.slices[1].subSlices);
+  });
+
+  test('rebuildSubRows_twoOverlappingSlices', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+
+    var s1 = newAsyncSlice(0, 1, t1, t1);
+    var subs1 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+    s1.subSlices = [subs1];
+    var s2 = newAsyncSlice(0, 1.5, t1, t1);
+    var subs2 = newAsyncSliceNamed('b', 0, 1, t1, t1);
+    s2.subSlices = [subs2];
+    g.push(s1);
+    g.push(s2);
+
+    g.updateBounds();
+
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+
+    var subRows = track.subRows;
+
+    assert.equal(subRows.length, 4);
+    assert.equal(subRows[0].length, 1);
+    assert.equal(subRows[1].length, 1);
+    assert.equal(subRows[2].length, 1);
+    assert.equal(subRows[3].length, 1);
+    assert.equal(subRows[1][0], g.slices[0].subSlices[0]);
+    assert.equal(subRows[3][0], g.slices[1].subSlices[0]);
+  });
+
+  test('rebuildSubRows_threePartlyOverlappingSlices', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+    g.push(newAsyncSlice(0, 1, t1, t1));
+    g.push(newAsyncSlice(0, 1.5, t1, t1));
+    g.push(newAsyncSlice(1, 1.5, t1, t1));
+    g.updateBounds();
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+    var subRows = track.subRows;
+
+    assert.equal(subRows.length, 2);
+    assert.equal(subRows[0].length, 2);
+    assert.equal(subRows[0][0], g.slices[0]);
+    assert.equal(subRows[0][1], g.slices[2]);
+    assert.equal(subRows[1][0], g.slices[1]);
+    assert.equal(subRows[1].length, 1);
+    assert.isUndefined(g.slices[0].subSlices);
+    assert.isUndefined(g.slices[1].subSlices);
+    assert.isUndefined(g.slices[2].subSlices);
+  });
+
+  test('rebuildSubRows_threeOverlappingSlices', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+
+    g.push(newAsyncSlice(0, 1, t1, t1));
+    g.push(newAsyncSlice(0, 1.5, t1, t1));
+    g.push(newAsyncSlice(2, 1, t1, t1));
+    g.updateBounds();
+
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+
+    var subRows = track.subRows;
+    assert.equal(subRows.length, 2);
+    assert.equal(subRows[0].length, 2);
+    assert.equal(subRows[1].length, 1);
+    assert.equal(subRows[0][0], g.slices[0]);
+    assert.equal(subRows[1][0], g.slices[1]);
+    assert.equal(subRows[0][1], g.slices[2]);
+  });
+
+  // Tests that no slices and their sub slices overlap.
+  test('rebuildSubRows_NonOverlappingSubSlices', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+
+    var slice1 = newAsyncSlice(0, 5, t1, t1);
+    var slice1Child = newAsyncSlice(1, 2, t1, t1);
+    slice1.subSlices = [slice1Child];
+    var slice2 = newAsyncSlice(3, 5, t1, t1);
+    var slice3 = newAsyncSlice(5, 4, t1, t1);
+    var slice3Child = newAsyncSlice(6, 2, t1, t1);
+    slice3.subSlices = [slice3Child];
+    g.push(slice1);
+    g.push(slice2);
+    g.push(slice3);
+    g.updateBounds();
+
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+
+    var subRows = track.subRows;
+    // Checks each sub row to see that we don't have any overlapping slices.
+    for (var i = 0; i < subRows.length; i++) {
+      var row = subRows[i];
+      for (var j = 0; j < row.length; j++) {
+        for (var k = j + 1; k < row.length; k++) {
+          assert.isTrue(row[j].end <= row[k].start);
+        }
+      }
+    }
+  });
+
+  test('rebuildSubRows_NonOverlappingSubSlicesThreeNestedLevels', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = new Process(model, 1);
+    var t1 = new Thread(p1, 1);
+    var g = new AsyncSliceGroup(t1);
+
+    var slice1 = newAsyncSlice(0, 4, t1, t1);
+    var slice1Child = newAsyncSlice(1, 2, t1, t1);
+    slice1.subSlices = [slice1Child];
+    var slice2 = newAsyncSlice(2, 7, t1, t1);
+    var slice3 = newAsyncSlice(5, 5, t1, t1);
+    var slice3Child = newAsyncSlice(6, 3, t1, t1);
+    var slice3Child2 = newAsyncSlice(7, 1, t1, t1);
+    slice3.subSlices = [slice3Child];
+    slice3Child.subSlices = [slice3Child2];
+    g.push(slice1);
+    g.push(slice2);
+    g.push(slice3);
+    g.updateBounds();
+
+    var track = new AsyncSliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = g;
+
+    var subRows = track.subRows;
+    // Checks each sub row to see that we don't have any overlapping slices.
+    for (var i = 0; i < subRows.length; i++) {
+      var row = subRows[i];
+      for (var j = 0; j < row.length; j++) {
+        for (var k = j + 1; k < row.length; k++) {
+          assert.isTrue(row[j].end <= row[k].start);
+        }
+      }
+    }
+  });
+
+  test('asyncSliceGroupContainerMap', function() {
+    var vp = new tv.c.TimelineViewport();
+    var containerToTrack = vp.containerToTrackObj;
+    var model = new tv.c.TraceModel();
+    var process = model.getOrCreateProcess(123);
+    var thread = process.getOrCreateThread(456);
+    var group = new AsyncSliceGroup(thread);
+
+    var processTrack = new ProcessTrack(vp);
+    var threadTrack = new ThreadTrack(vp);
+    var groupTrack = new AsyncSliceGroupTrack(vp);
+    processTrack.process = process;
+    threadTrack.thread = thread;
+    groupTrack.group = group;
+    processTrack.appendChild(threadTrack);
+    threadTrack.appendChild(groupTrack);
+
+    assert.equal(processTrack.eventContainer, process);
+    assert.equal(threadTrack.eventContainer, thread);
+    assert.equal(groupTrack.eventContainer, group);
+
+    assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+    assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+    assert.isUndefined(
+        containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'));
+
+    vp.modelTrackContainer = {
+      addContainersToTrackMap: function(containerToTrackObj) {
+        processTrack.addContainersToTrackMap(containerToTrackObj);
+      },
+      addEventListener: function() {}
+    };
+    vp.rebuildContainerToTrackMap();
+
+    // Check that all tracks call childs' addContainersToTrackMap()
+    // by checking the resulting map.
+    assert.equal(containerToTrack.getTrackByStableId('123'), processTrack);
+    assert.equal(containerToTrack.getTrackByStableId('123.456'), threadTrack);
+    assert.equal(containerToTrack.getTrackByStableId('123.456.AsyncSliceGroup'),
+        groupTrack);
+
+    // Check the track's eventContainer getter.
+    assert.equal(processTrack.eventContainer, process);
+    assert.equal(threadTrack.eventContainer, thread);
+    assert.equal(groupTrack.eventContainer, group);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/chart_track.html b/trace-viewer/trace_viewer/core/tracks/chart_track.html
new file mode 100644
index 0000000..aa3566d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/chart_track.html
@@ -0,0 +1,394 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/tracks/heading_track.html">
+<link rel="import" href="/core/event_presenter.html">
+<link rel="import" href="/base/ui.html">
+
+<style>
+.chart-track {
+  height: 30px;
+  position: relative;
+}
+</style>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var EventPresenter = tv.c.EventPresenter;
+  var LAST_SAMPLE_PIXELS = 8;
+
+  var LINE_WIDTH = 1;
+  var BACKGROUND_ALPHA_MULTIPLIER = 0.5;
+  var MIN_HEIGHT = 2;
+  var SQUARE_WIDTH = 3; // Unselected sample point.
+  var CIRCLE_RADIUS = 2; // Selected sample point.
+
+  var POINT_DENSITY_TRANSPARENT = 0.10;
+  var POINT_DENSITY_OPAQUE = 0.05;
+  var POINT_DENSITY_RANGE = POINT_DENSITY_TRANSPARENT - POINT_DENSITY_OPAQUE;
+
+  /**
+   * A track that displays a Counter object.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  // TODO(petrcermak): Rewrite this track to use ChartTrackSeries instead of
+  // Counter objects.
+  var ChartTrack =
+      tv.b.ui.define('chart-track', tv.c.tracks.HeadingTrack);
+
+  ChartTrack.prototype = {
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('chart-track');
+      this.chart_ = null;
+    },
+
+    get chart() {
+      return this.chart_;
+    },
+
+    set chart(chart) {
+      this.chart_ = chart;
+      this.invalidateDrawingContainer();
+    },
+
+    get height() {
+      return window.getComputedStyle(this).height;
+    },
+
+    set height(height) {
+      this.style.height = height;
+      this.invalidateDrawingContainer();
+    },
+
+    get hasVisibleContent() {
+      return !!this.chart_;
+    },
+
+    getModelEventFromItem: function(rect) {
+      throw new Error('Not implemented.');
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      switch (type) {
+        case tv.c.tracks.DrawType.SLICE:
+          this.drawSlices_(viewLWorld, viewRWorld);
+          break;
+      }
+    },
+
+    drawSlices_: function(viewLWorld, viewRWorld) {
+      var highDetails = this.viewport.highDetails;
+
+      var ctx = this.context();
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var bounds = this.getBoundingClientRect();
+      var height = bounds.height * pixelRatio;
+      var range = height - MIN_HEIGHT * pixelRatio;
+
+      var chart = this.chart_;
+
+      // Culling parametrs.
+      var vp = this.viewport;
+      var dt = vp.currentDisplayTransform;
+      var pixWidth = dt.xViewVectorToWorld(1);
+
+      // Drop samples that are less than skipDistancePix apart.
+      var skipDistancePix = 1;
+      var skipDistanceWorld = dt.xViewVectorToWorld(skipDistancePix);
+
+      // Figure out where drawing should begin.
+      var numSeries = chart.numSeries;
+      var numSamples = chart.numSamples;
+      var startIndex = tv.b.findLowIndexInSortedArray(
+          chart.timestamps,
+          function(x) { return x; },
+          viewLWorld);
+      var timestamps = chart.timestamps;
+
+      startIndex = startIndex - 1 > 0 ? startIndex - 1 : 0;
+      // Draw indices one by one until we fall off the viewRWorld.
+      var yScale = range / chart.maxTotal;
+
+      for (var seriesIndex = chart.numSeries - 1;
+           seriesIndex >= 0; seriesIndex--) {
+        var series = chart.series[seriesIndex];
+        ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
+            series.color, SelectionState.NONE);
+
+        // Draw the background and the line.
+        var drawSeries = function(background) {
+          var selectionStateLast = -1;
+
+          // Set i and x such that the first sample we draw is the
+          // startIndex sample.
+          var i = startIndex - 1;
+          var xLast = i >= 0 ?
+              timestamps[i] - skipDistanceWorld : -1;
+          var yLastView = height;
+
+          // Iterate over samples from i onward until we either fall off the
+          // viewRWorld or we run out of samples. To avoid drawing too much,
+          // after drawing a sample at xLast, skip subsequent samples that are
+          // less than skipDistanceWorld from xLast.
+          var hasMoved = false;
+
+          while (true) {
+            i++;
+            if (i >= numSamples) {
+              break;
+            }
+
+            var x = timestamps[i];
+            var y = chart.totals[i * numSeries + seriesIndex];
+            var yView = range - yScale * y;
+
+            // If the sample is to the right of the viewport, we add a fixed
+            // margin to reduce zooming clipping errors.
+            if (x > viewRWorld) {
+              if (hasMoved) {
+                xLast = x = viewRWorld;
+                ctx.lineTo(dt.xWorldToView(x), yLastView);
+              }
+              break;
+            }
+
+            if (i + 1 < numSamples) {
+              var xNext = timestamps[i + 1];
+              if (xNext - xLast <= skipDistanceWorld && xNext < viewRWorld) {
+                continue;
+              }
+
+              // If the sample is to the left of the viewport, we add a fixed
+              // margin to reduce zooming clipping errors.
+              if (x < viewLWorld) {
+                x = viewLWorld;
+              }
+            }
+
+            if (x - xLast < skipDistanceWorld && xLast < x) {
+              // We know that xNext > xLast + skipDistanceWorld, so we can
+              // safely move this sample's x over that much without passing
+              // xNext.  This ensure that the previous sample is visible when
+              // zoomed out very far.
+              x = xLast + skipDistanceWorld;
+            }
+
+            var selectionState = series.samples[i].selectionState;
+
+            if (hasMoved) {
+              ctx.lineTo(dt.xWorldToView(x), yLastView);
+              if (selectionState != selectionStateLast) {
+                if (background) {
+                  ctx.lineTo(dt.xWorldToView(x), height);
+                  ctx.closePath();
+                  ctx.fill();
+                } else {
+                  ctx.lineTo(dt.xWorldToView(x), yView);
+                  ctx.stroke();
+                }
+              }
+            }
+
+            if (selectionState != selectionStateLast) {
+              ctx.fillStyle = EventPresenter.getCounterSeriesColor(
+                  series.color, selectionState, BACKGROUND_ALPHA_MULTIPLIER);
+              ctx.lineWidth = LINE_WIDTH * pixelRatio;
+              ctx.beginPath();
+
+              if (background) {
+                ctx.moveTo(dt.xWorldToView(x), height);
+              } else {
+                ctx.moveTo(dt.xWorldToView(x), hasMoved ? yLastView : yView);
+              }
+            }
+
+            if (background) {
+                ctx.lineTo(dt.xWorldToView(x), yView);
+            } else {
+                ctx.lineTo(dt.xWorldToView(x), yView);
+            }
+
+            hasMoved = true;
+            xLast = x;
+            yLastView = yView;
+            selectionStateLast = selectionState;
+          }
+
+          if (hasMoved) {
+            if (background) {
+              ctx.lineTo(dt.xWorldToView(xLast), height);
+              ctx.closePath();
+              ctx.fill();
+            } else {
+              ctx.stroke();
+            }
+          }
+        }
+
+        drawSeries(true);
+        if (highDetails) {
+          drawSeries(false);
+        }
+
+        // Calculate point density and, consequently, opacity of sample points.
+        var endIndex = tv.b.findLowIndexInSortedArray(
+            chart.timestamps, function(x) { return x; }, viewRWorld);
+        if (chart.timestamps[endIndex] == viewRWorld) {
+          endIndex++;
+        }
+        var minVisible = (startIndex >= chart.timestamps.length ?
+                          viewLWorld : chart.timestamps[startIndex]);
+        var maxVisible = (endIndex < 1 ?
+                          viewRWorld : chart.timestamps[endIndex - 1]);
+        var rangeVisible = (minVisible >= maxVisible ?
+                            viewRWorld - viewLWorld : maxVisible - minVisible);
+
+        var density = (endIndex - startIndex) / (dt.scaleX * rangeVisible);
+        var clampedDensity = tv.b.clamp(density, POINT_DENSITY_OPAQUE,
+                                      POINT_DENSITY_TRANSPARENT);
+        var opacity =
+            (POINT_DENSITY_TRANSPARENT - clampedDensity) / POINT_DENSITY_RANGE;
+
+        // Draw sample points.
+        ctx.strokeStyle = EventPresenter.getCounterSeriesColor(
+            series.color, SelectionState.NONE);
+        var lastFillStyle = undefined;
+        for (var i = startIndex; timestamps[i] < viewRWorld; i++) {
+          var x = timestamps[i];
+          var y = chart.totals[i * numSeries + seriesIndex];
+          var yView = range - yScale * y;
+
+          if (series.samples[i].selected) {
+            var fillStyle = EventPresenter.getCounterSeriesColor(
+              series.color, series.samples[i].selectionState);
+            if (fillStyle !== lastFillStyle) {
+              ctx.fillStyle = lastFillStyle = fillStyle;
+            }
+            ctx.beginPath();
+            ctx.arc(dt.xWorldToView(x), yView, CIRCLE_RADIUS * pixelRatio, 0,
+                    2 * Math.PI);
+            ctx.fill();
+            ctx.stroke();
+          } else if (highDetails) {
+            var fillStyle = EventPresenter.getCounterSeriesColor(
+                series.color, series.samples[i].selectionState, opacity);
+            if (fillStyle !== lastFillStyle) {
+              ctx.fillStyle = lastFillStyle = fillStyle;
+            }
+            ctx.fillRect(dt.xWorldToView(x) - (SQUARE_WIDTH / 2) * pixelRatio,
+                         yView - (SQUARE_WIDTH / 2) * pixelRatio,
+                         SQUARE_WIDTH * pixelRatio, SQUARE_WIDTH * pixelRatio);
+          }
+        }
+      }
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      var allSeries = this.chart_.series;
+      for (var seriesIndex = 0; seriesIndex < allSeries.length; seriesIndex++) {
+        var samples = allSeries[seriesIndex].samples;
+        for (var i = 0; i < samples.length; i++)
+          eventToTrackMap.addEvent(samples[i], this);
+      }
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+
+      function getSampleWidth(x, i) {
+        if (i === chart.timestamps.length - 1) {
+          var dt = this.viewport.currentDisplayTransform;
+          var pixWidth = dt.xViewVectorToWorld(1);
+          return LAST_SAMPLE_PIXELS * pixWidth;
+        }
+        return chart.timestamps[i + 1] - chart.timestamps[i];
+      }
+
+      var chart = this.chart_;
+      var iLo = tv.b.findLowIndexInSortedIntervals(chart.timestamps,
+                                                   function(x) { return x; },
+                                                   getSampleWidth.bind(this),
+                                                   loWX);
+      var iHi = tv.b.findLowIndexInSortedIntervals(chart.timestamps,
+                                                   function(x) { return x; },
+                                                   getSampleWidth.bind(this),
+                                                   hiWX);
+
+      // Iterate over every sample intersecting..
+      for (var sampleIndex = iLo; sampleIndex <= iHi; sampleIndex++) {
+        if (sampleIndex < 0)
+          continue;
+        if (sampleIndex >= chart.timestamps.length)
+          continue;
+
+        // TODO(nduca): Pick the seriesIndexHit based on the loY - hiY values.
+        for (var seriesIndex = 0;
+             seriesIndex < this.chart.numSeries;
+             seriesIndex++) {
+          var series = this.chart.series[seriesIndex];
+          this.addValueToSelection(series.samples[sampleIndex], selection);
+        }
+      }
+    },
+
+    addItemNearToProvidedEventToSelection: function(sample, offset, selection) {
+      var index = sample.getSampleIndex();
+      var newIndex = index + offset;
+      if (newIndex < 0 || newIndex >= sample.series.samples.length)
+        return false;
+
+      this.addValueToSelection(sample.series.samples[newIndex], selection);
+      return true;
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      var chart = this.chart;
+      if (!chart.numSeries)
+        return;
+
+      var stackHeight = 0;
+
+      for (var i = 0; i < chart.numSeries; i++) {
+        var chartSample = tv.b.findClosestElementInSortedArray(
+            chart.series_[i].samples_,
+            function(x) { return x.timestamp; },
+            worldX,
+            worldMaxDist);
+
+        if (!chartSample)
+          continue;
+
+        this.addValueToSelection(chartSample, selection);
+      }
+    },
+
+    addValueToSelection: function(chartValue, selection) {
+      var event = this.getModelEventFromItem(chartValue);
+      if (event)
+        selection.push(event);
+    }
+  };
+
+  return {
+    ChartTrack: ChartTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/container_track.html b/trace-viewer/trace_viewer/core/tracks/container_track.html
new file mode 100644
index 0000000..8c6e0b0
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/container_track.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/task.html">
+<link rel="import" href="/core/tracks/track.html">
+<link rel="import" href="/core/filter.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var Task = tv.b.Task;
+
+  /**
+   * A generic track that contains other tracks as its children.
+   * @constructor
+   */
+  var ContainerTrack = tv.b.ui.define('container-track', tv.c.tracks.Track);
+  ContainerTrack.prototype = {
+    __proto__: tv.c.tracks.Track.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.Track.prototype.decorate.call(this, viewport);
+    },
+
+    detach: function() {
+      this.textContent = '';
+    },
+
+    get tracks_() {
+      var tracks = [];
+      for (var i = 0; i < this.children.length; i++) {
+        if (this.children[i].classList.contains('track'))
+          tracks.push(this.children[i]);
+      }
+      return tracks;
+    },
+
+    drawTrack: function(type) {
+      for (var i = 0; i < this.children.length; ++i) {
+        if (!(this.children[i] instanceof tv.c.tracks.Track))
+          continue;
+        this.children[i].drawTrack(type);
+      }
+    },
+
+    /**
+     * Adds items intersecting the given range to a selection.
+     * @param {number} loVX Lower X bound of the interval to search, in
+     *     viewspace.
+     * @param {number} hiVX Upper X bound of the interval to search, in
+     *     viewspace.
+     * @param {number} loY Lower Y bound of the interval to search, in
+     *     viewspace space.
+     * @param {number} hiY Upper Y bound of the interval to search, in
+     *     viewspace space.
+     * @param {Selection} selection Selection to which to add results.
+     */
+    addIntersectingItemsInRangeToSelection: function(
+        loVX, hiVX, loY, hiY, selection) {
+      for (var i = 0; i < this.tracks_.length; i++) {
+        var trackClientRect = this.tracks_[i].getBoundingClientRect();
+        var a = Math.max(loY, trackClientRect.top);
+        var b = Math.min(hiY, trackClientRect.bottom);
+        if (a <= b)
+          this.tracks_[i].addIntersectingItemsInRangeToSelection(
+              loVX, hiVX, loY, hiY, selection);
+      }
+
+      tv.c.tracks.Track.prototype.addIntersectingItemsInRangeToSelection.
+          apply(this, arguments);
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      for (var i = 0; i < this.children.length; ++i)
+        this.children[i].addEventsToTrackMap(eventToTrackMap);
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+      for (var i = 0; i < this.tracks_.length; i++)
+        this.tracks_[i].addAllObjectsMatchingFilterToSelection(
+            filter, selection);
+    },
+
+    addAllObjectsMatchingFilterToSelectionAsTask: function(filter, selection) {
+      var task = new Task();
+      for (var i = 0; i < this.tracks_.length; i++) {
+        task.subTask(function(i) { return function() {
+          this.tracks_[i].addAllObjectsMatchingFilterToSelection(
+              filter, selection);
+        } }(i), this);
+      }
+      return task;
+    },
+
+    addClosestEventToSelection: function(
+        worldX, worldMaxDist, loY, hiY, selection) {
+      for (var i = 0; i < this.tracks_.length; i++) {
+        var trackClientRect = this.tracks_[i].getBoundingClientRect();
+        var a = Math.max(loY, trackClientRect.top);
+        var b = Math.min(hiY, trackClientRect.bottom);
+        if (a <= b) {
+          this.tracks_[i].addClosestEventToSelection(
+              worldX, worldMaxDist, loY, hiY, selection);
+        }
+      }
+
+      tv.c.tracks.Track.prototype.addClosestEventToSelection.
+          apply(this, arguments);
+    }
+  };
+
+  return {
+    ContainerTrack: ContainerTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/counter_track.html b/trace-viewer/trace_viewer/core/tracks/counter_track.html
new file mode 100644
index 0000000..3dc670b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/counter_track.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/chart_track.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  /**
+   * A track that displays a Counter object.
+   * @constructor
+   * @extends {ChartTrack}
+   */
+  var CounterTrack =
+      tv.b.ui.define('counter-track', tv.c.tracks.ChartTrack);
+
+  CounterTrack.prototype = {
+    __proto__: tv.c.tracks.ChartTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ChartTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('counter-track');
+    },
+
+    get counter() {
+      return this.chart;
+    },
+
+    set counter(counter) {
+      this.heading = counter.name + ': ';
+      this.chart = buildChartFromCounter(counter);
+    },
+
+    getModelEventFromItem: function(chartValue) {
+      return chartValue;
+    }
+  };
+
+  var buildChartFromCounter = function(counter) {
+    // TODO(petrcermak): Build ChartTrackSeries object(s) instead of using the
+    // Counter object directly.
+    return counter;
+  };
+
+  return {
+    CounterTrack: CounterTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/counter_track_perf_test.html b/trace-viewer/trace_viewer/core/tracks/counter_track_perf_test.html
new file mode 100644
index 0000000..f56115b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/counter_track_perf_test.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/trace_viewer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/extras/full_config.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function getSynchronous(url) {
+    var req = new XMLHttpRequest();
+    req.open('GET', url, false);
+    // Without the mime type specified like this, the file's bytes are not
+    // retrieved correctly.
+    req.overrideMimeType('text/plain; charset=x-user-defined');
+    req.send(null);
+    return req.responseText;
+  }
+
+  var ZOOM_STEPS = 10;
+  var ZOOM_COEFFICIENT = 1.2;
+
+  var model = undefined;
+
+  var drawingContainer;
+  var viewportDiv;
+
+  var viewportWidth;
+  var worldMid;
+
+  var startScale = undefined;
+
+  function timedCounterTrackPerfTest(name, testFn, iterations) {
+
+    function setUpOnce() {
+      if (model !== undefined)
+        return;
+      var fileUrl = '/test_data/counter_tracks.html';
+      var events = getSynchronous(fileUrl);
+      model = new tv.c.TraceModel();
+      model.importTraces([events], true);
+    }
+
+    function setUp() {
+      setUpOnce();
+      viewportDiv = document.createElement('div');
+
+      var viewport = new tv.c.TimelineViewport(viewportDiv);
+
+      drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+      viewport.modelTrackContainer = drawingContainer;
+
+      var modelTrack = new tv.c.tracks.TraceModelTrack(viewport);
+      drawingContainer.appendChild(modelTrack);
+
+      modelTrack.model = model;
+
+      viewportDiv.appendChild(drawingContainer);
+
+      this.addHTMLOutput(viewportDiv);
+
+      // Size the canvas.
+      drawingContainer.updateCanvasSizeIfNeeded_();
+
+      // Size the viewport.
+      viewportWidth = drawingContainer.canvas.width;
+      var min = model.bounds.min;
+      var range = model.bounds.range;
+      worldMid = min + range / 2;
+
+      var boost = range * 0.15;
+      var dt = new tv.c.TimelineDisplayTransform();
+      dt.xSetWorldBounds(min - boost, min + range + boost, viewportWidth);
+      modelTrack.viewport.setDisplayTransformImmediately(dt);
+      startScale = dt.scaleX;
+
+      // Select half of the counter samples.
+      for (var pid in model.processes) {
+        var counters = model.processes[pid].counters;
+        for (var cid in counters) {
+          var series = counters[cid].series;
+          for (var i = 0; i < series.length; i++) {
+            var samples = series[i].samples;
+            for (var j = Math.floor(samples.length / 2); j < samples.length;
+                 j++) {
+              samples[j].selectionState =
+                  tv.c.trace_model.SelectionState.SELECTED;
+            }
+          }
+        }
+      }
+    };
+
+    function tearDown() {
+      viewportDiv.innerText = '';
+      drawingContainer = undefined;
+    }
+
+    timedPerfTest(name, testFn, {
+      setUp: setUp,
+      tearDown: tearDown,
+      iterations: iterations
+    });
+  }
+
+  var n110100 = [1, 10, 100];
+  n110100.forEach(function(val) {
+    timedCounterTrackPerfTest(
+        'draw_softwareCanvas_' + val,
+        function() {
+          var scale = startScale;
+          for (var i = 0; i < ZOOM_STEPS; i++) {
+            var dt = drawingContainer.viewport.currentDisplayTransform.clone();
+            scale *= ZOOM_COEFFICIENT;
+            dt.scaleX = scale;
+            dt.xPanWorldPosToViewPos(worldMid, 'center', viewportWidth);
+            drawingContainer.viewport.setDisplayTransformImmediately(dt);
+            drawingContainer.draw_();
+          }
+        }, val);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/counter_track_test.html b/trace-viewer/trace_viewer/core/tracks/counter_track_test.html
new file mode 100644
index 0000000..8e4b194
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/counter_track_test.html
@@ -0,0 +1,187 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Counter = tv.c.trace_model.Counter;
+  var Viewport = tv.c.TimelineViewport;
+  var CounterTrack = tv.c.tracks.CounterTrack;
+
+  var runTest = function(timestamps, samples, testFn) {
+    var testEl = document.createElement('div');
+
+    var ctr = new Counter(undefined, 'foo', '', 'foo');
+    var n = samples.length;
+
+    for (var i = 0; i < n; ++i) {
+      ctr.addSeries(new tv.c.trace_model.CounterSeries('value' + i,
+          tv.b.ui.getColorIdForGeneralPurposeString('value' + i)));
+    }
+
+    for (var i = 0; i < samples.length; ++i) {
+      for (var k = 0; k < timestamps.length; ++k) {
+        ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+      }
+    }
+
+    ctr.updateBounds();
+
+    var viewport = new Viewport(testEl);
+
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    testEl.appendChild(drawingContainer);
+
+    var track = new CounterTrack(viewport);
+    drawingContainer.appendChild(track);
+    this.addHTMLOutput(testEl);
+
+    // Force the container to update sizes so the test can use coordinates that
+    // make sense. This has to be after the adding of the track as we need to
+    // use the track header to figure out our positioning.
+    drawingContainer.updateCanvasSizeIfNeeded_();
+
+    var pixelRatio = window.devicePixelRatio || 1;
+
+    track.heading = ctr.name;
+    track.counter = ctr;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 10, track.clientWidth * pixelRatio);
+    track.viewport.setDisplayTransformImmediately(dt);
+
+    testFn(ctr, drawingContainer, track);
+  };
+
+  test('instantiate', function() {
+    var ctr = new Counter(undefined, 'testBasicCounter', '',
+        'testBasicCounter');
+    ctr.addSeries(new tv.c.trace_model.CounterSeries('value1',
+        tv.b.ui.getColorIdForGeneralPurposeString('testBasicCounter.value1')));
+    ctr.addSeries(new tv.c.trace_model.CounterSeries('value2',
+        tv.b.ui.getColorIdForGeneralPurposeString('testBasicCounter.value2')));
+
+    var timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+    var samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+                   [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+    for (var i = 0; i < samples.length; ++i) {
+      for (var k = 0; k < timestamps.length; ++k) {
+        ctr.series[i].addCounterSample(timestamps[k], samples[i][k]);
+      }
+    }
+
+    ctr.updateBounds();
+
+    var div = document.createElement('div');
+    var viewport = new Viewport(div);
+
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = new CounterTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.heading = ctr.name;
+    track.counter = ctr;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 7.7, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+  test('basicCounterXPointPicking', function() {
+    var timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+    var samples = [[0, 3, 1, 2, 3, 1, 3, 3.1],
+                   [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+    runTest.call(this, timestamps, samples, function(ctr, container, track) {
+      var clientRect = track.getBoundingClientRect();
+      var y75 = clientRect.top + (0.75 * clientRect.height);
+
+      // In bounds.
+      var sel = new tv.c.Selection();
+      var x = 0.15 * clientRect.width;
+      track.addIntersectingItemsInRangeToSelection(x, x + 1, y75, y75 + 1, sel);
+
+      assert.equal(sel.length, 2);
+      assert.equal(sel[0].series.counter, ctr);
+      assert.equal(sel[0].getSampleIndex(), 1);
+      assert.equal(sel[0].series.seriesIndex, 0);
+
+      assert.equal(sel[1].series.counter, ctr);
+      assert.equal(sel[1].getSampleIndex(), 1);
+      assert.equal(sel[1].series.seriesIndex, 1);
+
+      // Outside bounds.
+      sel = new tv.c.Selection();
+      var x = -0.5 * clientRect.width;
+      track.addIntersectingItemsInRangeToSelection(x, x + 1, y75, y75 + 1, sel);
+      assert.equal(sel.length, 0);
+
+      sel = new tv.c.Selection();
+      var x = 0.8 * clientRect.width;
+      track.addIntersectingItemsInRangeToSelection(x, x + 1, y75, y75 + 1, sel);
+      assert.equal(sel.length, 0);
+    });
+  });
+
+  test('counterTrackAddClosestEventToSelection', function() {
+    var timestamps = [0, 1, 2, 3, 4, 5, 6, 7];
+    var samples = [[0, 4, 1, 2, 3, 1, 3, 3.1],
+                   [5, 3, 1, 1.1, 0, 7, 0, 0.5]];
+
+    runTest.call(this, timestamps, samples, function(ctr, container, track) {
+      // Before with not range.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(-1, 0, 0, 0, sel);
+      assert.equal(sel.length, 0);
+
+      // Before with negative range.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(-1, -10, 0, 0, sel);
+      assert.equal(sel.length, 0);
+
+      // Before first sample.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(-1, 1, 0, 0, sel);
+      assert.equal(sel.length, 2);
+      assert.equal(sel[0].getSampleIndex(), 0);
+
+      // Between and closer to sample before.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+      assert.equal(sel[0].getSampleIndex(), 1);
+
+      // Between samples with bad range.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(1.45, 0.25, 0, 0, sel);
+      assert.equal(sel.length, 0);
+
+      // Between and closer to next sample.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(4.7, 6, 0, 0, sel);
+      assert.equal(sel[0].getSampleIndex(), 5);
+
+      // After last sample with good range.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(8.5, 2, 0, 0, sel);
+      assert.equal(sel[0].getSampleIndex(), 7);
+
+      // After last sample with bad range.
+      var sel = new tv.c.Selection();
+      track.addClosestEventToSelection(10, 1, 0, 0, sel);
+      assert.equal(sel.length, 0);
+    });
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/cpu_track.html b/trace-viewer/trace_viewer/core/tracks/cpu_track.html
new file mode 100644
index 0000000..414ec5c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/cpu_track.html
@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/container_track.html">
+<link rel="import" href="/core/tracks/slice_track.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  /**
+   * Visualizes a Cpu using a series of of SliceTracks.
+   * @constructor
+   */
+  var CpuTrack =
+      tv.b.ui.define('cpu-track', tv.c.tracks.ContainerTrack);
+  CpuTrack.prototype = {
+    __proto__: tv.c.tracks.ContainerTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('cpu-track');
+    },
+
+    get cpu() {
+      return this.cpu_;
+    },
+
+    set cpu(cpu) {
+      this.cpu_ = cpu;
+      this.updateContents_();
+    },
+
+    get tooltip() {
+      return this.tooltip_;
+    },
+
+    set tooltip(value) {
+      this.tooltip_ = value;
+      this.updateContents_();
+    },
+
+    get hasVisibleContent() {
+      return this.children.length > 0;
+    },
+
+    updateContents_: function() {
+      this.detach();
+      if (!this.cpu_)
+        return;
+      var slices = this.cpu_.slices;
+      if (slices.length) {
+        var track = new tv.c.tracks.SliceTrack(this.viewport);
+        track.slices = slices;
+        track.heading = this.cpu_.userFriendlyName + ':';
+        this.appendChild(track);
+      }
+
+      this.appendSamplesTracks_();
+
+      for (var counterName in this.cpu_.counters) {
+        var counter = this.cpu_.counters[counterName];
+        track = new tv.c.tracks.CounterTrack(this.viewport);
+        track.heading = this.cpu_.userFriendlyName + ' ' +
+            counter.name + ':';
+        track.counter = counter;
+        this.appendChild(track);
+      }
+    },
+
+    appendSamplesTracks_: function() {
+      var samples = this.cpu_.samples;
+      if (samples === undefined || samples.length === 0)
+        return;
+      var samplesByTitle = {};
+      samples.forEach(function(sample) {
+        if (samplesByTitle[sample.title] === undefined)
+          samplesByTitle[sample.title] = [];
+        samplesByTitle[sample.title].push(sample);
+      });
+
+      var sampleTitles = tv.b.dictionaryKeys(samplesByTitle);
+      sampleTitles.sort();
+
+      sampleTitles.forEach(function(sampleTitle) {
+        var samples = samplesByTitle[sampleTitle];
+        var samplesTrack = new tv.c.tracks.SliceTrack(this.viewport);
+        samplesTrack.group = this.cpu_;
+        samplesTrack.slices = samples;
+        samplesTrack.heading = this.cpu_.userFriendlyName + ': ' +
+            sampleTitle;
+        samplesTrack.tooltip = this.cpu_.userFriendlyDetails;
+        samplesTrack.selectionGenerator = function() {
+          var selection = new tv.c.Selection();
+          for (var i = 0; i < samplesTrack.slices.length; i++) {
+            selection.push(samplesTrack.slices[i]);
+          }
+          return selection;
+        };
+        this.appendChild(samplesTrack);
+      }, this);
+    }
+  };
+
+  return {
+    CpuTrack: CpuTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/cpu_track_test.html b/trace-viewer/trace_viewer/core/tracks/cpu_track_test.html
new file mode 100644
index 0000000..8ddf336
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/cpu_track_test.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Cpu = tv.c.trace_model.Cpu;
+  var CpuTrack = tv.c.tracks.CpuTrack;
+  var Slice = tv.c.trace_model.Slice;
+  var StackFrame = tv.c.trace_model.StackFrame;
+  var Sample = tv.c.trace_model.Sample;
+  var Thread = tv.c.trace_model.Thread;
+  var Viewport = tv.c.TimelineViewport;
+
+  test('basicCpu', function() {
+    var cpu = new Cpu({}, 7);
+    cpu.slices = [
+      new Slice('', 'a', 0, 1, {}, 1),
+      new Slice('', 'b', 1, 2.1, {}, 4.8)
+    ];
+    cpu.updateBounds();
+
+    var testEl = document.createElement('div');
+    var viewport = new Viewport(testEl);
+
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+
+    var track = new CpuTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    track.heading = 'CPU ' + cpu.cpuNumber;
+    track.cpu = cpu;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+
+  test('withSamples', function() {
+    var model = new tv.c.TraceModel();
+    var thread;
+    var cpu;
+    model.importTraces([], false, false, function() {
+      cpu = model.kernel.getOrCreateCpu(1);
+      thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+      var fA = model.addStackFrame(new StackFrame(
+          undefined, 1, 'cat', 'a', 7));
+      var fAB = model.addStackFrame(new StackFrame(
+          fA, 2, 'cat', 'b', 7));
+      var fABC = model.addStackFrame(new StackFrame(
+          fAB, 3, 'cat', 'c', 7));
+      var fAD = model.addStackFrame(new StackFrame(
+          fA, 4, 'cat', 'd', 7));
+
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    10, fABC, 10));
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    20, fAB, 10));
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    30, fAB, 10));
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    40, fAD, 10));
+
+      model.samples.push(new Sample(undefined, thread, 'page_fault',
+                                    25, fAB, 10));
+      model.samples.push(new Sample(undefined, thread, 'page_fault',
+                                    35, fAD, 10));
+    });
+
+    var testEl = document.createElement('div');
+    var viewport = new Viewport(testEl);
+
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+
+    var track = new CpuTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    track.heading = 'CPU ' + cpu.cpuNumber;
+    track.cpu = cpu;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 11.1, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/drawing_container.css b/trace-viewer/trace_viewer/core/tracks/drawing_container.css
new file mode 100644
index 0000000..defd254
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/drawing_container.css
@@ -0,0 +1,19 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.drawing-container {
+  -webkit-box-flex: 1;
+  display: inline;
+  overflow: auto;
+  position: relative;
+}
+
+.drawing-container-canvas {
+  -webkit-box-flex: 1;
+  display: block;
+  pointer-events: none;
+  position: absolute;
+  top: 0;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/drawing_container.html b/trace-viewer/trace_viewer/core/tracks/drawing_container.html
new file mode 100644
index 0000000..ea9ec3e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/drawing_container.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/drawing_container.css">
+
+<link rel="import" href="/core/tracks/track.html">
+<link rel="import" href="/base/raf.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var DrawType = {
+    SLICE: 1,
+    INSTANT_EVENT: 2,
+    BACKGROUND: 3,
+    GRID: 4,
+    FLOW_ARROWS: 5,
+    MARKERS: 6,
+    HIGHLIGHTS: 7,
+    ANNOTATIONS: 8
+  };
+
+  var DrawingContainer = tv.b.ui.define('drawing-container',
+                                        tv.c.tracks.Track);
+
+  DrawingContainer.prototype = {
+    __proto__: tv.c.tracks.Track.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.Track.prototype.decorate.call(this, viewport);
+      this.classList.add('drawing-container');
+
+      this.canvas_ = document.createElement('canvas');
+      this.canvas_.className = 'drawing-container-canvas';
+      this.canvas_.style.left = tv.c.constants.HEADING_WIDTH + 'px';
+      this.appendChild(this.canvas_);
+
+      this.ctx_ = this.canvas_.getContext('2d');
+
+      this.viewportChange_ = this.viewportChange_.bind(this);
+      this.viewport.addEventListener('change', this.viewportChange_);
+    },
+
+    // Needed to support the calls in TimelineTrackView.
+    get canvas() {
+      return this.canvas_;
+    },
+
+    context: function() {
+      return this.ctx_;
+    },
+
+    viewportChange_: function() {
+      this.invalidate();
+    },
+
+    invalidate: function() {
+      if (this.rafPending_)
+        return;
+      this.rafPending_ = true;
+
+      tv.b.requestPreAnimationFrame(this.preDraw_, this);
+    },
+
+    preDraw_: function() {
+      this.rafPending_ = false;
+      this.updateCanvasSizeIfNeeded_();
+
+      tv.b.requestAnimationFrameInThisFrameIfPossible(this.draw_, this);
+    },
+
+    draw_: function() {
+      this.ctx_.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+
+      var typesToDraw = [
+        DrawType.BACKGROUND,
+        DrawType.HIGHLIGHTS,
+        DrawType.GRID,
+        DrawType.INSTANT_EVENT,
+        DrawType.SLICE,
+        DrawType.MARKERS,
+        DrawType.ANNOTATIONS
+      ];
+
+      if (this.viewport.showFlowEvents)
+        typesToDraw.push(DrawType.FLOW_ARROWS);
+
+      for (var idx in typesToDraw) {
+        for (var i = 0; i < this.children.length; ++i) {
+          if (!(this.children[i] instanceof tv.c.tracks.Track))
+            continue;
+          this.children[i].drawTrack(typesToDraw[idx]);
+        }
+      }
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var bounds = this.canvas_.getBoundingClientRect();
+      var dt = this.viewport.currentDisplayTransform;
+      var viewLWorld = dt.xViewToWorld(0);
+      var viewRWorld = dt.xViewToWorld(
+          bounds.width * pixelRatio);
+
+      this.viewport.drawGridLines(this.ctx_, viewLWorld, viewRWorld);
+    },
+
+    updateCanvasSizeIfNeeded_: function() {
+      var visibleChildTracks =
+          tv.b.asArray(this.children).filter(this.visibleFilter_);
+
+      var thisBounds = this.getBoundingClientRect();
+
+      var firstChildTrackBounds = visibleChildTracks[0].getBoundingClientRect();
+      var lastChildTrackBounds =
+          visibleChildTracks[visibleChildTracks.length - 1].
+              getBoundingClientRect();
+
+      var innerWidth = firstChildTrackBounds.width -
+          tv.c.constants.HEADING_WIDTH;
+      var innerHeight = lastChildTrackBounds.bottom - firstChildTrackBounds.top;
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      if (this.canvas_.width != innerWidth * pixelRatio) {
+        this.canvas_.width = innerWidth * pixelRatio;
+        this.canvas_.style.width = innerWidth + 'px';
+      }
+
+      if (this.canvas_.height != innerHeight * pixelRatio) {
+        this.canvas_.height = innerHeight * pixelRatio;
+        this.canvas_.style.height = innerHeight + 'px';
+      }
+    },
+
+    visibleFilter_: function(element) {
+      if (!(element instanceof tv.c.tracks.Track))
+        return false;
+      return window.getComputedStyle(element).display !== 'none';
+    },
+
+    addClosestEventToSelection: function(
+        worldX, worldMaxDist, loY, hiY, selection) {
+      for (var i = 0; i < this.children.length; ++i) {
+        if (!(this.children[i] instanceof tv.c.tracks.Track))
+          continue;
+        var trackClientRect = this.children[i].getBoundingClientRect();
+        var a = Math.max(loY, trackClientRect.top);
+        var b = Math.min(hiY, trackClientRect.bottom);
+        if (a <= b) {
+          this.children[i].addClosestEventToSelection(
+              worldX, worldMaxDist, loY, hiY, selection);
+        }
+      }
+
+      tv.c.tracks.Track.prototype.addClosestEventToSelection.
+          apply(this, arguments);
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      for (var i = 0; i < this.children.length; ++i) {
+        if (!(this.children[i] instanceof tv.c.tracks.Track))
+          continue;
+        this.children[i].addEventsToTrackMap(eventToTrackMap);
+      }
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      for (var i = 0; i < this.children.length; ++i) {
+        if (!(this.children[i] instanceof tv.c.tracks.Track))
+          continue;
+        this.children[i].addContainersToTrackMap(containerToTrackMap);
+      }
+    }
+  };
+
+  return {
+    DrawingContainer: DrawingContainer,
+    DrawType: DrawType
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/drawing_container_perf_test.html b/trace-viewer/trace_viewer/core/tracks/drawing_container_perf_test.html
new file mode 100644
index 0000000..8163a2b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/drawing_container_perf_test.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/extras/full_config.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {  // @suppress longLineCheck
+  function getSynchronous(url) {
+    var req = new XMLHttpRequest();
+    req.open('GET', url, false);
+    // Without the mime type specified like this, the file's bytes are not
+    // retrieved correctly.
+    req.overrideMimeType('text/plain; charset=x-user-defined');
+    req.send(null);
+    return req.responseText;
+  }
+
+  var model = undefined;
+
+  var drawingContainer;
+  var viewportDiv;
+
+  function timedDrawingContainerPerfTest(name, testFn, iterations) {
+
+    function setUpOnce() {
+      if (model !== undefined)
+        return;
+      var fileUrl = '/test_data/thread_time_visualisation.json.gz';
+      var events = getSynchronous(fileUrl);
+      model = new tv.c.TraceModel();
+      model.importTraces([events], true);
+    }
+
+    function setUp() {
+      setUpOnce();
+      viewportDiv = document.createElement('div');
+
+      var viewport = new tv.c.TimelineViewport(viewportDiv);
+
+      drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+      viewport.modelTrackContainer = drawingContainer;
+
+      var modelTrack = new tv.c.tracks.TraceModelTrack(viewport);
+      drawingContainer.appendChild(modelTrack);
+
+      modelTrack.model = model;
+
+      viewportDiv.appendChild(drawingContainer);
+
+      this.addHTMLOutput(viewportDiv);
+
+      // Size the canvas.
+      drawingContainer.updateCanvasSizeIfNeeded_();
+
+      // Size the viewport.
+      var w = drawingContainer.canvas.width;
+      var min = model.bounds.min;
+      var range = model.bounds.range;
+
+      var boost = range * 0.15;
+      var dt = new tv.c.TimelineDisplayTransform();
+      dt.xSetWorldBounds(min - boost, min + range + boost, w);
+      modelTrack.viewport.setDisplayTransformImmediately(dt);
+    };
+
+    function tearDown() {
+      viewportDiv.innerText = '';
+      drawingContainer = undefined;
+    }
+
+    timedPerfTest(name, testFn, {
+      setUp: setUp,
+      tearDown: tearDown,
+      iterations: iterations
+    });
+  }
+
+  var n110100 = [1, 10, 100];
+  n110100.forEach(function(val) {
+    timedDrawingContainerPerfTest(
+        'draw_softwareCanvas_' + val,
+        function() {
+          drawingContainer.draw_();
+        }, val);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/heading_track.css b/trace-viewer/trace_viewer/core/tracks/heading_track.css
new file mode 100644
index 0000000..d3b70e2
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/heading_track.css
@@ -0,0 +1,35 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.heading-track {
+  -webkit-box-align: stretch;
+  -webkit-box-orient: horizontal;
+  display: -webkit-box;
+  margin: 0;
+  padding: 0 5px 0 0;
+}
+
+.heading-track > heading {
+  -webkit-box-sizing: border-box;
+  background-color: rgb(243, 245, 247);
+  border-right: 1px solid #8e8e8e;
+  box-sizing: border-box;
+  display: -webkit-flex;
+  -webkit-flex-direction: row;
+  align-items: center;
+  overflow-x: hidden;
+  padding-right: 5px;
+  text-align: left;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.heading-track > heading > .heading-arrow {
+  -webkit-flex: 0 0 auto;
+  margin-left: 5px;
+  margin-right: 5px;
+  width: 8px;
+  font-family: sans-serif;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/heading_track.html b/trace-viewer/trace_viewer/core/tracks/heading_track.html
new file mode 100644
index 0000000..15a88c4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/heading_track.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/heading_track.css">
+
+<link rel="import" href="/core/analysis/analysis_link.html">
+<link rel="import" href="/core/constants.html">
+<link rel="import" href="/core/tracks/track.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var DOWN_ARROW = String.fromCharCode(0x25BE);
+  var RIGHT_ARROW = String.fromCharCode(0x25B8);
+
+  /**
+   * A track with a header. Provides the basic heading and tooltip
+   * infrastructure. Subclasses must implement drawing code.
+   * @constructor
+   * @extends {HTMLDivElement}
+   */
+  var HeadingTrack = tv.b.ui.define('heading-track', tv.c.tracks.Track);
+
+  HeadingTrack.prototype = {
+    __proto__: tv.c.tracks.Track.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.Track.prototype.decorate.call(this, viewport);
+      this.classList.add('heading-track');
+
+      this.headingDiv_ = document.createElement('heading');
+      this.headingDiv_.style.width = tv.c.constants.HEADING_WIDTH + 'px';
+      this.headingDiv_.addEventListener(
+          'click', this.onHeadingDivClicked_.bind(this));
+      this.heading_ = '';
+      this.expanded_ = undefined;
+      this.selectionGenerator_ = undefined;
+      this.updateContents_();
+    },
+
+    get heading() {
+      return this.heading_;
+    },
+
+    set heading(text) {
+      this.heading_ = text;
+      this.updateContents_();
+    },
+
+    set tooltip(text) {
+      this.headingDiv_.title = text;
+    },
+
+    set selectionGenerator(generator) {
+      this.selectionGenerator_ = generator;
+      this.updateContents_();
+    },
+
+    get expanded() {
+      return this.expanded_;
+    },
+
+    set expanded(expanded) {
+      expanded = expanded;
+      if (this.expanded_ == expanded)
+        return;
+      this.expanded_ = expanded;
+      this.expandedStateChanged_();
+    },
+
+    expandedStateChanged_: function() {
+      this.updateHeadigDiv_();
+    },
+
+    onHeadingDivClicked_: function() {
+      var e = new Event('heading-clicked', true, false);
+      this.dispatchEvent(e);
+    },
+
+    updateContents_: function() {
+      this.updateHeadigDiv_();
+    },
+
+    updateHeadigDiv_: function() {
+      /**
+       * If this is a heading track of a sampling thread, we add a link to
+       * the heading text ("Sampling Thread"). We associate a selection
+       * generator with the link so that sampling profiling results are
+       * displayed in the bottom frame when you click the link.
+       */
+      this.headingDiv_.innerHTML = '';
+      var span = document.createElement('span');
+      span.classList.add('heading-arrow');
+      if (this.expanded === true)
+        span.textContent = DOWN_ARROW;
+      else if (this.expanded === false)
+        span.textContent = RIGHT_ARROW;
+      else
+        span.textContent = '';
+      this.headingDiv_.appendChild(span);
+
+      if (this.selectionGenerator_) {
+        this.headingLink_ = document.createElement('tv-c-analysis-link');
+        this.headingLink_.selection = this.selectionGenerator_;
+        this.headingLink_.textContent = '';
+        this.headingDiv_.appendChild(this.headingLink_);
+        this.headingLink_.appendChild(document.createTextNode(this.heading_));
+      } else {
+        span = document.createElement('span');
+        span.textContent = this.heading_;
+        this.headingDiv_.appendChild(span);
+      }
+      this.appendChild(this.headingDiv_);
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      throw new Error('draw implementation missing');
+    }
+  };
+
+  return {
+    HeadingTrack: HeadingTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/highlighter.html b/trace-viewer/trace_viewer/core/tracks/highlighter.html
new file mode 100644
index 0000000..50d8eee
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/highlighter.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2014 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/extension_registry.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Allows custom highlighting to be added to the full model track.
+ */
+tv.exportTo('tv.c.tracks', function() {
+
+  /**
+   * Highlights cetrain features of the model.
+   * @constructor
+   */
+  function Highlighter(viewport) {
+    if (viewport === undefined) {
+      throw new Error('viewport must be provided');
+    }
+    this.viewport_ = viewport;
+  };
+
+  Highlighter.prototype = {
+    __proto__: Object.prototype,
+
+    processModel: function(model) {
+      throw new Error('processModel implementation missing');
+    },
+
+    drawHighlight: function(ctx, dt, viewLWorld, viewRWorld, viewHeight) {
+      throw new Error('drawHighlight implementation missing');
+    }
+  };
+
+
+  var options = new tv.b.ExtensionRegistryOptions(tv.b.BASIC_REGISTRY_MODE);
+  options.defaultMetadata = {};
+  options.mandatoryBaseClass = Highlighter;
+  tv.b.decorateExtensionRegistry(Highlighter, options);
+
+  return {
+    Highlighter: Highlighter
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/kernel_track.html b/trace-viewer/trace_viewer/core/tracks/kernel_track.html
new file mode 100644
index 0000000..07d435a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/kernel_track.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/process_track_base.html">
+<link rel="import" href="/core/tracks/cpu_track.html">
+<link rel="import" href="/core/tracks/spacing_track.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var Cpu = tv.c.trace_model.Cpu;
+  var CpuTrack = tv.c.tracks.cpu_track;
+  var ProcessTrackBase = tv.c.tracks.ProcessTrackBase;
+  var SpacingTrack = tv.c.tracks.SpacingTrack;
+
+  /**
+   * @constructor
+   */
+  var KernelTrack = tv.b.ui.define('kernel-track', ProcessTrackBase);
+
+  KernelTrack.prototype = {
+    __proto__: ProcessTrackBase.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ProcessTrackBase.prototype.decorate.call(this, viewport);
+    },
+
+
+    // Kernel maps to processBase because we derive from ProcessTrackBase.
+    set kernel(kernel) {
+      this.processBase = kernel;
+    },
+
+    get kernel() {
+      return this.processBase;
+    },
+
+    get eventContainer() {
+      return this.kernel;
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      containerToTrackMap.addContainer(this.kernel, this);
+    },
+
+    willAppendTracks_: function() {
+      var cpus = tv.b.dictionaryValues(this.kernel.cpus);
+      cpus.sort(tv.c.trace_model.Cpu.compare);
+
+      var didAppendAtLeastOneTrack = false;
+      for (var i = 0; i < cpus.length; ++i) {
+        var cpu = cpus[i];
+        var track = new tv.c.tracks.CpuTrack(this.viewport);
+        track.cpu = cpu;
+        if (!track.hasVisibleContent)
+          continue;
+        this.appendChild(track);
+        didAppendAtLeastOneTrack = true;
+      }
+      if (didAppendAtLeastOneTrack)
+        this.appendChild(new SpacingTrack(this.viewport));
+    }
+  };
+
+
+  return {
+    KernelTrack: KernelTrack
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/letter_dot_track.html b/trace-viewer/trace_viewer/core/tracks/letter_dot_track.html
new file mode 100644
index 0000000..775d010
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/letter_dot_track.html
@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/heading_track.html">
+<link rel="import" href="/base/ui/color_scheme.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+<style>
+.letter-dot-track {
+  height: 18px;
+}
+</style>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var SelectionState = tv.c.trace_model.SelectionState;
+
+  /**
+   * A track that displays an array of dots with filled letters inside them.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var LetterDotTrack = tv.b.ui.define(
+      'letter-dot-track', tv.c.tracks.HeadingTrack);
+
+  LetterDotTrack.prototype = {
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('letter-dot-track');
+      this.items_ = undefined;
+    },
+
+    getModelEventFromItem: function(item) {
+      throw new Error('Not implemented.');
+    },
+
+    get items() {
+      return this.items_;
+    },
+
+    set items(items) {
+      this.items_ = items;
+      this.invalidateDrawingContainer();
+    },
+
+    get height() {
+      return window.getComputedStyle(this).height;
+    },
+
+    set height(height) {
+      this.style.height = height;
+    },
+
+    get dumpRadiusView() {
+      return 7 * (window.devicePixelRatio || 1);
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      if (this.items_ === undefined)
+        return;
+      switch (type) {
+        case tv.c.tracks.DrawType.SLICE:
+          this.drawSlices_(viewLWorld, viewRWorld);
+          break;
+      }
+    },
+
+    drawSlices_: function(viewLWorld, viewRWorld) {
+      var ctx = this.context();
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var bounds = this.getBoundingClientRect();
+      var height = bounds.height * pixelRatio;
+      var halfHeight = height * 0.5;
+      var twoPi = Math.PI * 2;
+      var palette = tv.b.ui.getColorPalette();
+      var highlightIdBoost = tv.b.ui.paletteProperties.highlightIdBoost;
+
+      // Culling parameters.
+      var dt = this.viewport.currentDisplayTransform;
+      var dumpRadiusView = this.dumpRadiusView;
+      var itemRadiusWorld = dt.xViewVectorToWorld(height);
+
+      // Draw the memory dumps.
+      var items = this.items_;
+      var loI = tv.b.findLowIndexInSortedArray(
+          items,
+          function(item) { return item.start; },
+          viewLWorld);
+
+      var oldFont = ctx.font;
+      ctx.font = '400 ' + Math.floor(9 * pixelRatio) + 'px Arial';
+      ctx.strokeStyle = 'rgb(0,0,0)';
+      ctx.textBaseline = 'middle';
+      ctx.textAlign = 'center';
+
+      var drawItems = function(selected) {
+        for (var i = loI; i < items.length; ++i) {
+          var item = items[i];
+          var x = item.start;
+          if (x - itemRadiusWorld > viewRWorld)
+            break;
+          if (item.selected !== selected)
+            continue;
+          var xView = dt.xWorldToView(x);
+
+          if (item.selected)
+            ctx.fillStyle = palette[item.colorId + highlightIdBoost];
+          else
+            ctx.fillStyle = palette[item.colorId];
+          ctx.beginPath();
+          ctx.arc(xView, halfHeight, dumpRadiusView + 0.5, 0, twoPi);
+          ctx.fill();
+          if (item.selected) {
+            ctx.lineWidth = 3;
+            ctx.strokeStyle = 'rgb(100,100,0)';
+            ctx.stroke();
+
+            ctx.beginPath();
+            ctx.arc(xView, halfHeight, dumpRadiusView, 0, twoPi);
+            ctx.lineWidth = 1.5;
+            ctx.strokeStyle = 'rgb(255,255,0)';
+            ctx.stroke();
+          } else {
+            ctx.lineWidth = 1;
+            ctx.strokeStyle = 'rgb(0,0,0)';
+            ctx.stroke();
+          }
+
+          ctx.fillStyle = 'rgb(255, 255, 255)';
+          ctx.fillText(item.dotLetter, xView, halfHeight);
+        }
+      };
+
+      // Draw unselected items first to make sure they don't occlude selected
+      // items.
+      drawItems(false);
+      drawItems(true);
+
+      ctx.lineWidth = 1;
+      ctx.font = oldFont;
+
+      // For performance reasons we only check the SelectionState of the first
+      // memory dump. If it's DIMMED we assume that all are DIMMED.
+      // TODO(petrcermak): Allow partial highlight.
+      var selectionState = SelectionState.NONE;
+      if (items.length &&
+          items[0].selectionState === SelectionState.DIMMED) {
+        selectionState = SelectionState.DIMMED;
+      }
+
+      // Dim the track when there is an active highlight.
+      if (selectionState === SelectionState.DIMMED) {
+        var width = bounds.width * pixelRatio;
+        ctx.fillStyle = 'rgba(255,255,255,0.5)';
+        ctx.fillRect(0, 0, width, height);
+      }
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      if (this.items_ === undefined)
+        return;
+
+      this.items_.forEach(function(item) {
+        var event = this.getModelEventFromItem(item);
+        if (event)
+          eventToTrackMap.addEvent(event, this);
+      }, this);
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+      if (this.items_ === undefined)
+        return;
+
+      var itemRadiusWorld = viewPixWidthWorld * this.dumpRadiusView;
+      tv.b.iterateOverIntersectingIntervals(
+          this.items_,
+          function(x) { return x.start - itemRadiusWorld; },
+          function(x) { return 2 * itemRadiusWorld; },
+          loWX, hiWX,
+          function(item) {
+            var event = this.getModelEventFromItem(item);
+            if (event)
+              selection.push(event);
+          }.bind(this));
+    },
+
+    /**
+     * Add the item to the left or right of the provided event, if any, to the
+     * selection.
+     * @param {event} The current event item.
+     * @param {Number} offset Number of slices away from the event to look.
+     * @param {Selection} selection The selection to add an event to,
+     * if found.
+     * @return {boolean} Whether an event was found.
+     * @private
+     */
+    addItemNearToProvidedEventToSelection: function(event, offset, selection) {
+      if (this.items_ === undefined)
+        return;
+
+      var items = this.items_;
+      var index = items.indexOf(event);
+      var newIndex = index + offset;
+      if (newIndex >= 0 && newIndex < items.length) {
+        var event = this.getModelEventFromItem(items[newIndex]);
+        if (event)
+          selection.push(event);
+        return true;
+      }
+      return false;
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      if (this.items_ === undefined)
+        return;
+
+      var item = tv.b.findClosestElementInSortedArray(
+          this.items_,
+          function(x) { return x.start; },
+          worldX,
+          worldMaxDist);
+
+      if (!item)
+        return;
+
+      var event = this.getModelEventFromItem(item);
+      if (event)
+        selection.push(event);
+    }
+  };
+
+  return {
+    LetterDotTrack: LetterDotTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/letter_dot_track_test.html b/trace-viewer/trace_viewer/core/tracks/letter_dot_track_test.html
new file mode 100644
index 0000000..e79974d
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/letter_dot_track_test.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+<link rel="import" href="/core/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var LetterDotTrack = tv.c.tracks.LetterDotTrack;
+  var Selection = tv.c.Selection;
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var Viewport = tv.c.TimelineViewport;
+
+  var createItems = function() {
+    var items = [
+      {start: 5, colorId: 7, dotLetter: 'a', selected: true},
+      {start: 20, colorId: 2, dotLetter: 'b', selected: true},
+      {start: 35, colorId: 4, dotLetter: 'c', selected: false},
+      {start: 50, colorId: 4, dotLetter: 'd', selected: false}
+    ];
+    return items;
+  };
+
+  test('instantiate', function() {
+    var items = createItems();
+    items[1].selectionState = SelectionState.SELECTED;
+
+    var div = document.createElement('div');
+
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = LetterDotTrack(viewport);
+    track.getModelEventFromItem = function(item) { return item; }
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.items = items;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 50, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+  test('selectionHitTesting', function() {
+    var items = createItems();
+
+    var track = new LetterDotTrack(new Viewport());
+    track.getModelEventFromItem = function(item) { return item; }
+    track.items = items;
+
+    // Fake a view pixel size.
+    var devicePixelRatio = window.devicePixelRatio || 1;
+    var viewPixWidthWorld = 0.1 / devicePixelRatio;
+
+    // Hit outside range
+    var selection = [];
+    track.addIntersectingItemsInRangeToSelectionInWorldSpace(
+        3, 4, viewPixWidthWorld, selection);
+    assert.equal(selection.length, 0);
+
+    // Hit the first item, via pixel-nearness.
+    selection = [];
+    track.addIntersectingItemsInRangeToSelectionInWorldSpace(
+        19.98, 19.99, viewPixWidthWorld, selection);
+    assert.equal(selection.length, 1);
+    assert.equal(selection[0], items[1]);
+
+    // Hit the instance, between the 1st and 2nd snapshots
+    selection = [];
+    track.addIntersectingItemsInRangeToSelectionInWorldSpace(
+        30, 50, viewPixWidthWorld, selection);
+    assert.equal(selection.length, 2);
+    assert.equal(selection[0], items[2]);
+    assert.equal(selection[1], items[3]);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/memory_dump_track.html b/trace-viewer/trace_viewer/core/tracks/memory_dump_track.html
new file mode 100644
index 0000000..5b3c9ea
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/memory_dump_track.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/letter_dot_track.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  /**
+   * A track that displays an array of memoryDump objects.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var MemoryDumpTrack = tv.b.ui.define(
+      'memory-dump-track', tv.c.tracks.LetterDotTrack);
+
+  MemoryDumpTrack.prototype = {
+    __proto__: tv.c.tracks.LetterDotTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.LetterDotTrack.prototype.decorate.call(this, viewport);
+      this.heading = 'Memory dumps';
+      this.memoryDumps_ = undefined;
+    },
+
+    get memoryDumps() {
+      return this.memoryDumps_;
+    },
+
+    set memoryDumps(memoryDumps) {
+      this.memoryDumps_ = memoryDumps;
+      if (memoryDumps === undefined) {
+        this.items = undefined;
+        return;
+      }
+      var memoryColorId = tv.b.ui.getColorIdForReservedName('memory_dump');
+      this.items = this.memoryDumps_.map(function(memoryDump) {
+        return {
+          start: memoryDump.start,
+          get selected() {
+            return this.memoryDump.selected;
+          },
+          colorId: memoryColorId,
+          dotLetter: 'M',
+          memoryDump: memoryDump
+        };
+      });
+    },
+
+    getModelEventFromItem: function(item) {
+      return item.memoryDump;
+    }
+  };
+
+  return {
+    MemoryDumpTrack: MemoryDumpTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/memory_dump_track_test.html b/trace-viewer/trace_viewer/core/tracks/memory_dump_track_test.html
new file mode 100644
index 0000000..3267d82
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/memory_dump_track_test.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/trace_model/global_memory_dump.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+<link rel="import" href="/core/tracks/memory_dump_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var MemoryDumpTrack = tv.c.tracks.MemoryDumpTrack;
+  var Selection = tv.c.Selection;
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var Viewport = tv.c.TimelineViewport;
+
+  var createDumps = function() {
+    var m = new tv.c.TraceModel();
+    var dumps = [
+      new tv.c.trace_model.GlobalMemoryDump(m, 5),
+      new tv.c.trace_model.GlobalMemoryDump(m, 20),
+      new tv.c.trace_model.GlobalMemoryDump(m, 35),
+      new tv.c.trace_model.GlobalMemoryDump(m, 50)
+    ];
+    return dumps;
+  };
+
+  test('instantiate', function() {
+    var dumps = createDumps();
+    dumps[1].selectionState = SelectionState.SELECTED;
+
+    var div = document.createElement('div');
+
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = MemoryDumpTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.memoryDumps = dumps;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 50, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+  test('modelMapping', function() {
+    var dumps = createDumps();
+
+    var div = document.createElement('div');
+    var viewport = new Viewport(div);
+    var track = MemoryDumpTrack(viewport);
+    track.memoryDumps = dumps;
+
+    var d0 = track.getModelEventFromItem(track.items[0]);
+    assert.equal(d0, dumps[0]);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/multi_row_track.html b/trace-viewer/trace_viewer/core/tracks/multi_row_track.html
new file mode 100644
index 0000000..69c323b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/multi_row_track.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/container_track.html">
+<link rel="import" href="/core/trace_model/trace_model_settings.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var TraceModelSettings = tv.c.TraceModelSettings;
+
+  /**
+   * A track that displays a group of objects in multiple rows.
+   * @constructor
+   * @extends {ContainerTrack}
+   */
+  var MultiRowTrack = tv.b.ui.define(
+      'multi-row-track', tv.c.tracks.ContainerTrack);
+
+  MultiRowTrack.prototype = {
+
+    __proto__: tv.c.tracks.ContainerTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+      this.tooltip_ = '';
+      this.heading_ = '';
+
+      this.groupingSource_ = undefined;
+      this.itemsToGroup_ = undefined;
+
+      this.defaultToCollapsedWhenSubRowCountMoreThan = 1;
+
+      this.itemsGroupedOnLastUpdateContents_ = undefined;
+
+      this.currentSubRows_ = [];
+      this.expanded_ = true;
+    },
+
+    get itemsToGroup() {
+      return this.itemsToGroup_;
+    },
+
+    setItemsToGroup: function(itemsToGroup, opt_groupingSource) {
+      this.itemsToGroup_ = itemsToGroup;
+      this.groupingSource_ = opt_groupingSource;
+      this.updateContents_();
+      this.updateExpandedStateFromGroupingSource_();
+    },
+
+    get heading() {
+      return this.heading_;
+    },
+
+    set heading(h) {
+      this.heading_ = h;
+      this.updateContents_();
+    },
+
+    get tooltip() {
+      return this.tooltip_;
+    },
+
+    set tooltip(t) {
+      this.tooltip_ = t;
+      this.updateContents_();
+    },
+
+    get subRows() {
+      return this.currentSubRows_;
+    },
+
+    get hasVisibleContent() {
+      return this.children.length > 0;
+    },
+
+    get expanded() {
+      return this.expanded_;
+    },
+
+    set expanded(expanded) {
+      expanded = expanded;
+      if (this.expanded_ == expanded)
+        return;
+      this.expanded_ = expanded;
+      this.expandedStateChanged_();
+    },
+
+    onHeadingClicked_: function(e) {
+      if (this.subRows.length <= 1)
+        return;
+      this.expanded = !this.expanded;
+
+      if (this.groupingSource_) {
+        var modelSettings = new TraceModelSettings(
+            this.groupingSource_.model);
+        modelSettings.setSettingFor(this.groupingSource_, 'expanded',
+                                    this.expanded);
+      }
+
+      e.stopPropagation();
+    },
+
+    updateExpandedStateFromGroupingSource_: function() {
+      if (this.groupingSource_) {
+        var numSubRows = this.subRows.length;
+        var modelSettings = new TraceModelSettings(
+            this.groupingSource_.model);
+        if (numSubRows > 1) {
+          var defaultExpanded;
+          if (numSubRows > this.defaultToCollapsedWhenSubRowCountMoreThan) {
+            defaultExpanded = false;
+          } else {
+            defaultExpanded = true;
+          }
+          this.expanded = modelSettings.getSettingFor(
+              this.groupingSource_, 'expanded', defaultExpanded);
+        } else {
+          this.expanded = undefined;
+        }
+      }
+    },
+
+    expandedStateChanged_: function() {
+      var minH = Math.max(2, Math.ceil(18 / this.children.length));
+      var h = (this.expanded_ ? 18 : minH) + 'px';
+      for (var i = 0; i < this.children.length; i++)
+        this.children[i].height = h;
+
+      if (this.children.length > 0)
+        this.children[0].expanded = this.expanded;
+    },
+
+    updateContents_: function() {
+      tv.c.tracks.ContainerTrack.prototype.updateContents_.call(this);
+      if (!this.itemsToGroup_) {
+        this.updateHeadingAndTooltip_();
+        this.currentSubRows_ = [];
+        return;
+      }
+
+      if (this.areArrayContentsSame_(this.itemsGroupedOnLastUpdateContents_,
+                                     this.itemsToGroup_)) {
+        this.updateHeadingAndTooltip_();
+        return;
+      }
+
+      this.itemsGroupedOnLastUpdateContents_ = this.itemsToGroup_;
+
+      this.detach();
+      if (!this.itemsToGroup_.length) {
+        this.currentSubRows_ = [];
+        return;
+      }
+      var subRows = this.buildSubRows_(this.itemsToGroup_);
+      this.currentSubRows_ = subRows;
+      for (var srI = 0; srI < subRows.length; srI++) {
+        var subRow = subRows[srI];
+        if (!subRow.length)
+          continue;
+        var track = this.addSubTrack_(subRow);
+        track.addEventListener(
+          'heading-clicked', this.onHeadingClicked_.bind(this));
+      }
+      this.updateHeadingAndTooltip_();
+      this.expandedStateChanged_();
+    },
+
+    updateHeadingAndTooltip_: function() {
+      if (!this.firstChild)
+        return;
+      this.firstChild.heading = this.heading_;
+      this.firstChild.tooltip = this.tooltip_;
+    },
+
+    /**
+     * Breaks up the list of slices into N rows, each of which is a list of
+     * slices that are non overlapping.
+     */
+    buildSubRows_: function(itemsToGroup) {
+      throw new Error('Not implemented');
+    },
+
+    addSubTrack_: function(subRowItems) {
+      throw new Error('Not implemented');
+    },
+
+    areArrayContentsSame_: function(a, b) {
+      if (!a || !b)
+        return false;
+      if (!a.length || !b.length)
+        return false;
+      if (a.length != b.length)
+        return false;
+      for (var i = 0; i < a.length; ++i) {
+        if (a[i] != b[i])
+          return false;
+      }
+      return true;
+    }
+  };
+
+  return {
+    MultiRowTrack: MultiRowTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/object_instance_group_track.html b/trace-viewer/trace_viewer/core/tracks/object_instance_group_track.html
new file mode 100644
index 0000000..c454d25
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/object_instance_group_track.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/object_instance_view.html">
+<link rel="import" href="/core/tracks/multi_row_track.html">
+<link rel="import" href="/core/tracks/object_instance_track.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays a ObjectInstanceGroup.
+   * @constructor
+   * @extends {ContainerTrack}
+   */
+  var ObjectInstanceGroupTrack = tv.b.ui.define(
+      'object-instance-group-track', tv.c.tracks.MultiRowTrack);
+
+  ObjectInstanceGroupTrack.prototype = {
+
+    __proto__: tv.c.tracks.MultiRowTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('object-instance-group-track');
+      this.objectInstances_ = undefined;
+    },
+
+    get objectInstances() {
+      return this.itemsToGroup;
+    },
+
+    set objectInstances(objectInstances) {
+      this.setItemsToGroup(objectInstances);
+    },
+
+    addSubTrack_: function(objectInstances) {
+      var hasMultipleRows = this.subRows.length > 1;
+      var track = new tv.c.tracks.ObjectInstanceTrack(this.viewport);
+      track.objectInstances = objectInstances;
+      this.appendChild(track);
+      return track;
+    },
+
+    buildSubRows_: function(objectInstances) {
+      objectInstances.sort(function(x, y) {
+        return x.creationTs - y.creationTs;
+      });
+
+      var subRows = [];
+      for (var i = 0; i < objectInstances.length; i++) {
+        var objectInstance = objectInstances[i];
+
+        var found = false;
+        for (var j = 0; j < subRows.length; j++) {
+          var subRow = subRows[j];
+          var lastItemInSubRow = subRow[subRow.length - 1];
+          if (objectInstance.creationTs >= lastItemInSubRow.deletionTs) {
+            found = true;
+            subRow.push(objectInstance);
+            break;
+          }
+        }
+        if (!found) {
+          var subRow = [objectInstance];
+          subRows.push(subRow);
+        }
+      }
+      return subRows;
+    },
+    updateHeadingAndTooltip_: function() {
+    }
+  };
+
+  return {
+    ObjectInstanceGroupTrack: ObjectInstanceGroupTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/object_instance_track.css b/trace-viewer/trace_viewer/core/tracks/object_instance_track.css
new file mode 100644
index 0000000..0919e85
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/object_instance_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.object-instance-track {
+  height: 18px;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/object_instance_track.html b/trace-viewer/trace_viewer/core/tracks/object_instance_track.html
new file mode 100644
index 0000000..22756f8
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/object_instance_track.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/object_instance_track.css">
+
+<link rel="import" href="/base/extension_registry.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/tracks/heading_track.html">
+<link rel="import" href="/core/event_presenter.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  var SelectionState = tv.c.trace_model.SelectionState;
+  var EventPresenter = tv.c.EventPresenter;
+
+  /**
+   * A track that displays an array of Slice objects.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+
+  var ObjectInstanceTrack = tv.b.ui.define(
+      'object-instance-track', tv.c.tracks.HeadingTrack);
+
+  ObjectInstanceTrack.prototype = {
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('object-instance-track');
+      this.objectInstances_ = [];
+      this.objectSnapshots_ = [];
+    },
+
+    get objectInstances() {
+      return this.objectInstances_;
+    },
+
+    set objectInstances(objectInstances) {
+      if (!objectInstances || objectInstances.length == 0) {
+        this.heading = '';
+        this.objectInstances_ = [];
+        this.objectSnapshots_ = [];
+        return;
+      }
+      this.heading = objectInstances[0].typeName;
+      this.objectInstances_ = objectInstances;
+      this.objectSnapshots_ = [];
+      this.objectInstances_.forEach(function(instance) {
+        this.objectSnapshots_.push.apply(
+            this.objectSnapshots_, instance.snapshots);
+      }, this);
+      this.objectSnapshots_.sort(function(a, b) {
+        return a.ts - b.ts;
+      });
+    },
+
+    get height() {
+      return window.getComputedStyle(this).height;
+    },
+
+    set height(height) {
+      this.style.height = height;
+    },
+
+    get snapshotRadiusView() {
+      return 7 * (window.devicePixelRatio || 1);
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      switch (type) {
+        case tv.c.tracks.DrawType.SLICE:
+          this.drawSlices_(viewLWorld, viewRWorld);
+          break;
+      }
+    },
+
+    drawSlices_: function(viewLWorld, viewRWorld) {
+      var ctx = this.context();
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var bounds = this.getBoundingClientRect();
+      var height = bounds.height * pixelRatio;
+      var halfHeight = height * 0.5;
+      var twoPi = Math.PI * 2;
+
+      // Culling parameters.
+      var dt = this.viewport.currentDisplayTransform;
+      var snapshotRadiusView = this.snapshotRadiusView;
+      var snapshotRadiusWorld = dt.xViewVectorToWorld(height);
+      var loI;
+
+      // Begin rendering in world space.
+      ctx.save();
+      dt.applyTransformToCanvas(ctx);
+
+      // Instances
+      var objectInstances = this.objectInstances_;
+      var loI = tv.b.findLowIndexInSortedArray(
+          objectInstances,
+          function(instance) {
+            return instance.deletionTs;
+          },
+          viewLWorld);
+      ctx.strokeStyle = 'rgb(0,0,0)';
+      for (var i = loI; i < objectInstances.length; ++i) {
+        var instance = objectInstances[i];
+        var x = instance.creationTs;
+        if (x > viewRWorld)
+          break;
+
+        var right = instance.deletionTs == Number.MAX_VALUE ?
+            viewRWorld : instance.deletionTs;
+        ctx.fillStyle = EventPresenter.getObjectInstanceColor(instance);
+        ctx.fillRect(x, pixelRatio, right - x, height - 2 * pixelRatio);
+      }
+      ctx.restore();
+
+      // Snapshots. Has to run in worldspace because ctx.arc gets transformed.
+      var objectSnapshots = this.objectSnapshots_;
+      loI = tv.b.findLowIndexInSortedArray(
+          objectSnapshots,
+          function(snapshot) {
+            return snapshot.ts + snapshotRadiusWorld;
+          },
+          viewLWorld);
+      for (var i = loI; i < objectSnapshots.length; ++i) {
+        var snapshot = objectSnapshots[i];
+        var x = snapshot.ts;
+        if (x - snapshotRadiusWorld > viewRWorld)
+          break;
+        var xView = dt.xWorldToView(x);
+
+        ctx.fillStyle = EventPresenter.getObjectSnapshotColor(snapshot);
+        ctx.beginPath();
+        ctx.arc(xView, halfHeight, snapshotRadiusView, 0, twoPi);
+        ctx.fill();
+        if (snapshot.selected) {
+          ctx.lineWidth = 5;
+          ctx.strokeStyle = 'rgb(100,100,0)';
+          ctx.stroke();
+
+          ctx.beginPath();
+          ctx.arc(xView, halfHeight, snapshotRadiusView - 1, 0, twoPi);
+          ctx.lineWidth = 2;
+          ctx.strokeStyle = 'rgb(255,255,0)';
+          ctx.stroke();
+        } else {
+          ctx.lineWidth = 1;
+          ctx.strokeStyle = 'rgb(0,0,0)';
+          ctx.stroke();
+        }
+      }
+      ctx.lineWidth = 1;
+
+      // For performance reasons we only check the SelectionState of the first
+      // instance. If it's DIMMED we assume that all are DIMMED.
+      // TODO(egraether): Allow partial highlight.
+      var selectionState = SelectionState.NONE;
+      if (objectInstances.length &&
+          objectInstances[0].selectionState === SelectionState.DIMMED) {
+        selectionState = SelectionState.DIMMED;
+      }
+
+      // Dim the track when there is an active highlight.
+      if (selectionState === SelectionState.DIMMED) {
+        var width = bounds.width * pixelRatio;
+        ctx.fillStyle = 'rgba(255,255,255,0.5)';
+        ctx.fillRect(0, 0, width, height);
+        ctx.restore();
+      }
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      if (this.objectInstance_ !== undefined) {
+        this.objectInstance_.forEach(function(obj) {
+          eventToTrackMap.addEvent(obj, this);
+        }, this);
+      }
+
+      if (this.objectSnapshots_ !== undefined) {
+        this.objectSnapshots_.forEach(function(obj) {
+          eventToTrackMap.addEvent(obj, this);
+        }, this);
+      }
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+      // Pick snapshots first.
+      var foundSnapshot = false;
+      function onSnapshot(snapshot) {
+        selection.push(snapshot);
+        foundSnapshot = true;
+      }
+      var snapshotRadiusView = this.snapshotRadiusView;
+      var snapshotRadiusWorld = viewPixWidthWorld * snapshotRadiusView;
+      tv.b.iterateOverIntersectingIntervals(
+          this.objectSnapshots_,
+          function(x) { return x.ts - snapshotRadiusWorld; },
+          function(x) { return 2 * snapshotRadiusWorld; },
+          loWX, hiWX,
+          onSnapshot);
+      if (foundSnapshot)
+        return;
+
+      // Try picking instances.
+      tv.b.iterateOverIntersectingIntervals(
+          this.objectInstances_,
+          function(x) { return x.creationTs; },
+          function(x) { return x.deletionTs - x.creationTs; },
+          loWX, hiWX,
+          selection.push.bind(selection));
+    },
+
+    /**
+     * Add the item to the left or right of the provided event, if any, to the
+     * selection.
+     * @param {event} The current event item.
+     * @param {Number} offset Number of slices away from the event to look.
+     * @param {Selection} selection The selection to add an event to,
+     * if found.
+     * @return {boolean} Whether an event was found.
+     * @private
+     */
+    addItemNearToProvidedEventToSelection: function(event, offset, selection) {
+      var events;
+      if (event instanceof tv.c.trace_model.ObjectSnapshot)
+        events = this.objectSnapshots_;
+      else if (event instanceof tv.c.trace_model.ObjectInstance)
+        events = this.objectInstances_;
+      else
+        throw new Error('Unrecognized event');
+
+      var index = events.indexOf(event);
+      var newIndex = index + offset;
+      if (newIndex >= 0 && newIndex < events.length) {
+        selection.push(events[newIndex]);
+        return true;
+      }
+      return false;
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      var snapshot = tv.b.findClosestElementInSortedArray(
+          this.objectSnapshots_,
+          function(x) { return x.ts; },
+          worldX,
+          worldMaxDist);
+
+      if (!snapshot)
+        return;
+
+      selection.push(snapshot);
+
+      // TODO(egraether): Search for object instances as well, which was not
+      // implemented because it makes little sense with the current visual and
+      // needs to take care of overlapping intervals.
+    }
+  };
+
+
+  var options = new tv.b.ExtensionRegistryOptions(
+      tv.b.TYPE_BASED_REGISTRY_MODE);
+  tv.b.decorateExtensionRegistry(ObjectInstanceTrack, options);
+
+  return {
+    ObjectInstanceTrack: ObjectInstanceTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/object_instance_track_test.html b/trace-viewer/trace_viewer/core/tracks/object_instance_track_test.html
new file mode 100644
index 0000000..cce44bb
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/object_instance_track_test.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/core/trace_model/object_collection.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+<link rel="import" href="/core/tracks/object_instance_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var Selection = tv.c.Selection;
+  var ObjectInstanceTrack = tv.c.tracks.ObjectInstanceTrack;
+  var Viewport = tv.c.TimelineViewport;
+
+  var createObjects = function() {
+    var objects = new tv.c.trace_model.ObjectCollection({});
+    objects.idWasCreated('0x1000', 'tv.e.cc', 'Frame', 10);
+    objects.addSnapshot('0x1000', 'tv.e.cc', 'Frame', 10, 'snapshot-1');
+    objects.addSnapshot('0x1000', 'tv.e.cc', 'Frame', 25, 'snapshot-2');
+    objects.addSnapshot('0x1000', 'tv.e.cc', 'Frame', 40, 'snapshot-3');
+    objects.idWasDeleted('0x1000', 'tv.e.cc', 'Frame', 45);
+
+    objects.idWasCreated('0x1001', 'skia', 'Picture', 20);
+    objects.addSnapshot('0x1001', 'skia', 'Picture', 20, 'snapshot-1');
+    objects.idWasDeleted('0x1001', 'skia', 'Picture', 25);
+    return objects;
+  };
+
+  test('instantiate', function() {
+    var objects = createObjects();
+    var frames = objects.getAllInstancesByTypeName()['Frame'];
+    frames[0].snapshots[1].selectionState =
+        tv.c.trace_model.SelectionState.SELECTED;
+
+    var div = document.createElement('div');
+
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = ObjectInstanceTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.heading = 'testBasic';
+    track.objectInstances = frames;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 50, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+  test('selectionHitTestingWithThreadTrack', function() {
+    var objects = createObjects();
+    var frames = objects.getAllInstancesByTypeName()['Frame'];
+
+    var track = ObjectInstanceTrack(new Viewport());
+    track.objectInstances = frames;
+
+    // Hit outside range
+    var selection = new Selection();
+    track.addIntersectingItemsInRangeToSelectionInWorldSpace(
+        8, 8.1, 0.1, selection);
+    assert.equal(selection.length, 0);
+
+    // Hit the first snapshot, via pixel-nearness.
+    selection = new Selection();
+    track.addIntersectingItemsInRangeToSelectionInWorldSpace(
+        9.98, 9.99, 0.1, selection);
+    assert.equal(selection.length, 1);
+    assert.instanceOf(selection[0], tv.c.trace_model.ObjectSnapshot);
+
+    // Hit the instance, between the 1st and 2nd snapshots
+    selection = new Selection();
+    track.addIntersectingItemsInRangeToSelectionInWorldSpace(
+        20, 20.1, 0.1, selection);
+    assert.equal(selection.length, 1);
+    assert.instanceOf(selection[0], tv.c.trace_model.ObjectInstance);
+  });
+
+  test('addItemNearToProvidedEventToSelection', function() {
+    var objects = createObjects();
+    var frames = objects.getAllInstancesByTypeName()['Frame'];
+
+    var track = ObjectInstanceTrack(new Viewport());
+    track.objectInstances = frames;
+
+    var instance = new tv.c.trace_model.ObjectInstance(
+        {}, '0x1000', 'cat', 'n', 10);
+
+    assert.doesNotThrow(function() {
+      track.addItemNearToProvidedEventToSelection(instance, 0, undefined);
+    });
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/process_summary_track.html b/trace-viewer/trace_viewer/core/tracks/process_summary_track.html
new file mode 100644
index 0000000..cba6d01
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/process_summary_track.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/rect_track.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/color_scheme.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * Visualizes a Process's state using a series of rects to represent activity.
+   * @constructor
+   */
+  var ProcessSummaryTrack = tv.b.ui.define('process-summary-track',
+                                           tv.c.tracks.RectTrack);
+
+  ProcessSummaryTrack.buildRectsFromProcess = function(process) {
+    if (!process)
+      return [];
+
+    var ops = [];
+    // build list of start/end ops for each top level or important slice
+    var pushOp = function(isStart, time, slice) {
+      ops.push({
+        isStart: isStart,
+        time: time,
+        slice: slice
+      });
+    };
+    for (var tid in process.threads) {
+      var sliceGroup = process.threads[tid].sliceGroup;
+
+      sliceGroup.topLevelSlices.forEach(function(slice) {
+        pushOp(true, slice.start, undefined);
+        pushOp(false, slice.end, undefined);
+      });
+      sliceGroup.slices.forEach(function(slice) {
+        if (slice.important) {
+          pushOp(true, slice.start, slice);
+          pushOp(false, slice.end, slice);
+        }
+      });
+    }
+    ops.sort(function(a, b) { return a.time - b.time; });
+
+    var rects = [];
+    /**
+     * Build a row of rects which display one way for unimportant activity,
+     * and during important slices, show up as those important slices.
+     *
+     * If an important slice starts in the middle of another,
+     * just drop it on the floor.
+     */
+    var genericColorId = tv.b.ui.getColorIdForReservedName('generic_work');
+    var pushRect = function(start, end, slice) {
+      rects.push({
+        start: start,
+        end: end,
+        duration: end - start,
+        colorId: slice ? slice.colorId : genericColorId,
+        title: slice ? slice.title : undefined
+      });
+    }
+    var depth = 0;
+    var currentSlice = undefined;
+    var lastStart = undefined;
+    ops.forEach(function(op) {
+      depth += op.isStart ? 1 : -1;
+
+      if (currentSlice) {
+        // simply find end of current important slice
+        if (!op.isStart && op.slice == currentSlice) {
+          // important slice has ended
+          pushRect(lastStart, op.time, currentSlice);
+          lastStart = depth >= 1 ? op.time : undefined;
+          currentSlice = undefined;
+        }
+      } else {
+        if (op.isStart) {
+          if (depth == 1) {
+            lastStart = op.time;
+            currentSlice = op.slice;
+          } else if (op.slice) {
+            // switch to slice
+            if (op.time != lastStart) {
+              pushRect(lastStart, op.time, undefined);
+              lastStart = op.time;
+            }
+            currentSlice = op.slice;
+          }
+        } else {
+          if (depth == 0) {
+            pushRect(lastStart, op.time, undefined);
+            lastStart = undefined;
+          }
+        }
+      }
+    });
+    return rects;
+  };
+
+  ProcessSummaryTrack.prototype = {
+    __proto__: tv.c.tracks.RectTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.RectTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('inverse-expand');
+    },
+
+    get process() {
+      return this.process_;
+    },
+
+    set process(process) {
+      this.process_ = process;
+      this.rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+    },
+
+    getModelEventFromItem: function(thing) {
+      // Do nothing, since not selectable
+      return undefined;
+    }
+  };
+
+  return {
+    ProcessSummaryTrack: ProcessSummaryTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/process_summary_track_test.html b/trace-viewer/trace_viewer/core/tracks/process_summary_track_test.html
new file mode 100644
index 0000000..b682ec6
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/process_summary_track_test.html
@@ -0,0 +1,101 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/slice_group.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/tracks/process_summary_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ProcessSummaryTrack = tv.c.tracks.ProcessSummaryTrack;
+  var newSlice = tv.c.test_utils.newSlice;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  test('buildRectSimple', function() {
+    var process;
+    var model = tv.c.test_utils.newModel(function(model) {
+      process = model.getOrCreateProcess(1);
+      // XXXX
+      //    XXXX
+      var thread1 = process.getOrCreateThread(1);
+      thread1.sliceGroup.pushSlice(newSlice(1, 4));
+      var thread2 = process.getOrCreateThread(2);
+      thread2.sliceGroup.pushSlice(newSlice(4, 4));
+    });
+
+    var rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+    assert.equal(rects.length, 1);
+    var rect = rects[0];
+    assert.closeTo(rect.start, 1, 1e-5);
+    assert.closeTo(rect.end, 8, 1e-5);
+  });
+
+  test('buildRectComplex', function() {
+    var process;
+    var model = tv.c.test_utils.newModel(function(model) {
+      process = model.getOrCreateProcess(1);
+      // XXXX    X X XX
+      //    XXXX XXX    X
+      var thread1 = process.getOrCreateThread(1);
+      thread1.sliceGroup.pushSlice(newSlice(1, 4));
+      thread1.sliceGroup.pushSlice(newSlice(9, 1));
+      thread1.sliceGroup.pushSlice(newSlice(11, 1));
+      thread1.sliceGroup.pushSlice(newSlice(13, 2));
+      var thread2 = process.getOrCreateThread(2);
+      thread2.sliceGroup.pushSlice(newSlice(4, 4));
+      thread2.sliceGroup.pushSlice(newSlice(9, 3));
+      thread2.sliceGroup.pushSlice(newSlice(16, 1));
+    });
+
+    var rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+    assert.equal(4, rects.length);
+    assert.closeTo(rects[0].start, 1, 1e-5);
+    assert.closeTo(rects[0].end, 8, 1e-5);
+    assert.closeTo(rects[1].start, 9, 1e-5);
+    assert.closeTo(rects[1].end, 12, 1e-5);
+    assert.closeTo(rects[2].start, 13, 1e-5);
+    assert.closeTo(rects[2].end, 15, 1e-5);
+    assert.closeTo(rects[3].start, 16, 1e-5);
+    assert.closeTo(rects[3].end, 17, 1e-5);
+  });
+
+  test('buildRectImportantSlice', function() {
+    var process;
+    var model = tv.c.test_utils.newModel(function(model) {
+      //    [    unimportant    ]
+      //         [important]
+      var a = newSliceNamed('unimportant', 4, 21);
+      var b = newSliceNamed('important', 9, 11);
+      b.important = true;
+      process = model.getOrCreateProcess(1);
+      process.getOrCreateThread(1).sliceGroup.pushSlices([a, b]);
+
+      model.importantSlice = b;
+    });
+
+    var rects = ProcessSummaryTrack.buildRectsFromProcess(process);
+
+    assert.equal(3, rects.length);
+    assert.closeTo(rects[0].start, 4, 1e-5);
+    assert.closeTo(rects[0].end, 9, 1e-5);
+    assert.closeTo(rects[1].start, 9, 1e-5);
+    assert.closeTo(rects[1].end, 20, 1e-5);
+    assert.closeTo(rects[2].start, 20, 1e-5);
+    assert.closeTo(rects[2].end, 25, 1e-5);
+
+    // middle rect represents important slice, so colorId & title are preserved
+    assert.equal(rects[1].title, model.importantSlice.title);
+    assert.equal(rects[1].colorId, model.importantSlice.colorId);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/process_track.html b/trace-viewer/trace_viewer/core/tracks/process_track.html
new file mode 100644
index 0000000..c15bf26
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/process_track.html
@@ -0,0 +1,145 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/process_track_base.html">
+<link rel="import" href="/core/draw_helpers.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  var ProcessTrackBase = tv.c.tracks.ProcessTrackBase;
+
+  /**
+   * @constructor
+   */
+  var ProcessTrack = tv.b.ui.define('process-track', ProcessTrackBase);
+
+  ProcessTrack.prototype = {
+    __proto__: ProcessTrackBase.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ProcessTrackBase.prototype.decorate.call(this, viewport);
+    },
+
+    drawTrack: function(type) {
+      switch (type) {
+        case tv.c.tracks.DrawType.INSTANT_EVENT:
+          if (!this.processBase.instantEvents ||
+              this.processBase.instantEvents.length === 0)
+            break;
+
+          var ctx = this.context();
+
+          var pixelRatio = window.devicePixelRatio || 1;
+          var bounds = this.getBoundingClientRect();
+          var canvasBounds = ctx.canvas.getBoundingClientRect();
+
+          ctx.save();
+          ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+          var dt = this.viewport.currentDisplayTransform;
+          var viewLWorld = dt.xViewToWorld(0);
+          var viewRWorld = dt.xViewToWorld(
+              bounds.width * pixelRatio);
+
+          tv.c.drawInstantSlicesAsLines(
+              ctx,
+              this.viewport.currentDisplayTransform,
+              viewLWorld,
+              viewRWorld,
+              bounds.height,
+              this.processBase.instantEvents,
+              2);
+
+          ctx.restore();
+
+          break;
+
+        case tv.c.tracks.DrawType.BACKGROUND:
+          this.drawBackground_();
+          // Don't bother recursing further, Process is the only level that
+          // draws backgrounds.
+          return;
+      }
+
+      tv.c.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+    },
+
+    drawBackground_: function() {
+      var ctx = this.context();
+      var canvasBounds = ctx.canvas.getBoundingClientRect();
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var draw = false;
+      ctx.fillStyle = '#eee';
+      for (var i = 0; i < this.children.length; ++i) {
+        if (!(this.children[i] instanceof tv.c.tracks.Track) ||
+            (this.children[i] instanceof tv.c.tracks.SpacingTrack))
+          continue;
+
+        draw = !draw;
+        if (!draw)
+          continue;
+
+        var bounds = this.children[i].getBoundingClientRect();
+        ctx.fillRect(0, pixelRatio * (bounds.top - canvasBounds.top),
+            ctx.canvas.width, pixelRatio * bounds.height);
+      }
+    },
+
+    // Process maps to processBase because we derive from ProcessTrackBase.
+    set process(process) {
+      this.processBase = process;
+    },
+
+    get process() {
+      return this.processBase;
+    },
+
+    get eventContainer() {
+      return this.process;
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      containerToTrackMap.addContainer(this.process, this);
+      this.tracks_.forEach(function(track) {
+        track.addContainersToTrackMap(containerToTrackMap);
+      });
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+      function onPickHit(instantEvent) {
+        selection.push(instantEvent);
+      }
+      tv.b.iterateOverIntersectingIntervals(this.processBase.instantEvents,
+          function(x) { return x.start; },
+          function(x) { return x.duration; },
+          loWX, hiWX,
+          onPickHit.bind(this));
+
+      tv.c.tracks.ContainerTrack.prototype.
+          addIntersectingItemsInRangeToSelectionInWorldSpace.
+          apply(this, arguments);
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      this.addClosestInstantEventToSelection(this.processBase.instantEvents,
+                                             worldX, worldMaxDist, selection);
+      tv.c.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+          apply(this, arguments);
+    }
+  };
+
+  return {
+    ProcessTrack: ProcessTrack
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/process_track_base.css b/trace-viewer/trace_viewer/core/tracks/process_track_base.css
new file mode 100644
index 0000000..c7fe1c4
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/process_track_base.css
@@ -0,0 +1,32 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.process-track-header {
+  -webkit-flex: 0 0 auto;
+  background-image: -webkit-gradient(linear,
+                                     0 0, 100% 0,
+                                     from(#E5E5E5),
+                                     to(#D1D1D1));
+  border-bottom: 1px solid #8e8e8e;
+  border-top: 1px solid white;
+  font-size: 75%;
+}
+
+.process-track-base:not(.expanded) > .track:not(.inverse-expand) {
+  display: none;
+}
+
+.process-track-base.expanded > .track.inverse-expand {
+  display: none;
+}
+
+.process-track-name:before {
+  content: '\25B8'; /* Right triangle */
+  padding: 0 5px;
+}
+
+.process-track-base.expanded .process-track-name:before {
+  content: '\25BE'; /* Down triangle */
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/process_track_base.html b/trace-viewer/trace_viewer/core/tracks/process_track_base.html
new file mode 100644
index 0000000..b9af833
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/process_track_base.html
@@ -0,0 +1,261 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/process_track_base.css">
+
+<link rel="import" href="/core/tracks/container_track.html">
+<link rel="import" href="/core/tracks/counter_track.html">
+<link rel="import" href="/core/tracks/object_instance_group_track.html">
+<link rel="import" href="/core/tracks/process_summary_track.html">
+<link rel="import" href="/core/tracks/spacing_track.html">
+<link rel="import" href="/core/tracks/thread_track.html">
+<link rel="import" href="/core/trace_model/trace_model_settings.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  var ObjectSnapshotView = tv.c.analysis.ObjectSnapshotView;
+  var ObjectInstanceView = tv.c.analysis.ObjectInstanceView;
+  var TraceModelSettings = tv.c.TraceModelSettings;
+  var SpacingTrack = tv.c.tracks.SpacingTrack;
+
+  /**
+   * Visualizes a Process by building ThreadTracks and CounterTracks.
+   * @constructor
+   */
+  var ProcessTrackBase =
+      tv.b.ui.define('process-track-base', tv.c.tracks.ContainerTrack);
+
+  ProcessTrackBase.prototype = {
+
+    __proto__: tv.c.tracks.ContainerTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+
+      this.processBase_ = undefined;
+
+      this.classList.add('process-track-base');
+      this.classList.add('expanded');
+
+      this.processNameEl_ = tv.b.ui.createSpan();
+      this.processNameEl_.classList.add('process-track-name');
+
+      this.headerEl_ = tv.b.ui.createDiv({className: 'process-track-header'});
+      this.headerEl_.appendChild(this.processNameEl_);
+      this.headerEl_.addEventListener('click', this.onHeaderClick_.bind(this));
+
+      this.appendChild(this.headerEl_);
+    },
+
+    get processBase() {
+      return this.processBase_;
+    },
+
+    set processBase(processBase) {
+      this.processBase_ = processBase;
+
+      if (this.processBase_) {
+        var modelSettings = new TraceModelSettings(this.processBase_.model);
+        var defaultValue;
+        if (this.processBase_.labels !== undefined &&
+            this.processBase_.labels.length == 1 &&
+            this.processBase_.labels[0] == 'chrome://tracing') {
+          defaultValue = false;
+        } else {
+          defaultValue = true;
+        }
+        this.expanded = modelSettings.getSettingFor(
+            this.processBase_, 'expanded', defaultValue);
+      }
+
+      this.updateContents_();
+    },
+
+    get expanded() {
+      return this.classList.contains('expanded');
+    },
+
+    set expanded(expanded) {
+      expanded = !!expanded;
+
+      if (this.expanded === expanded)
+        return;
+
+      this.classList.toggle('expanded');
+
+      // Expanding and collapsing tracks is, essentially, growing and shrinking
+      // the viewport. We dispatch a change event to trigger any processing
+      // to happen.
+      this.viewport_.dispatchChangeEvent();
+
+      if (!this.processBase_)
+        return;
+
+      var modelSettings = new TraceModelSettings(this.processBase_.model);
+      modelSettings.setSettingFor(this.processBase_, 'expanded', expanded);
+    },
+
+    get hasVisibleContent() {
+      if (this.expanded)
+        return this.children.length > 1;
+      return true;
+    },
+
+    onHeaderClick_: function(e) {
+      e.stopPropagation();
+      e.preventDefault();
+      this.expanded = !this.expanded;
+    },
+
+    updateContents_: function() {
+      this.tracks_.forEach(function(track) {
+        this.removeChild(track);
+      }, this);
+
+      if (!this.processBase_)
+        return;
+
+      this.processNameEl_.textContent = this.processBase_.userFriendlyName;
+      this.headerEl_.title = this.processBase_.userFriendlyDetails;
+
+      // Create the object instance tracks for this process.
+      this.willAppendTracks_();
+      this.appendSummaryTrack_();
+      this.appendObjectInstanceTracks_();
+      this.appendCounterTracks_();
+      this.appendThreadTracks_();
+      this.didAppendTracks_();
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      this.tracks_.forEach(function(track) {
+        track.addEventsToTrackMap(eventToTrackMap);
+      });
+    },
+
+    willAppendTracks_: function() {
+    },
+
+    didAppendTracks_: function() {
+    },
+
+    appendSummaryTrack_: function() {
+      var track = new tv.c.tracks.ProcessSummaryTrack(this.viewport);
+      track.process = this.process;
+      if (!track.hasVisibleContent)
+        return;
+      this.appendChild(track);
+      this.appendChild(new SpacingTrack(this.viewport));
+    },
+
+    appendObjectInstanceTracks_: function() {
+      var instancesByTypeName =
+          this.processBase_.objects.getAllInstancesByTypeName();
+      var instanceTypeNames = tv.b.dictionaryKeys(instancesByTypeName);
+      instanceTypeNames.sort();
+
+      var didAppendAtLeastOneTrack = false;
+      instanceTypeNames.forEach(function(typeName) {
+        var allInstances = instancesByTypeName[typeName];
+
+        // If a object snapshot has a view it will be shown,
+        // unless the view asked for it to not be shown.
+        var instanceViewInfo = ObjectInstanceView.getTypeInfo(
+            undefined, typeName);
+        var snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+            undefined, typeName);
+        if (instanceViewInfo && !instanceViewInfo.metadata.showInTrackView)
+          instanceViewInfo = undefined;
+        if (snapshotViewInfo && !snapshotViewInfo.metadata.showInTrackView)
+          snapshotViewInfo = undefined;
+        var hasViewInfo = instanceViewInfo || snapshotViewInfo;
+
+        // There are some instances that don't merit their own track in
+        // the UI. Filter them out.
+        var visibleInstances = [];
+        for (var i = 0; i < allInstances.length; i++) {
+          var instance = allInstances[i];
+
+          // Do not create tracks for instances that have no snapshots.
+          if (instance.snapshots.length === 0)
+            continue;
+
+          // Do not create tracks for instances that have implicit snapshots
+          // and don't have a view.
+          if (instance.hasImplicitSnapshots && !hasViewInfo)
+            continue;
+
+          visibleInstances.push(instance);
+        }
+        if (visibleInstances.length === 0)
+          return;
+
+        // Look up the constructor for this track, or use the default
+        // constructor if none exists.
+        var trackConstructor =
+            tv.c.tracks.ObjectInstanceTrack.getConstructor(
+                undefined, typeName);
+        if (!trackConstructor) {
+          var snapshotViewInfo = ObjectSnapshotView.getTypeInfo(
+              undefined, typeName);
+          if (snapshotViewInfo && snapshotViewInfo.metadata.showInstances) {
+            trackConstructor = tv.c.tracks.ObjectInstanceGroupTrack;
+          } else {
+            trackConstructor = tv.c.tracks.ObjectInstanceTrack;
+          }
+        }
+        var track = new trackConstructor(this.viewport);
+        track.objectInstances = visibleInstances;
+        this.appendChild(track);
+        didAppendAtLeastOneTrack = true;
+      }, this);
+      if (didAppendAtLeastOneTrack)
+        this.appendChild(new SpacingTrack(this.viewport));
+    },
+
+    appendCounterTracks_: function() {
+      // Add counter tracks for this process.
+      var counters = tv.b.dictionaryValues(this.processBase.counters);
+      counters.sort(tv.c.trace_model.Counter.compare);
+
+      // Create the counters for this process.
+      counters.forEach(function(counter) {
+        var track = new tv.c.tracks.CounterTrack(this.viewport);
+        track.counter = counter;
+        this.appendChild(track);
+        this.appendChild(new SpacingTrack(this.viewport));
+      }.bind(this));
+    },
+
+    appendThreadTracks_: function() {
+      // Get a sorted list of threads.
+      var threads = tv.b.dictionaryValues(this.processBase.threads);
+      threads.sort(tv.c.trace_model.Thread.compare);
+
+      // Create the threads.
+      threads.forEach(function(thread) {
+        var track = new tv.c.tracks.ThreadTrack(this.viewport);
+        track.thread = thread;
+        if (!track.hasVisibleContent)
+          return;
+        this.appendChild(track);
+        this.appendChild(new SpacingTrack(this.viewport));
+      }.bind(this));
+    }
+  };
+
+  return {
+    ProcessTrackBase: ProcessTrackBase
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/rect_annotation_view.html b/trace-viewer/trace_viewer/core/tracks/rect_annotation_view.html
new file mode 100644
index 0000000..e28b98b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/rect_annotation_view.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/annotation_view.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.annotations', function() {
+  /**
+   * A view responsible for drawing a single highlight rectangle box on
+   * the timeline.
+   * @extends {AnnotationView}
+   * @constructor
+   */
+  function RectAnnotationView(viewport, annotation) {
+    this.viewport_ = viewport;
+    this.annotation_ = annotation;
+  }
+
+  RectAnnotationView.prototype = {
+    __proto__: tv.c.annotations.AnnotationView.prototype,
+
+    draw: function(ctx) {
+      var dt = this.viewport_.currentDisplayTransform;
+      var startCoords =
+          this.annotation_.startLocation.toViewCoordinates(this.viewport_);
+      var endCoords =
+          this.annotation_.endLocation.toViewCoordinates(this.viewport_);
+
+      // Prevent drawing into the ruler track by clamping the initial Y
+      // point and the rect's Y size.
+      var startY = startCoords.viewY - ctx.canvas.getBoundingClientRect().top;
+      var sizeY = endCoords.viewY - startCoords.viewY;
+      if (startY + sizeY < 0) {
+        // In this case sizeY is negative. If final Y is negative,
+        // overwrite startY so that the rectangle ends at y=0.
+        startY = sizeY;
+      } else if (startY < 0) {
+        startY = 0;
+      }
+
+      ctx.fillStyle = this.annotation_.fillStyle;
+      ctx.fillRect(startCoords.viewX, startY,
+          endCoords.viewX - startCoords.viewX, sizeY);
+    }
+  };
+
+  return {
+    RectAnnotationView: RectAnnotationView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/rect_track.css b/trace-viewer/trace_viewer/core/tracks/rect_track.css
new file mode 100644
index 0000000..0467c91
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/rect_track.css
@@ -0,0 +1,8 @@
+/* Copyright (c) 2014 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.rect-track {
+  height: 18px;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/rect_track.html b/trace-viewer/trace_viewer/core/tracks/rect_track.html
new file mode 100644
index 0000000..f390182
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/rect_track.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/rect_track.css">
+
+<link rel="import" href="/core/tracks/heading_track.html">
+<link rel="import" href="/core/fast_rect_renderer.html">
+<link rel="import" href="/core/draw_helpers.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  /**
+   * A track that displays an array of Rect objects.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var RectTrack = tv.b.ui.define(
+      'rect-track', tv.c.tracks.HeadingTrack);
+
+  RectTrack.prototype = {
+
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('rect-track');
+      this.asyncStyle_ = false;
+      this.rects_ = null;
+    },
+
+    get asyncStyle() {
+      return this.asyncStyle_;
+    },
+
+    set asyncStyle(v) {
+      this.asyncStyle_ = !!v;
+    },
+
+    get rects() {
+      return this.rects_;
+    },
+
+    set rects(rects) {
+      this.rects_ = rects || [];
+      this.invalidateDrawingContainer();
+    },
+
+    get height() {
+      return window.getComputedStyle(this).height;
+    },
+
+    set height(height) {
+      this.style.height = height;
+      this.invalidateDrawingContainer();
+    },
+
+    get hasVisibleContent() {
+      return this.rects_.length > 0;
+    },
+
+    getModelEventFromItem: function(rect) {
+      throw new Error('Not implemented.');
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      switch (type) {
+        case tv.c.tracks.DrawType.SLICE:
+          this.drawRects_(viewLWorld, viewRWorld);
+          break;
+      }
+    },
+
+    drawRects_: function(viewLWorld, viewRWorld) {
+      var ctx = this.context();
+
+      ctx.save();
+      var bounds = this.getBoundingClientRect();
+      tv.c.drawSlices(
+          ctx,
+          this.viewport.currentDisplayTransform,
+          viewLWorld,
+          viewRWorld,
+          bounds.height,
+          this.rects_,
+          this.asyncStyle_);
+      ctx.restore();
+
+      if (bounds.height <= 6)
+        return;
+
+      var fontSize, yOffset;
+      if (bounds.height < 15) {
+        fontSize = 6;
+        yOffset = 1.0;
+      } else {
+        fontSize = 10;
+        yOffset = 2.5;
+      }
+      tv.c.drawLabels(
+          ctx,
+          this.viewport.currentDisplayTransform,
+          viewLWorld,
+          viewRWorld,
+          this.rects_,
+          this.asyncStyle_,
+          fontSize,
+          yOffset);
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      if (this.rects_ === undefined || this.rects_ === null)
+        return;
+
+      this.rects_.forEach(function(rect) {
+        var event = this.getModelEventFromItem(rect);
+        if (event)
+          eventToTrackMap.addEvent(event, this);
+      }, this);
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+      function onRect(rect) {
+        var event = this.getModelEventFromItem(rect);
+        if (event)
+          selection.push(event);
+      }
+      onRect = onRect.bind(this);
+      tv.b.iterateOverIntersectingIntervals(this.rects_,
+          function(x) { return x.start; },
+          function(x) { return x.duration; },
+          loWX, hiWX,
+          onRect);
+    },
+
+    /**
+     * Find the index for the given rect.
+     * @return {index} Index of the given rect, or undefined.
+     * @private
+     */
+    indexOfRect_: function(rect) {
+      var index = tv.b.findLowIndexInSortedArray(this.rects_,
+          function(x) { return x.start; },
+          rect.start);
+      while (index < this.rects_.length &&
+          rect.start == this.rects_[index].start &&
+          rect.colorId != this.rects_[index].colorId) {
+        index++;
+      }
+      return index < this.rects_.length ? index : undefined;
+    },
+
+    /**
+     * Add the item to the left or right of the provided event, if any, to the
+     * selection.
+     * @param {rect} The current rect.
+     * @param {Number} offset Number of rects away from the event to look.
+     * @param {Selection} selection The selection to add an event to,
+     * if found.
+     * @return {boolean} Whether an event was found.
+     * @private
+     */
+    addItemNearToProvidedEventToSelection: function(event, offset, selection) {
+      var index = this.indexOfRect_(event);
+      if (index === undefined)
+        return false;
+
+      var newIndex = index + offset;
+      if (newIndex < 0 || newIndex >= this.rects_.length)
+        return false;
+
+      var event = this.rects_[newIndex];
+      if (event)
+        selection.push(event);
+      return true;
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+      for (var i = 0; i < this.rects_.length; ++i) {
+        if (filter.matchSlice(this.rects_[i])) {
+          var event = this.getModelEventFromItem(this.rects_[i]);
+          if (event)
+            selection.push(event);
+        }
+      }
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      var rect = tv.b.findClosestIntervalInSortedIntervals(
+          this.rects_,
+          function(x) { return x.start; },
+          function(x) { return x.end; },
+          worldX,
+          worldMaxDist);
+
+      if (rect) {
+        var event = this.getModelEventFromItem(rect);
+        if (event)
+          selection.push(event);
+      }
+    }
+  };
+
+  return {
+    RectTrack: RectTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/rect_track_test.html b/trace-viewer/trace_viewer/core/tracks/rect_track_test.html
new file mode 100644
index 0000000..2b36043
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/rect_track_test.html
@@ -0,0 +1,383 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/trace_model/slice.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/draw_helpers.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Selection = tv.c.Selection;
+  var RectTrack = tv.c.tracks.RectTrack;
+  var Slice = tv.c.trace_model.Slice;
+  var Viewport = tv.c.TimelineViewport;
+
+  var monkeyPatchTrack = function(track) {
+    track.getModelEventFromItem = function(rect) {
+      return rect;
+    };
+    return track;
+  };
+
+  test('instantiate_withRects', function() {
+    var div = document.createElement('div');
+
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = RectTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.heading = 'testBasicRects';
+    track.rects = [
+      new Slice('', 'a', 0, 1, {}, 1),
+      new Slice('', 'b', 1, 2.1, {}, 4.8),
+      new Slice('', 'b', 1, 7, {}, 0.5),
+      new Slice('', 'c', 2, 7.6, {}, 0.4)
+    ];
+
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 8.8, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+  test('instantiate_shrinkingRectSize', function() {
+    var div = document.createElement('div');
+
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = RectTrack(viewport);
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.heading = 'testShrinkingRectSizes';
+    var x = 0;
+    var widths = [10, 5, 4, 3, 2, 1, 0.5, 0.4, 0.3, 0.2, 0.1, 0.05];
+    var slices = [];
+    for (var i = 0; i < widths.length; i++) {
+      var s = new Slice('', 'a', 1, x, {}, widths[i]);
+      x += s.duration + 0.5;
+      slices.push(s);
+    }
+    track.rects = slices;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 1.1 * x, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+
+  test('instantiate_elide', function() {
+    var optDicts = [{ trackName: 'elideOff', elide: false },
+                    { trackName: 'elideOn', elide: true }];
+
+    var tooLongTitle = 'Unless eliding this SHOULD NOT BE DISPLAYED.  ';
+    var bigTitle = 'Very big title name that goes on longer ' +
+                   'than you may expect';
+
+    for (var dictIndex in optDicts) {
+      var dict = optDicts[dictIndex];
+
+      var div = document.createElement('div');
+      div.appendChild(document.createTextNode(dict.trackName));
+
+      var viewport = new Viewport(div);
+      var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+      div.appendChild(drawingContainer);
+
+      var track = new RectTrack(viewport);
+      drawingContainer.appendChild(track);
+
+      this.addHTMLOutput(div);
+      drawingContainer.invalidate();
+
+      track.SHOULD_ELIDE_TEXT = dict.elide;
+      track.heading = 'Visual: ' + dict.trackName;
+      track.rects = [
+        // title, colorId, start, args, opt_duration
+        new Slice('', 'a ' + tooLongTitle + bigTitle, 0, 1, {}, 1),
+        new Slice('', bigTitle, 1, 2.1, {}, 4.8),
+        new Slice('', 'cccc cccc cccc', 1, 7, {}, 0.5),
+        new Slice('', 'd', 2, 7.6, {}, 1.0)
+      ];
+      var dt = new tv.c.TimelineDisplayTransform();
+      dt.xSetWorldBounds(0, 9.5, track.clientWidth);
+      track.viewport.setDisplayTransformImmediately(dt);
+    }
+  });
+
+  test('findAllObjectsMatchingInRectTrack', function() {
+    var track = monkeyPatchTrack(RectTrack(new tv.c.TimelineViewport()));
+    track.rects = [
+      new Slice('', 'a', 0, 1, {}, 1),
+      new Slice('', 'b', 1, 2.1, {}, 4.8),
+      new Slice('', 'b', 1, 7, {}, 0.5),
+      new Slice('', 'c', 2, 7.6, {}, 0.4)
+    ];
+    var selection = new Selection();
+    track.addAllObjectsMatchingFilterToSelection(
+        new tv.c.TitleOrCategoryFilter('b'), selection);
+
+    assert.equal(2, selection.length);
+    assert.equal(track.rects[1], selection[0]);
+    assert.equal(track.rects[2], selection[1]);
+  });
+
+  test('selectionHitTesting', function() {
+    var testEl = document.createElement('div');
+    testEl.appendChild(tv.b.ui.createScopedStyle('heading { width: 100px; }'));
+    testEl.style.width = '600px';
+
+    var viewport = new Viewport(testEl);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    testEl.appendChild(drawingContainer);
+
+    var track = monkeyPatchTrack(new RectTrack(viewport));
+    drawingContainer.appendChild(track);
+    this.addHTMLOutput(testEl);
+
+    drawingContainer.updateCanvasSizeIfNeeded_();
+
+    track.heading = 'testSelectionHitTesting';
+    track.rects = [
+      new Slice('', 'a', 0, 1, {}, 1),
+      new Slice('', 'b', 1, 5, {}, 4.8)
+    ];
+    var y = track.getBoundingClientRect().top + 5;
+    var pixelRatio = window.devicePixelRatio || 1;
+    var wW = 10;
+    var vW = drawingContainer.canvas.getBoundingClientRect().width;
+
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, wW, vW * pixelRatio);
+    track.viewport.setDisplayTransformImmediately(dt);
+
+    var selection = new Selection();
+    var x = (1.5 / wW) * vW;
+    track.addIntersectingItemsInRangeToSelection(x, x + 1, y, y + 1, selection);
+    assert.equal(track.rects[0], selection[0]);
+
+    var selection = new Selection();
+    x = (2.1 / wW) * vW;
+    track.addIntersectingItemsInRangeToSelection(x, x + 1, y, y + 1, selection);
+    assert.equal(0, selection.length);
+
+    var selection = new Selection();
+    x = (6.8 / wW) * vW;
+    track.addIntersectingItemsInRangeToSelection(x, x + 1, y, y + 1, selection);
+    assert.equal(track.rects[1], selection[0]);
+
+    var selection = new Selection();
+    x = (9.9 / wW) * vW;
+    track.addIntersectingItemsInRangeToSelection(x, x + 1, y, y + 1, selection);
+    assert.equal(0, selection.length);
+  });
+
+  test('elide', function() {
+    var testEl = document.createElement('div');
+
+    var viewport = new Viewport(testEl);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    testEl.appendChild(drawingContainer);
+
+    var track = new RectTrack(viewport);
+    drawingContainer.appendChild(track);
+    this.addHTMLOutput(testEl);
+
+    drawingContainer.updateCanvasSizeIfNeeded_();
+
+    var bigtitle = 'Super duper long long title ' +
+        'holy moly when did you get so verbose?';
+    var smalltitle = 'small';
+    track.heading = 'testElide';
+    track.rects = [
+      // title, colorId, start, args, opt_duration
+      new Slice('', bigtitle, 0, 1, {}, 1),
+      new Slice('', smalltitle, 1, 2, {}, 1)
+    ];
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 3.3, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+
+    var stringWidthPair = undefined;
+    var pixWidth = dt.xViewVectorToWorld(1);
+
+    // Small titles on big slices are not elided.
+    stringWidthPair =
+        tv.c.elidedTitleCache_.get(
+            track.context(),
+            pixWidth,
+            smalltitle,
+            tv.c.elidedTitleCache_.labelWidth(
+                track.context(),
+                smalltitle),
+            1);
+    assert.equal(smalltitle, stringWidthPair.string);
+
+    // Keep shrinking the slice until eliding starts.
+    var elidedWhenSmallEnough = false;
+    for (var sliceLength = 1; sliceLength >= 0.00001; sliceLength /= 2.0) {
+      stringWidthPair =
+          tv.c.elidedTitleCache_.get(
+              track.context(),
+              pixWidth,
+              smalltitle,
+              tv.c.elidedTitleCache_.labelWidth(
+                  track.context(),
+                  smalltitle),
+              sliceLength);
+      if (stringWidthPair.string.length < smalltitle.length) {
+        elidedWhenSmallEnough = true;
+        break;
+      }
+    }
+    assert.isTrue(elidedWhenSmallEnough);
+
+    // Big titles are elided immediately.
+    var superBigTitle = '';
+    for (var x = 0; x < 10; x++) {
+      superBigTitle += bigtitle;
+    }
+    stringWidthPair =
+        tv.c.elidedTitleCache_.get(
+            track.context(),
+            pixWidth,
+            superBigTitle,
+            tv.c.elidedTitleCache_.labelWidth(
+                track.context(),
+                superBigTitle),
+            1);
+    assert.isTrue(stringWidthPair.string.length < superBigTitle.length);
+
+    // And elided text ends with ...
+    var len = stringWidthPair.string.length;
+    assert.equal('...', stringWidthPair.string.substring(len - 3, len));
+  });
+
+  test('rectTrackAddItemNearToProvidedEvent', function() {
+    var track = monkeyPatchTrack(new RectTrack(new tv.c.TimelineViewport()));
+    track.rects = [
+      new Slice('', 'a', 0, 1, {}, 1),
+      new Slice('', 'b', 1, 2.1, {}, 4.8),
+      new Slice('', 'b', 1, 7, {}, 0.5),
+      new Slice('', 'c', 2, 7.6, {}, 0.4)
+    ];
+    var sel = new Selection();
+    track.addAllObjectsMatchingFilterToSelection(
+        new tv.c.TitleOrCategoryFilter('b'), sel);
+    var ret;
+
+    // Select to the right of B.
+    var selRight = new Selection();
+    ret = track.addItemNearToProvidedEventToSelection(sel[0], 1, selRight);
+    assert.isTrue(ret);
+    assert.equal(track.rects[2], selRight[0]);
+
+    // Select to the right of the 2nd b.
+    var selRight2 = new Selection();
+    ret = track.addItemNearToProvidedEventToSelection(sel[0], 2, selRight2);
+    assert.isTrue(ret);
+    assert.equal(track.rects[3], selRight2[0]);
+
+    // Select to 2 to the right of the 2nd b.
+    var selRightOfRight = new Selection();
+    ret = track.addItemNearToProvidedEventToSelection(
+        selRight[0], 1, selRightOfRight);
+    assert.isTrue(ret);
+    assert.equal(track.rects[3], selRightOfRight[0]);
+
+    // Select to the right of the rightmost slice.
+    var selNone = new Selection();
+    ret = track.addItemNearToProvidedEventToSelection(
+        selRightOfRight[0], 1, selNone);
+    assert.isFalse(ret);
+    assert.equal(0, selNone.length);
+
+    // Select A and then select left.
+    var sel = new Selection();
+    track.addAllObjectsMatchingFilterToSelection(
+        new tv.c.TitleOrCategoryFilter('a'), sel);
+    var ret;
+
+    selNone = new Selection();
+    ret = track.addItemNearToProvidedEventToSelection(sel[0], -1, selNone);
+    assert.isFalse(ret);
+    assert.equal(0, selNone.length);
+  });
+
+  test('rectTrackAddClosestEventToSelection', function() {
+    var track = monkeyPatchTrack(new RectTrack(new tv.c.TimelineViewport()));
+    track.rects = [
+      new Slice('', 'a', 0, 1, {}, 1),
+      new Slice('', 'b', 1, 2.1, {}, 4.8),
+      new Slice('', 'b', 1, 7, {}, 0.5),
+      new Slice('', 'c', 2, 7.6, {}, 0.4)
+    ];
+
+    // Before with not range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(0, 0, 0, 0, sel);
+    assert.equal(0, sel.length);
+
+    // Before with negative range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(1.5, -10, 0, 0, sel);
+    assert.equal(0, sel.length);
+
+    // Before first slice.
+    var sel = new Selection();
+    track.addClosestEventToSelection(0.5, 1, 0, 0, sel);
+    assert.equal(1, sel.length);
+    assert.equal(track.rects[0], sel[0]);
+
+    // Within first slice closer to start.
+    var sel = new Selection();
+    track.addClosestEventToSelection(1.3, 1, 0, 0, sel);
+    assert.equal(track.rects[0], sel[0]);
+
+    // Between slices with good range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(2.08, 3, 0, 0, sel);
+    assert.equal(track.rects[1], sel[0]);
+
+    // Between slices with bad range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(2.05, 0.03, 0, 0, sel);
+    assert.equal(0, sel.length);
+
+    // Within slice closer to end.
+    var sel = new Selection();
+    track.addClosestEventToSelection(6, 100, 0, 0, sel);
+    assert.equal(track.rects[1], sel[0]);
+
+    // Within slice with bad range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(1.8, 0.1, 0, 0, sel);
+    assert.equal(0, sel.length);
+
+    // After last slice with good range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(8.5, 1, 0, 0, sel);
+    assert.equal(track.rects[3], sel[0]);
+
+    // After last slice with bad range.
+    var sel = new Selection();
+    track.addClosestEventToSelection(10, 1, 0, 0, sel);
+    assert.equal(0, sel.length);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/ruler_track.css b/trace-viewer/trace_viewer/core/tracks/ruler_track.css
new file mode 100644
index 0000000..67a04a9
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/ruler_track.css
@@ -0,0 +1,12 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.ruler-track {
+  height: 12px;
+}
+
+.ruler-track.tall-mode {
+  height: 30px;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/ruler_track.html b/trace-viewer/trace_viewer/core/tracks/ruler_track.html
new file mode 100644
index 0000000..410dbcc
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/ruler_track.html
@@ -0,0 +1,356 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/ruler_track.css">
+
+<link rel="import" href="/core/constants.html">
+<link rel="import" href="/core/tracks/track.html">
+<link rel="import" href="/core/tracks/heading_track.html">
+<link rel="import" href="/core/draw_helpers.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays the ruler.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var RulerTrack = tv.b.ui.define('ruler-track', tv.c.tracks.HeadingTrack);
+
+  var logOf10 = Math.log(10);
+  function log10(x) {
+    return Math.log(x) / logOf10;
+  }
+
+  RulerTrack.prototype = {
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('ruler-track');
+      this.strings_secs_ = [];
+      this.strings_msecs_ = [];
+
+      this.viewportChange_ = this.viewportChange_.bind(this);
+      viewport.addEventListener('change', this.viewportChange_);
+
+    },
+
+    detach: function() {
+      tv.c.tracks.HeadingTrack.prototype.detach.call(this);
+      this.viewport.removeEventListener('change',
+                                        this.viewportChange_);
+    },
+
+    viewportChange_: function() {
+      if (this.viewport.interestRange.isEmpty)
+        this.classList.remove('tall-mode');
+      else
+        this.classList.add('tall-mode');
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      switch (type) {
+        case tv.c.tracks.DrawType.GRID:
+          this.drawGrid_(viewLWorld, viewRWorld);
+          break;
+        case tv.c.tracks.DrawType.MARKERS:
+          if (!this.viewport.interestRange.isEmpty)
+            this.viewport.interestRange.draw(this.context(),
+                                             viewLWorld, viewRWorld);
+          break;
+      }
+    },
+
+    drawGrid_: function(viewLWorld, viewRWorld) {
+      var ctx = this.context();
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var canvasBounds = ctx.canvas.getBoundingClientRect();
+      var trackBounds = this.getBoundingClientRect();
+      var width = canvasBounds.width * pixelRatio;
+      var height = trackBounds.height * pixelRatio;
+
+      var hasInterestRange = !this.viewport.interestRange.isEmpty;
+
+      var rulerHeight = hasInterestRange ? (height * 2) / 5 : height;
+
+      var vp = this.viewport;
+      var dt = vp.currentDisplayTransform;
+
+      var idealMajorMarkDistancePix = 150 * pixelRatio;
+      var idealMajorMarkDistanceWorld =
+          dt.xViewVectorToWorld(idealMajorMarkDistancePix);
+
+      var majorMarkDistanceWorld;
+
+      // The conservative guess is the nearest enclosing 0.1, 1, 10, 100, etc.
+      var conservativeGuess =
+          Math.pow(10, Math.ceil(log10(idealMajorMarkDistanceWorld)));
+
+      // Once we have a conservative guess, consider things that evenly add up
+      // to the conservative guess, e.g. 0.5, 0.2, 0.1 Pick the one that still
+      // exceeds the ideal mark distance.
+      var divisors = [10, 5, 2, 1];
+      for (var i = 0; i < divisors.length; ++i) {
+        var tightenedGuess = conservativeGuess / divisors[i];
+        if (dt.xWorldVectorToView(tightenedGuess) < idealMajorMarkDistancePix)
+          continue;
+        majorMarkDistanceWorld = conservativeGuess / divisors[i - 1];
+        break;
+      }
+
+      var unit;
+      var unitDivisor;
+      var tickLabels = undefined;
+      if (majorMarkDistanceWorld < 100) {
+        unit = 'ms';
+        unitDivisor = 1;
+        tickLabels = this.strings_msecs_;
+      } else {
+        unit = 's';
+        unitDivisor = 1000;
+        tickLabels = this.strings_secs_;
+      }
+
+      var numTicksPerMajor = 5;
+      var minorMarkDistanceWorld = majorMarkDistanceWorld / numTicksPerMajor;
+      var minorMarkDistancePx = dt.xWorldVectorToView(minorMarkDistanceWorld);
+
+      var firstMajorMark =
+          Math.floor(viewLWorld / majorMarkDistanceWorld) *
+              majorMarkDistanceWorld;
+
+      var minorTickH = Math.floor(rulerHeight * 0.25);
+
+      ctx.save();
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      ctx.lineWidth = Math.round(pixelRatio);
+
+      // Apply subpixel translate to get crisp lines.
+      // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/
+      var crispLineCorrection = (ctx.lineWidth % 2) / 2;
+      ctx.translate(crispLineCorrection, -crispLineCorrection);
+
+      ctx.fillStyle = 'rgb(0, 0, 0)';
+      ctx.strokeStyle = 'rgb(0, 0, 0)';
+      ctx.textAlign = 'left';
+      ctx.textBaseline = 'top';
+
+      ctx.font = (9 * pixelRatio) + 'px sans-serif';
+
+      vp.majorMarkPositions = [];
+
+      // Each iteration of this loop draws one major mark
+      // and numTicksPerMajor minor ticks.
+      //
+      // Rendering can't be done in world space because canvas transforms
+      // affect line width. So, do the conversions manually.
+      ctx.beginPath();
+      for (var curX = firstMajorMark;
+           curX < viewRWorld;
+           curX += majorMarkDistanceWorld) {
+
+        var curXView = Math.floor(dt.xWorldToView(curX));
+
+        var unitValue = curX / unitDivisor;
+        var roundedUnitValue = Math.floor(unitValue * 100000) / 100000;
+
+        if (!tickLabels[roundedUnitValue])
+          tickLabels[roundedUnitValue] = roundedUnitValue + ' ' + unit;
+        ctx.fillText(tickLabels[roundedUnitValue],
+                     curXView + (2 * pixelRatio), 0);
+
+        vp.majorMarkPositions.push(curXView);
+
+        // Major mark
+        tv.c.drawLine(ctx, curXView, 0, curXView, rulerHeight);
+
+        // Minor marks
+        for (var i = 1; i < numTicksPerMajor; ++i) {
+          var xView = Math.floor(curXView + minorMarkDistancePx * i);
+          tv.c.drawLine(ctx,
+              xView, rulerHeight - minorTickH,
+              xView, rulerHeight);
+        }
+      }
+
+      // Draw bottom bar.
+      ctx.strokeStyle = 'rgb(0, 0, 0)';
+      tv.c.drawLine(ctx, 0, height, width, height);
+      ctx.stroke();
+
+      // Give distance between directly adjacent markers.
+      if (!hasInterestRange)
+        return;
+
+      // Draw middle bar.
+      tv.c.drawLine(ctx, 0, rulerHeight, width, rulerHeight);
+      ctx.stroke();
+
+      // Distance Variables.
+      var displayDistance;
+      var displayTextColor = 'rgb(0,0,0)';
+
+      // Arrow Variables.
+      var arrowSpacing = 10 * pixelRatio;
+      var arrowColor = 'rgb(128,121,121)';
+      var arrowPosY = rulerHeight * 1.75;
+      var arrowWidthView = 3 * pixelRatio;
+      var arrowLengthView = 10 * pixelRatio;
+      var spaceForArrowsView = 2 * (arrowWidthView + arrowSpacing);
+
+      ctx.textBaseline = 'middle';
+      ctx.font = (14 * pixelRatio) + 'px sans-serif';
+      var textPosY = arrowPosY;
+
+      var interestRange = vp.interestRange;
+
+      // If the range is zero, draw it's min timestamp next to the line.
+      if (interestRange.range === 0) {
+        var markerWorld = interestRange.min;
+        var markerView = dt.xWorldToView(markerWorld);
+        var displayValue = markerWorld / unitDivisor;
+        displayValue = Math.abs((Math.floor(displayValue * 1000) / 1000));
+
+        var textToDraw = displayValue + ' ' + unit;
+        var textLeftView = markerView + 4 * pixelRatio;
+        var textWidthView = ctx.measureText(textToDraw).width;
+
+        // Put text to the left in case it gets cut off.
+        if (textLeftView + textWidthView > width)
+          textLeftView = markerView - 4 * pixelRatio - textWidthView;
+
+        ctx.fillStyle = displayTextColor;
+        ctx.fillText(textToDraw, textLeftView, textPosY);
+        return;
+      }
+
+      var leftMarker = interestRange.min;
+      var rightMarker = interestRange.max;
+
+      var leftMarkerView = dt.xWorldToView(leftMarker);
+      var rightMarkerView = dt.xWorldToView(rightMarker);
+
+      var distanceBetweenMarkers = interestRange.range;
+      var distanceBetweenMarkersView =
+          dt.xWorldVectorToView(distanceBetweenMarkers);
+      var positionInMiddleOfMarkersView =
+          leftMarkerView + (distanceBetweenMarkersView / 2);
+
+      // Determine units.
+      if (distanceBetweenMarkers < 100) {
+        unit = 'ms';
+        unitDivisor = 1;
+      } else {
+        unit = 's';
+        unitDivisor = 1000;
+      }
+
+      // Calculate display value to print.
+      displayDistance = distanceBetweenMarkers / unitDivisor;
+      var roundedDisplayDistance =
+          Math.abs((Math.floor(displayDistance * 1000) / 1000));
+      var textToDraw = roundedDisplayDistance + ' ' + unit;
+      var textWidthView = ctx.measureText(textToDraw).width;
+      var spaceForArrowsAndTextView =
+          textWidthView + spaceForArrowsView + arrowSpacing;
+
+      // Set text positions.
+      var textLeftView = positionInMiddleOfMarkersView - textWidthView / 2;
+      var textRightView = textLeftView + textWidthView;
+
+      if (spaceForArrowsAndTextView > distanceBetweenMarkersView) {
+        // Print the display distance text right of the 2 markers.
+        textLeftView = rightMarkerView + 2 * arrowSpacing;
+
+        // Put text to the left in case it gets cut off.
+        if (textLeftView + textWidthView > width)
+          textLeftView = leftMarkerView - 2 * arrowSpacing - textWidthView;
+
+        ctx.fillStyle = displayTextColor;
+        ctx.fillText(textToDraw, textLeftView, textPosY);
+
+        // Draw the arrows pointing from outside in and a line in between.
+        ctx.strokeStyle = arrowColor;
+        ctx.beginPath();
+        tv.c.drawLine(ctx, leftMarkerView, arrowPosY, rightMarkerView,
+            arrowPosY);
+        ctx.stroke();
+
+        ctx.fillStyle = arrowColor;
+        tv.c.drawArrow(ctx,
+            leftMarkerView - 1.5 * arrowSpacing, arrowPosY,
+            leftMarkerView, arrowPosY,
+            arrowLengthView, arrowWidthView);
+        tv.c.drawArrow(ctx,
+            rightMarkerView + 1.5 * arrowSpacing, arrowPosY,
+            rightMarkerView, arrowPosY,
+            arrowLengthView, arrowWidthView);
+
+      } else if (spaceForArrowsView <= distanceBetweenMarkersView) {
+        var leftArrowStart;
+        var rightArrowStart;
+        if (spaceForArrowsAndTextView <= distanceBetweenMarkersView) {
+          // Print the display distance text.
+          ctx.fillStyle = displayTextColor;
+          ctx.fillText(textToDraw, textLeftView, textPosY);
+
+          leftArrowStart = textLeftView - arrowSpacing;
+          rightArrowStart = textRightView + arrowSpacing;
+        } else {
+          leftArrowStart = positionInMiddleOfMarkersView;
+          rightArrowStart = positionInMiddleOfMarkersView;
+        }
+
+        // Draw the arrows pointing inside out.
+        ctx.strokeStyle = arrowColor;
+        ctx.fillStyle = arrowColor;
+        tv.c.drawArrow(ctx,
+            leftArrowStart, arrowPosY,
+            leftMarkerView, arrowPosY,
+            arrowLengthView, arrowWidthView);
+        tv.c.drawArrow(ctx,
+            rightArrowStart, arrowPosY,
+            rightMarkerView, arrowPosY,
+            arrowLengthView, arrowWidthView);
+      }
+
+      ctx.restore();
+    },
+
+    /**
+     * Adds items intersecting the given range to a selection.
+     * @param {number} loVX Lower X bound of the interval to search, in
+     *     viewspace.
+     * @param {number} hiVX Upper X bound of the interval to search, in
+     *     viewspace.
+     * @param {number} loVY Lower Y bound of the interval to search, in
+     *     viewspace.
+     * @param {number} hiVY Upper Y bound of the interval to search, in
+     *     viewspace.
+     * @param {Selection} selection Selection to which to add results.
+     */
+    addIntersectingItemsInRangeToSelection: function(
+        loVX, hiVX, loY, hiY, selection) {
+      // Does nothing. There's nothing interesting to pick on the ruler
+      // track.
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+    }
+  };
+
+  return {
+    RulerTrack: RulerTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/ruler_track_test.html b/trace-viewer/trace_viewer/core/tracks/ruler_track_test.html
new file mode 100644
index 0000000..d44c8a6
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/ruler_track_test.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+<link rel="import" href="/core/tracks/ruler_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var div = document.createElement('div');
+
+    var viewport = new tv.c.TimelineViewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = tv.c.tracks.RulerTrack(viewport);
+    drawingContainer.appendChild(track);
+    this.addHTMLOutput(div);
+
+    drawingContainer.invalidate();
+
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.setPanAndScale(0, track.clientWidth / 1000);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/sample_track.html b/trace-viewer/trace_viewer/core/tracks/sample_track.html
new file mode 100644
index 0000000..3dc09d5
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/sample_track.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays an array of Sample objects.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var SampleTrack = tv.b.ui.define(
+      'sample-track', tv.c.tracks.RectTrack);
+
+  SampleTrack.prototype = {
+
+    __proto__: tv.c.tracks.RectTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.RectTrack.prototype.decorate.call(this, viewport);
+    },
+
+    get samples() {
+      return this.rects;
+    },
+
+    set samples(samples) {
+      this.rects = samples;
+    },
+
+    getModelEventFromItem: function(sample) {
+      return sample;
+    }
+  };
+
+  return {
+    SampleTrack: SampleTrack
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/sample_track_test.html b/trace-viewer/trace_viewer/core/tracks/sample_track_test.html
new file mode 100644
index 0000000..3c10621
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/sample_track_test.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/trace_model/sample.html">
+<link rel="import" href="/core/trace_model/stack_frame.html">
+<link rel="import" href="/core/tracks/sample_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Selection = tv.c.Selection;
+  var SampleTrack = tv.c.tracks.SampleTrack;
+  var Sample = tv.c.trace_model.Sample;
+  var StackFrame = tv.c.trace_model.StackFrame;
+
+  test('getModelEventFromItemTest', function() {
+    var track = new SampleTrack(new tv.c.TimelineViewport());
+    var fA = new StackFrame(undefined, 1, 'cat', 'a', 7);
+    var sample = new Sample(undefined, undefined, 'instructions_retired',
+                            10, fA, 10);
+    track.samples = [sample];
+    var me0 = track.getModelEventFromItem(track.samples[0]);
+    assert.equal(me0, sample);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/slice_group_track.html b/trace-viewer/trace_viewer/core/tracks/slice_group_track.html
new file mode 100644
index 0000000..7c6ab7a
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/slice_group_track.html
@@ -0,0 +1,164 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/multi_row_track.html">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays a SliceGroup.
+   * @constructor
+   * @extends {MultiRowTrack}
+   */
+  var SliceGroupTrack = tv.b.ui.define(
+      'slice-group-track', tv.c.tracks.MultiRowTrack);
+
+  SliceGroupTrack.prototype = {
+
+    __proto__: tv.c.tracks.MultiRowTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.MultiRowTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('slice-group-track');
+      this.group_ = undefined;
+      // Set the collapse threshold so we don't collapse by default, but the
+      // user can explicitly collapse if they want it.
+      this.defaultToCollapsedWhenSubRowCountMoreThan = 100;
+    },
+
+    addSubTrack_: function(slices) {
+      var track = new tv.c.tracks.SliceTrack(this.viewport);
+      track.slices = slices;
+      this.appendChild(track);
+      return track;
+    },
+
+    get group() {
+      return this.group_;
+    },
+
+    set group(group) {
+      this.group_ = group;
+      this.setItemsToGroup(this.group_.slices, this.group_);
+    },
+
+    get eventContainer() {
+      return this.group;
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      containerToTrackMap.addContainer(this.group, this);
+    },
+
+    /**
+     * Breaks up the list of slices into N rows, each of which is a list of
+     * slices that are non overlapping.
+     */
+    buildSubRows_: function(slices) {
+      // This function works by walking through slices by start time.
+      //
+      // The basic idea here is to insert each slice as deep into the subrow
+      // list as it can go such that every subSlice is fully contained by its
+      // parent slice.
+      //
+      // Visually, if we start with this:
+      //  0:  [    a       ]
+      //  1:    [  b  ]
+      //  2:    [c][d]
+      //
+      // To place this slice:
+      //               [e]
+      // We first check row 2's last item, [d]. [e] wont fit into [d] (they dont
+      // even intersect). So we go to row 1. That gives us [b], and [d] wont fit
+      // into that either. So, we go to row 0 and its last slice, [a]. That can
+      // completely contain [e], so that means we should add [e] as a subchild
+      // of [a]. That puts it on row 1, yielding:
+      //  0:  [    a       ]
+      //  1:    [  b  ][e]
+      //  2:    [c][d]
+      //
+      // If we then get this slice:
+      //                      [f]
+      // We do the same deepest-to-shallowest walk of the subrows trying to fit
+      // it. This time, it doesn't fit in any open slice. So, we simply append
+      // it to row 0:
+      //  0:  [    a       ]  [f]
+      //  1:    [  b  ][e]
+      //  2:    [c][d]
+      if (!slices.length)
+        return [];
+
+      var ops = [];
+      for (var i = 0; i < slices.length; i++) {
+        if (slices[i].subSlices)
+          slices[i].subSlices.splice(0,
+                                     slices[i].subSlices.length);
+        ops.push(i);
+      }
+
+      ops.sort(function(ix, iy) {
+        var x = slices[ix];
+        var y = slices[iy];
+        if (x.start != y.start)
+          return x.start - y.start;
+
+        // Elements get inserted into the slices array in order of when the
+        // slices start. Because slices must be properly nested, we break
+        // start-time ties by assuming that the elements appearing earlier in
+        // the slices array (and thus ending earlier) start earlier.
+        return ix - iy;
+      });
+
+      var subRows = [[]];
+      this.badSlices_ = [];  // TODO(simonjam): Connect this again.
+
+      for (var i = 0; i < ops.length; i++) {
+        var op = ops[i];
+        var slice = slices[op];
+
+        // Try to fit the slice into the existing subrows.
+        var inserted = false;
+        for (var j = subRows.length - 1; j >= 0; j--) {
+          if (subRows[j].length == 0)
+            continue;
+
+          var insertedSlice = subRows[j][subRows[j].length - 1];
+          if (slice.start < insertedSlice.start) {
+            this.badSlices_.push(slice);
+            inserted = true;
+          }
+          if (insertedSlice.bounds(slice)) {
+            // Insert it into subRow j + 1.
+            while (subRows.length <= j + 1)
+              subRows.push([]);
+            subRows[j + 1].push(slice);
+            if (insertedSlice.subSlices)
+              insertedSlice.subSlices.push(slice);
+            inserted = true;
+            break;
+          }
+        }
+        if (inserted)
+          continue;
+
+        // Append it to subRow[0] as a root.
+        subRows[0].push(slice);
+      }
+
+      return subRows;
+    }
+  };
+
+  return {
+    SliceGroupTrack: SliceGroupTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/slice_group_track_test.html b/trace-viewer/trace_viewer/core/tracks/slice_group_track_test.html
new file mode 100644
index 0000000..e9d0587
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/slice_group_track_test.html
@@ -0,0 +1,291 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/trace_model/slice_group.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ProcessTrack = tv.c.tracks.ProcessTrack;
+  var ThreadTrack = tv.c.tracks.ThreadTrack;
+  var SliceGroup = tv.c.trace_model.SliceGroup;
+  var SliceGroupTrack = tv.c.tracks.SliceGroupTrack;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  test('subRowBuilderBasic', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+    var sA = group.pushSlice(newSliceNamed('a', 1, 2));
+    var sB = group.pushSlice(newSliceNamed('a', 3, 1));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 1);
+    assert.equal(subRows[0].length, 2);
+    assert.deepEqual(subRows[0], [sA, sB]);
+  });
+
+  test('subRowBuilderBasic2', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+    var sA = group.pushSlice(newSliceNamed('a', 1, 4));
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 2);
+    assert.equal(subRows[0].length, 1);
+    assert.equal(subRows[1].length, 1);
+    assert.deepEqual(subRows[0], [sA]);
+    assert.deepEqual(subRows[1], [sB]);
+  });
+
+  test('subRowBuilderNestedExactly', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    var sB = group.pushSlice(newSliceNamed('b', 1, 4));
+    var sA = group.pushSlice(newSliceNamed('a', 1, 4));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 2);
+    assert.equal(subRows[0].length, 1);
+    assert.equal(subRows[1].length, 1);
+    assert.deepEqual(subRows[0], [sB]);
+    assert.deepEqual(subRows[1], [sA]);
+  });
+
+  test('subRowBuilderInstantEvents', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    var sA = group.pushSlice(newSliceNamed('a', 1, 0));
+    var sB = group.pushSlice(newSliceNamed('b', 2, 0));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 1);
+    assert.equal(subRows[0].length, 2);
+    assert.deepEqual(subRows[0], [sA, sB]);
+  });
+
+  test('subRowBuilderTwoInstantEvents', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    var sA = group.pushSlice(newSliceNamed('a', 1, 0));
+    var sB = group.pushSlice(newSliceNamed('b', 1, 0));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 2);
+    assert.deepEqual(subRows[0], [sA]);
+    assert.deepEqual(subRows[1], [sB]);
+  });
+
+  test('subRowBuilderOutOfOrderAddition', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    // Pattern being tested:
+    // [    a     ][   b   ]
+    // Where insertion is done backward.
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+    var sA = group.pushSlice(newSliceNamed('a', 1, 2));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 1);
+    assert.equal(subRows[0].length, 2);
+    assert.deepEqual(subRows[0], [sA, sB]);
+  });
+
+  test('subRowBuilderOutOfOrderAddition2', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    // Pattern being tested:
+    // [    a     ]
+    //   [  b   ]
+    // Where insertion is done backward.
+    var sB = group.pushSlice(newSliceNamed('b', 3, 1));
+    var sA = group.pushSlice(newSliceNamed('a', 1, 5));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 2);
+    assert.equal(subRows[0].length, 1);
+    assert.equal(subRows[1].length, 1);
+    assert.deepEqual(subRows[0], [sA]);
+    assert.deepEqual(subRows[1], [sB]);
+  });
+
+  test('subRowBuilderOnNestedZeroLength', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    // Pattern being tested:
+    // [    a    ]
+    // [  b1 ]  []<- b2 where b2.duration = 0 and b2.end == a.end.
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB1 = group.pushSlice(newSliceNamed('b1', 1, 2));
+    var sB2 = group.pushSlice(newSliceNamed('b2', 4, 0));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 2);
+    assert.deepEqual(subRows[0], [sA]);
+    assert.deepEqual(subRows[1], [sB1, sB2]);
+  });
+
+  test('subRowBuilderOnGroup1', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    // Pattern being tested:
+    // [    a     ]   [  c   ]
+    //   [  b   ]
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB = group.pushSlice(newSliceNamed('b', 1.5, 1));
+    var sC = group.pushSlice(newSliceNamed('c', 5, 0));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+    var subRows = track.subRows;
+
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 2);
+    assert.deepEqual(subRows[0], [sA, sC]);
+    assert.deepEqual(subRows[1], [sB]);
+  });
+
+  test('subRowBuilderOnGroup2', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    // Pattern being tested:
+    // [    a     ]   [  d   ]
+    //   [  b   ]
+    //    [ c ]
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB = group.pushSlice(newSliceNamed('b', 1.5, 1));
+    var sC = group.pushSlice(newSliceNamed('c', 1.75, 0.5));
+    var sD = group.pushSlice(newSliceNamed('c', 5, 0.25));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+
+    var subRows = track.subRows;
+    assert.equal(track.badSlices_.length, 0);
+    assert.equal(subRows.length, 3);
+    assert.deepEqual(subRows[0], [sA, sD]);
+    assert.deepEqual(subRows[1], [sB]);
+    assert.deepEqual(subRows[2], [sC]);
+  });
+
+  test('trackFiltering', function() {
+    var m = new tv.c.TraceModel();
+    var t1 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    var group = t1.sliceGroup;
+
+    var sA = group.pushSlice(newSliceNamed('a', 1, 3));
+    var sB = group.pushSlice(newSliceNamed('b', 1.5, 1));
+
+    var track = new SliceGroupTrack(new tv.c.TimelineViewport());
+    track.group = group;
+
+    assert.equal(track.subRows.length, 2);
+    assert.isTrue(track.hasVisibleContent);
+  });
+
+test('sliceGroupContainerMap', function() {
+    var vp = new tv.c.TimelineViewport();
+    var containerToTrack = vp.containerToTrackObj;
+    var model = new tv.c.TraceModel();
+    var process = model.getOrCreateProcess(123);
+    var thread = process.getOrCreateThread(456);
+    var group = new SliceGroup(thread);
+
+    var processTrack = new ProcessTrack(vp);
+    var threadTrack = new ThreadTrack(vp);
+    var groupTrack = new SliceGroupTrack(vp);
+    processTrack.process = process;
+    threadTrack.thread = thread;
+    groupTrack.group = group;
+    processTrack.appendChild(threadTrack);
+    threadTrack.appendChild(groupTrack);
+
+    assert.equal(processTrack.eventContainer, process);
+    assert.equal(threadTrack.eventContainer, thread);
+    assert.equal(groupTrack.eventContainer, group);
+
+    assert.isUndefined(containerToTrack.getTrackByStableId('123'));
+    assert.isUndefined(containerToTrack.getTrackByStableId('123.456'));
+    assert.isUndefined(
+        containerToTrack.getTrackByStableId('123.456.SliceGroup'));
+
+    vp.modelTrackContainer = {
+      addContainersToTrackMap: function(containerToTrackObj) {
+        processTrack.addContainersToTrackMap(containerToTrackObj);
+      },
+      addEventListener: function() {}
+    };
+    vp.rebuildContainerToTrackMap();
+
+    // Check that all tracks call childs' addContainersToTrackMap()
+    // by checking the resulting map.
+    assert.equal(containerToTrack.getTrackByStableId('123'), processTrack);
+    assert.equal(containerToTrack.getTrackByStableId('123.456'), threadTrack);
+    assert.equal(containerToTrack.getTrackByStableId('123.456.SliceGroup'),
+        groupTrack);
+
+    // Check the track's eventContainer getter.
+    assert.equal(processTrack.eventContainer, process);
+    assert.equal(threadTrack.eventContainer, thread);
+    assert.equal(groupTrack.eventContainer, group);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/slice_track.html b/trace-viewer/trace_viewer/core/tracks/slice_track.html
new file mode 100644
index 0000000..4bb66dd
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/slice_track.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/rect_track.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays an array of Slice objects.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var SliceTrack = tv.b.ui.define(
+      'slice-track', tv.c.tracks.RectTrack);
+
+  SliceTrack.prototype = {
+
+    __proto__: tv.c.tracks.RectTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.RectTrack.prototype.decorate.call(this, viewport);
+    },
+
+    get slices() {
+      return this.rects;
+    },
+
+    set slices(slices) {
+      this.rects = slices;
+    },
+
+    getModelEventFromItem: function(slice) {
+      return slice;
+    }
+  };
+
+  return {
+    SliceTrack: SliceTrack
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/slice_track_test.html b/trace-viewer/trace_viewer/core/tracks/slice_track_test.html
new file mode 100644
index 0000000..a6e655e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/slice_track_test.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/trace_model/slice.html">
+<link rel="import" href="/core/tracks/slice_track.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Selection = tv.c.Selection;
+  var SliceTrack = tv.c.tracks.SliceTrack;
+  var Slice = tv.c.trace_model.Slice;
+
+  test('getModelEventFromItem', function() {
+    var track = new SliceTrack(new tv.c.TimelineViewport());
+    var slice = new Slice('', 'a', 0, 1, {}, 1);
+    track.slices = [slice];
+    var sel = new Selection();
+    var me0 = track.getModelEventFromItem(track.slices[0]);
+    assert.equal(slice, me0);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/spacing_track.css b/trace-viewer/trace_viewer/core/tracks/spacing_track.css
new file mode 100644
index 0000000..094eee0
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/spacing_track.css
@@ -0,0 +1,7 @@
+/* Copyright (c) 2013 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+.spacing-track {
+  height: 4px;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/spacing_track.html b/trace-viewer/trace_viewer/core/tracks/spacing_track.html
new file mode 100644
index 0000000..f27cc6c
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/spacing_track.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/spacing_track.css">
+
+<link rel="import" href="/core/tracks/heading_track.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * @constructor
+   */
+  var SpacingTrack = tv.b.ui.define('spacing-track',
+                                    tv.c.tracks.HeadingTrack);
+
+  SpacingTrack.prototype = {
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('spacing-track');
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+    }
+  };
+
+  return {
+    SpacingTrack: SpacingTrack
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/core/tracks/stacked_bars_track.html b/trace-viewer/trace_viewer/core/tracks/stacked_bars_track.html
new file mode 100644
index 0000000..c78b39e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/stacked_bars_track.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/heading_track.html">
+<link rel="import" href="/base/ui.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * A track that displays traces as stacked bars.
+   * @constructor
+   * @extends {HeadingTrack}
+   */
+  var StackedBarsTrack = tv.b.ui.define(
+      'stacked-bars-track', tv.c.tracks.HeadingTrack);
+
+  StackedBarsTrack.prototype = {
+
+    __proto__: tv.c.tracks.HeadingTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.HeadingTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('stacked-bars-track');
+      this.objectInstance_ = null;
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      var objectSnapshots = this.objectInstance_.snapshots;
+      objectSnapshots.forEach(function(obj) {
+        eventToTrackMap.addEvent(obj, this);
+      }, this);
+    },
+
+    /**
+     * Used to hit-test clicks in the graph.
+     */
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+      function onSnapshot(snapshot) {
+        selection.push(snapshot);
+      }
+
+      var snapshots = this.objectInstance_.snapshots;
+      var maxBounds = this.objectInstance_.parent.model.bounds.max;
+
+      tv.b.iterateOverIntersectingIntervals(
+          snapshots,
+          function(x) { return x.ts; },
+          function(x, i) {
+            if (i == snapshots.length - 1) {
+              if (snapshots.length == 1)
+                return maxBounds;
+
+              return snapshots[i].ts - snapshots[i - 1].ts;
+            }
+
+            return snapshots[i + 1].ts - snapshots[i].ts;
+          },
+          loWX, hiWX,
+          onSnapshot);
+    },
+
+    /**
+     * Add the item to the left or right of the provided item, if any, to the
+     * selection.
+     * @param {slice} The current slice.
+     * @param {Number} offset Number of slices away from the object to look.
+     * @param {Selection} selection The selection to add an event to,
+     * if found.
+     * @return {boolean} Whether an event was found.
+     * @private
+     */
+    addItemNearToProvidedEventToSelection: function(event, offset, selection) {
+      if (!(event instanceof tv.c.trace_model.ObjectSnapshot))
+        throw new Error('Unrecognized event');
+      var objectSnapshots = this.objectInstance_.snapshots;
+      var index = objectSnapshots.indexOf(event);
+      var newIndex = index + offset;
+      if (newIndex >= 0 && newIndex < objectSnapshots.length) {
+        selection.push(objectSnapshots[newIndex]);
+        return true;
+      }
+      return false;
+    },
+
+    addAllObjectsMatchingFilterToSelection: function(filter, selection) {
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      var snapshot = tv.b.findClosestElementInSortedArray(
+          this.objectInstance_.snapshots,
+          function(x) { return x.ts; },
+          worldX,
+          worldMaxDist);
+
+      if (!snapshot)
+        return;
+
+      selection.push(snapshot);
+    }
+  };
+
+  return {
+    StackedBarsTrack: StackedBarsTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/thread_track.css b/trace-viewer/trace_viewer/core/tracks/thread_track.css
new file mode 100644
index 0000000..c42cee0
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/thread_track.css
@@ -0,0 +1,10 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.thread-track {
+  -webkit-box-orient: vertical;
+  display: -webkit-box;
+  position: relative;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/thread_track.html b/trace-viewer/trace_viewer/core/tracks/thread_track.html
new file mode 100644
index 0000000..2278c4b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/thread_track.html
@@ -0,0 +1,162 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="stylesheet" href="/core/tracks/thread_track.css">
+
+<link rel="import" href="/core/tracks/container_track.html">
+<link rel="import" href="/core/tracks/sample_track.html">
+<link rel="import" href="/core/tracks/slice_track.html">
+<link rel="import" href="/core/tracks/slice_group_track.html">
+<link rel="import" href="/core/tracks/async_slice_group_track.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/iteration_helpers.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * Visualizes a Thread using a series of SliceTracks.
+   * @constructor
+   */
+  var ThreadTrack = tv.b.ui.define('thread-track',
+                                   tv.c.tracks.ContainerTrack);
+  ThreadTrack.prototype = {
+    __proto__: tv.c.tracks.ContainerTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('thread-track');
+    },
+
+    get thread() {
+      return this.thread_;
+    },
+
+    set thread(thread) {
+      this.thread_ = thread;
+      this.updateContents_();
+    },
+
+    get hasVisibleContent() {
+      return this.tracks_.length > 0;
+    },
+
+    get eventContainer() {
+      return this.thread;
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      containerToTrackMap.addContainer(this.thread, this);
+      for (var i = 0; i < this.childNodes.length; ++i)
+        this.childNodes[i].addContainersToTrackMap(containerToTrackMap);
+    },
+
+    updateContents_: function() {
+      this.detach();
+
+      if (!this.thread_)
+        return;
+
+      this.heading = this.thread_.userFriendlyName + ': ';
+      this.tooltip = this.thread_.userFriendlyDetails;
+
+      if (this.thread_.asyncSliceGroup.length)
+        this.appendAsyncSliceTracks_();
+
+      this.appendThreadSamplesTracks_();
+
+      if (this.thread_.timeSlices) {
+        var timeSlicesTrack = new tv.c.tracks.SliceTrack(this.viewport);
+        timeSlicesTrack.heading = '';
+        timeSlicesTrack.height = tv.c.THIN_SLICE_HEIGHT + 'px';
+        timeSlicesTrack.slices = this.thread_.timeSlices;
+        if (timeSlicesTrack.hasVisibleContent)
+          this.appendChild(timeSlicesTrack);
+      }
+
+      if (this.thread_.sliceGroup.length) {
+        var track = new tv.c.tracks.SliceGroupTrack(this.viewport);
+        track.heading = this.thread_.userFriendlyName;
+        track.tooltip = this.thread_.userFriendlyDetails;
+        track.group = this.thread_.sliceGroup;
+        if (track.hasVisibleContent)
+          this.appendChild(track);
+      }
+    },
+
+    appendAsyncSliceTracks_: function() {
+      var subGroups = this.thread_.asyncSliceGroup.viewSubGroups;
+      subGroups.forEach(function(subGroup) {
+        var asyncTrack = new tv.c.tracks.AsyncSliceGroupTrack(this.viewport);
+        var title = subGroup.slices[0].viewSubGroupTitle;
+        asyncTrack.group = subGroup;
+        asyncTrack.heading = title;
+        if (asyncTrack.hasVisibleContent)
+          this.appendChild(asyncTrack);
+      }, this);
+    },
+
+    appendThreadSamplesTracks_: function() {
+      var threadSamples = this.thread_.samples;
+      if (threadSamples === undefined || threadSamples.length === 0)
+        return;
+      var samplesByTitle = {};
+      threadSamples.forEach(function(sample) {
+        if (samplesByTitle[sample.title] === undefined)
+          samplesByTitle[sample.title] = [];
+        samplesByTitle[sample.title].push(sample);
+      });
+
+      var sampleTitles = tv.b.dictionaryKeys(samplesByTitle);
+      sampleTitles.sort();
+
+      sampleTitles.forEach(function(sampleTitle) {
+        var samples = samplesByTitle[sampleTitle];
+        var samplesTrack = new tv.c.tracks.SampleTrack(this.viewport);
+        samplesTrack.group = this.thread_;
+        samplesTrack.samples = samples;
+        samplesTrack.heading = this.thread_.userFriendlyName + ': ' +
+            sampleTitle;
+        samplesTrack.tooltip = this.thread_.userFriendlyDetails;
+        samplesTrack.selectionGenerator = function() {
+          var selection = new tv.c.Selection();
+          for (var i = 0; i < samplesTrack.samples.length; i++) {
+            selection.push(samplesTrack.samples[i]);
+          }
+          return selection;
+        };
+        this.appendChild(samplesTrack);
+      }, this);
+    },
+
+    collapsedDidChange: function(collapsed) {
+      if (collapsed) {
+        var h = parseInt(this.tracks[0].height);
+        for (var i = 0; i < this.tracks.length; ++i) {
+          if (h > 2) {
+            this.tracks[i].height = Math.floor(h) + 'px';
+          } else {
+            this.tracks[i].style.display = 'none';
+          }
+          h = h * 0.5;
+        }
+      } else {
+        for (var i = 0; i < this.tracks.length; ++i) {
+          this.tracks[i].height = this.tracks[0].height;
+          this.tracks[i].style.display = '';
+        }
+      }
+    }
+  };
+
+  return {
+    ThreadTrack: ThreadTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/thread_track_test.html b/trace-viewer/trace_viewer/core/tracks/thread_track_test.html
new file mode 100644
index 0000000..7d35255
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/thread_track_test.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/trace_model/instant_event.html">
+<link rel="import" href="/core/tracks/thread_track.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var HighlightInstantEvent = tv.c.trace_model.ThreadHighlightInstantEvent;
+  var Process = tv.c.trace_model.Process;
+  var Selection = tv.c.Selection;
+  var StackFrame = tv.c.trace_model.StackFrame;
+  var Sample = tv.c.trace_model.Sample;
+  var Thread = tv.c.trace_model.Thread;
+  var ThreadSlice = tv.c.trace_model.ThreadSlice;
+  var ThreadTrack = tv.c.tracks.ThreadTrack;
+  var Viewport = tv.c.TimelineViewport;
+  var newAsyncSlice = tv.c.test_utils.newAsyncSlice;
+  var newAsyncSliceNamed = tv.c.test_utils.newAsyncSliceNamed;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  test('selectionHitTestingWithThreadTrack', function() {
+    var model = new tv.c.TraceModel();
+    var p1 = model.getOrCreateProcess(1);
+    var t1 = p1.getOrCreateThread(1);
+    t1.sliceGroup.pushSlice(new ThreadSlice('', 'a', 0, 1, {}, 4));
+    t1.sliceGroup.pushSlice(new ThreadSlice('', 'b', 0, 5.1, {}, 4));
+
+    var testEl = document.createElement('div');
+    testEl.appendChild(tv.b.ui.createScopedStyle('heading { width: 100px; }'));
+    testEl.style.width = '600px';
+
+    var viewport = new Viewport(testEl);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    testEl.appendChild(drawingContainer);
+
+    var track = new ThreadTrack(viewport);
+    drawingContainer.appendChild(track);
+    drawingContainer.updateCanvasSizeIfNeeded_();
+    track.thread = t1;
+
+    var y = track.getBoundingClientRect().top;
+    var h = track.getBoundingClientRect().height;
+    var wW = 10;
+    var vW = drawingContainer.canvas.getBoundingClientRect().width;
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, wW, vW);
+    track.viewport.setDisplayTransformImmediately(dt);
+
+    var selection = new Selection();
+    var x = (1.5 / wW) * vW;
+    track.addIntersectingItemsInRangeToSelection(x, x + 1, y, y + 1, selection);
+    assert.equal(t1.sliceGroup.slices[0], selection[0]);
+
+    var selection = new Selection();
+    track.addIntersectingItemsInRangeToSelection(
+        (1.5 / wW) * vW, (1.8 / wW) * vW,
+        y, y + h, selection);
+    assert.equal(t1.sliceGroup.slices[0], selection[0]);
+  });
+
+  test('filterThreadSlices', function() {
+    var model = new tv.c.TraceModel();
+    var thread = new Thread(new Process(model, 7), 1);
+    thread.sliceGroup.pushSlice(newSliceNamed('a', 0, 0));
+    thread.asyncSliceGroup.push(newAsyncSliceNamed('a', 0, 5, t, t));
+
+    var t = new ThreadTrack(new tv.c.TimelineViewport());
+    t.thread = thread;
+
+    assert.equal(t.tracks_.length, 2);
+    assert.instanceOf(t.tracks_[0], tv.c.tracks.AsyncSliceGroupTrack);
+    assert.instanceOf(t.tracks_[1], tv.c.tracks.SliceGroupTrack);
+  });
+
+  test('sampleThreadSlices', function() {
+    var model = new tv.c.TraceModel();
+    var thread;
+    var cpu;
+    model.importTraces([], false, false, function() {
+      cpu = model.kernel.getOrCreateCpu(1);
+      thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+
+      var fA = model.addStackFrame(new StackFrame(
+          undefined, 1, 'cat', 'a', 7));
+      var fAB = model.addStackFrame(new StackFrame(
+          fA, 2, 'cat', 'b', 7));
+      var fABC = model.addStackFrame(new StackFrame(
+          fAB, 3, 'cat', 'c', 7));
+      var fAD = model.addStackFrame(new StackFrame(
+          fA, 4, 'cat', 'd', 7));
+
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    10, fABC, 10));
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    20, fAB, 10));
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    30, fAB, 10));
+      model.samples.push(new Sample(undefined, thread, 'instructions_retired',
+                                    40, fAD, 10));
+
+      model.samples.push(new Sample(undefined, thread, 'page_fault',
+                                    25, fAB, 10));
+      model.samples.push(new Sample(undefined, thread, 'page_fault',
+                                    35, fAD, 10));
+    });
+
+    var t = new ThreadTrack(new tv.c.TimelineViewport());
+    t.thread = thread;
+    assert.equal(t.tracks_.length, 2);
+
+    // Instructions retired
+    var t0 = t.tracks_[0];
+    assert.notEqual(t0.heading.indexOf('instructions_retired'), -1);
+    assert.instanceOf(t0, tv.c.tracks.SampleTrack);
+    assert.equal(t0.samples.length, 4);
+    t0.samples.forEach(function(s) {
+      assert.instanceOf(s, tv.c.trace_model.Sample);
+    });
+
+    // page_fault
+    var t1 = t.tracks_[1];
+    assert.notEqual(t1.heading.indexOf('page_fault'), -1);
+    assert.instanceOf(t1, tv.c.tracks.SampleTrack);
+    assert.equal(t1.samples.length, 2);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/trace_model_track.html b/trace-viewer/trace_viewer/core/tracks/trace_model_track.html
new file mode 100644
index 0000000..ccc208e
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/trace_model_track.html
@@ -0,0 +1,413 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/highlighter.html">
+<link rel="import" href="/core/tracks/container_track.html">
+<link rel="import" href="/core/tracks/kernel_track.html">
+<link rel="import" href="/core/tracks/alert_track.html">
+<link rel="import" href="/core/tracks/memory_dump_track.html">
+<link rel="import" href="/core/tracks/process_track.html">
+<link rel="import" href="/core/draw_helpers.html">
+<link rel="import" href="/base/ui.html">
+
+<style>
+.model-track {
+  -webkit-box-flex: 1;
+}
+</style>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.tracks', function() {
+
+  // TODO(nduca): Move this elsewhere and make it non-hacky.
+  function HackyMultiRowTrack(viewport, model) {
+    var mrt = new tv.c.tracks.MultiRowTrack(viewport);
+    mrt.heading = 'Interactions';
+    mrt.buildSubRows_ = function(slices) {
+      slices.sort(function(x, y) {
+        var r = x.title.localeCompare(y.title);
+        if (r)
+          return r;
+        return x.start - y.start;
+      });
+      return tv.c.tracks.AsyncSliceGroupTrack.prototype.buildSubRows_.call(
+          {}, slices, true);
+    };
+    mrt.addSubTrack_ = function(slices) {
+      var track = new tv.c.tracks.SliceTrack(this.viewport);
+      track.slices = slices;
+      this.appendChild(track);
+      return track;
+    };
+
+    mrt.setItemsToGroup(model.interaction_records, {
+      guid: tv.b.GUID.allocate(),
+      model: model,
+      getSettingsKey: function() {
+        return undefined;
+      }
+    });
+
+    return mrt;
+  }
+
+  /**
+   * Visualizes a Model by building ProcessTracks and
+   * CpuTracks.
+   * @constructor
+   */
+  var TraceModelTrack = tv.b.ui.define(
+      'trace-model-track', tv.c.tracks.ContainerTrack);
+
+
+  TraceModelTrack.prototype = {
+
+    __proto__: tv.c.tracks.ContainerTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.ContainerTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('model-track');
+
+      var typeInfos = tv.c.tracks.Highlighter.getAllRegisteredTypeInfos();
+      this.highlighters_ = typeInfos.map(
+        function(typeInfo) {
+          return new typeInfo.constructor(viewport);
+        });
+
+      this.upperMode_ = false;
+      this.annotationViews_ = [];
+    },
+
+    // upperMode is true if the track is being used on the ruler.
+    get upperMode() {
+      return this.upperMode_;
+    },
+
+    set upperMode(upperMode) {
+      this.upperMode_ = upperMode;
+      this.updateContents_();
+    },
+
+    detach: function() {
+      tv.c.tracks.ContainerTrack.prototype.detach.call(this);
+    },
+
+    get model() {
+      return this.model_;
+    },
+
+    set model(model) {
+      this.model_ = model;
+      this.updateContents_();
+
+      this.model_.addEventListener('annotationChange',
+          this.updateAnnotations_.bind(this));
+    },
+
+    get hasVisibleContent() {
+      return this.children.length > 0;
+    },
+
+    updateContents_: function() {
+      this.textContent = '';
+      if (!this.model_)
+        return;
+
+      if (this.upperMode_)
+        this.updateContentsForUpperMode_();
+      else
+        this.updateContentsForLowerMode_();
+    },
+
+    updateContentsForUpperMode_: function() {
+    },
+
+    updateContentsForLowerMode_: function() {
+      if (this.model_.interaction_records.length) {
+        var mrt = new HackyMultiRowTrack(this.viewport_, this.model_);
+        this.appendChild(mrt);
+      }
+
+      if (this.model_.alerts.length) {
+        var at = new tv.c.tracks.AlertTrack(this.viewport_);
+        at.alerts = this.model_.alerts;
+        this.appendChild(at);
+      }
+
+      if (this.model_.globalMemoryDumps.length) {
+        var mdt = new tv.c.tracks.MemoryDumpTrack(this.viewport_);
+        mdt.memoryDumps = this.model_.globalMemoryDumps;
+        this.appendChild(mdt);
+      }
+
+      this.appendKernelTrack_();
+
+      // Get a sorted list of processes.
+      var processes = this.model_.getAllProcesses();
+      processes.sort(tv.c.trace_model.Process.compare);
+
+      for (var i = 0; i < processes.length; ++i) {
+        var process = processes[i];
+
+        var track = new tv.c.tracks.ProcessTrack(this.viewport);
+        track.process = process;
+        if (!track.hasVisibleContent)
+          continue;
+
+        this.appendChild(track);
+      }
+      this.viewport_.rebuildEventToTrackMap();
+      this.viewport_.rebuildContainerToTrackMap();
+
+      for (var i = 0; i < this.highlighters_.length; i++) {
+        this.highlighters_[i].processModel(this.model_);
+      }
+
+      this.updateAnnotations_();
+    },
+
+    updateAnnotations_: function() {
+      this.annotationViews_ = [];
+      var annotations = this.model_.getAllAnnotations();
+      for (var i = 0; i < annotations.length; i++) {
+        this.annotationViews_.push(
+            annotations[i].getOrCreateView(this.viewport_));
+      }
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+      if (!this.model_)
+        return;
+
+      var tracks = this.children;
+      for (var i = 0; i < tracks.length; ++i)
+        tracks[i].addEventsToTrackMap(eventToTrackMap);
+
+      if (this.instantEvents === undefined)
+        return;
+
+      var vp = this.viewport_;
+      this.instantEvents.forEach(function(ev) {
+        eventToTrackMap.addEvent(ev, this);
+      }.bind(this));
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+      var tracks = this.children;
+      for (var i = 0; i < tracks.length; ++i)
+        tracks[i].addContainersToTrackMap(containerToTrackMap);
+    },
+
+    appendKernelTrack_: function() {
+      var kernel = this.model.kernel;
+      var track = new tv.c.tracks.KernelTrack(this.viewport);
+      track.kernel = this.model.kernel;
+      if (!track.hasVisibleContent)
+        return;
+      this.appendChild(track);
+    },
+
+    drawTrack: function(type) {
+      var ctx = this.context();
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var bounds = this.getBoundingClientRect();
+      var canvasBounds = ctx.canvas.getBoundingClientRect();
+
+      ctx.save();
+      ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+      var dt = this.viewport.currentDisplayTransform;
+      var viewLWorld = dt.xViewToWorld(0);
+      var viewRWorld = dt.xViewToWorld(bounds.width * pixelRatio);
+
+      switch (type) {
+        case tv.c.tracks.DrawType.GRID:
+          this.viewport.drawMajorMarkLines(ctx);
+          // The model is the only thing that draws grid lines.
+          ctx.restore();
+          return;
+
+        case tv.c.tracks.DrawType.FLOW_ARROWS:
+          if (this.model_.flowIntervalTree.size === 0) {
+            ctx.restore();
+            return;
+          }
+
+          this.drawFlowArrows_(viewLWorld, viewRWorld);
+          ctx.restore();
+          return;
+
+        case tv.c.tracks.DrawType.INSTANT_EVENT:
+          if (!this.model_.instantEvents ||
+              this.model_.instantEvents.length === 0)
+            break;
+
+          tv.c.drawInstantSlicesAsLines(
+              ctx,
+              this.viewport.currentDisplayTransform,
+              viewLWorld,
+              viewRWorld,
+              bounds.height,
+              this.model_.instantEvents,
+              4);
+
+          break;
+
+        case tv.c.tracks.DrawType.MARKERS:
+          if (!this.viewport.interestRange.isEmpty) {
+            this.viewport.interestRange.draw(ctx, viewLWorld, viewRWorld);
+            this.viewport.interestRange.drawIndicators(
+                ctx, viewLWorld, viewRWorld);
+          }
+          ctx.restore();
+          return;
+
+        case tv.c.tracks.DrawType.HIGHLIGHTS:
+          for (var i = 0; i < this.highlighters_.length; i++) {
+            this.highlighters_[i].drawHighlight(ctx, dt, viewLWorld, viewRWorld,
+                bounds.height);
+          }
+          ctx.restore();
+          return;
+
+        case tv.c.tracks.DrawType.ANNOTATIONS:
+          for (var i = 0; i < this.annotationViews_.length; i++) {
+            this.annotationViews_[i].draw(this.context());
+          }
+          ctx.restore();
+          return;
+      }
+      ctx.restore();
+
+      tv.c.tracks.ContainerTrack.prototype.drawTrack.call(this, type);
+    },
+
+    drawFlowArrows_: function(viewLWorld, viewRWorld) {
+      var ctx = this.context();
+      var dt = this.viewport.currentDisplayTransform;
+      dt.applyTransformToCanvas(ctx);
+
+      var pixWidth = dt.xViewVectorToWorld(1);
+
+      ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
+      ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+      ctx.lineWidth = pixWidth > 1.0 ? 1 : pixWidth;
+
+      var events =
+          this.model_.flowIntervalTree.findIntersection(viewLWorld, viewRWorld);
+
+      var canvasBounds = ctx.canvas.getBoundingClientRect();
+      for (var i = 0; i < events.length; ++i)
+        this.drawFlowArrow_(ctx, events[i], canvasBounds, pixWidth);
+    },
+
+    drawFlowArrow_: function(ctx, flowEvent,
+                             canvasBounds, pixWidth) {
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var startTrack = this.viewport.trackForEvent(flowEvent.startSlice);
+      var endTrack = this.viewport.trackForEvent(flowEvent.endSlice);
+
+      var startBounds = startTrack.getBoundingClientRect();
+      var endBounds = endTrack.getBoundingClientRect();
+
+      if (flowEvent.startSlice.selected || flowEvent.endSlice.selected) {
+        ctx.shadowBlur = 1;
+        ctx.shadowColor = 'red';
+        ctx.shadowOffsety = 2;
+        ctx.strokeStyle = 'red';
+      } else if (flowEvent.selected) {
+        ctx.shadowBlur = 1;
+        ctx.shadowColor = 'orange';
+        ctx.shadowOffsety = 2;
+        ctx.strokeStyle = 'orange';
+      } else {
+        ctx.shadowBlur = 0;
+        ctx.shadowOffsetX = 0;
+        ctx.strokeStyle = 'rgba(0, 0, 0, 0.4)';
+      }
+
+      var startSize = startBounds.left + startBounds.top +
+          startBounds.bottom + startBounds.right;
+      var endSize = endBounds.left + endBounds.top +
+          endBounds.bottom + endBounds.right;
+      // Nothing to do if both ends of the track are collapsed.
+      if (startSize === 0 && endSize === 0)
+        return;
+
+      var startY = this.calculateTrackY_(startTrack, canvasBounds);
+      var endY = this.calculateTrackY_(endTrack, canvasBounds);
+
+      var pixelStartY = pixelRatio * startY;
+      var pixelEndY = pixelRatio * endY;
+      var half = (flowEvent.end - flowEvent.start) / 2;
+
+      ctx.beginPath();
+      ctx.moveTo(flowEvent.start, pixelStartY);
+      ctx.bezierCurveTo(
+          flowEvent.start + half, pixelStartY,
+          flowEvent.start + half, pixelEndY,
+          flowEvent.end, pixelEndY);
+      ctx.stroke();
+
+      var arrowWidth = 5 * pixWidth * pixelRatio;
+      var distance = flowEvent.end - flowEvent.start;
+      if (distance <= (2 * arrowWidth))
+        return;
+
+      var tipX = flowEvent.end;
+      var tipY = pixelEndY;
+      var arrowHeight = (endBounds.height / 4) * pixelRatio;
+      tv.c.drawTriangle(ctx,
+          tipX, tipY,
+          tipX - arrowWidth, tipY - arrowHeight,
+          tipX - arrowWidth, tipY + arrowHeight);
+      ctx.fill();
+    },
+
+    calculateTrackY_: function(track, canvasBounds) {
+      var bounds = track.getBoundingClientRect();
+      var size = bounds.left + bounds.top + bounds.bottom + bounds.right;
+      if (size === 0)
+        return this.calculateTrackY_(track.parentNode, canvasBounds);
+
+      return bounds.top - canvasBounds.top + (bounds.height / 2);
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+      function onPickHit(instantEvent) {
+        selection.push(instantEvent);
+      }
+      tv.b.iterateOverIntersectingIntervals(this.model_.instantEvents,
+          function(x) { return x.start; },
+          function(x) { return x.duration; },
+          loWX, hiWX,
+          onPickHit.bind(this));
+
+      tv.c.tracks.ContainerTrack.prototype.
+          addIntersectingItemsInRangeToSelectionInWorldSpace.
+          apply(this, arguments);
+    },
+
+    addClosestEventToSelection: function(worldX, worldMaxDist, loY, hiY,
+                                         selection) {
+      this.addClosestInstantEventToSelection(this.model_.instantEvents,
+                                             worldX, worldMaxDist, selection);
+      tv.c.tracks.ContainerTrack.prototype.addClosestEventToSelection.
+          apply(this, arguments);
+    }
+  };
+
+  return {
+    TraceModelTrack: TraceModelTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/track.css b/trace-viewer/trace_viewer/core/tracks/track.css
new file mode 100644
index 0000000..3d56eef
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/track.css
@@ -0,0 +1,33 @@
+/* Copyright (c) 2012 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.track-button {
+  background-color: rgba(255, 255, 255, 0.5);
+  border: 1px solid rgba(0, 0, 0, 0.1);
+  color: rgba(0,0,0,0.2);
+  font-size: 10px;
+  height: 12px;
+  text-align: center;
+  width: 12px;
+}
+
+.track-button:hover {
+  background-color: rgba(255, 255, 255, 1.0);
+  border: 1px solid rgba(0, 0, 0, 0.5);
+  box-shadow: 0 0 .05em rgba(0, 0, 0, 0.4);
+  color: rgba(0, 0, 0, 1);
+}
+
+.track-close-button {
+  left: 2px;
+  position: absolute;
+  top: 2px;
+}
+
+.track-collapse-button {
+  left: 3px;
+  position: absolute;
+  top: 2px;
+}
diff --git a/trace-viewer/trace_viewer/core/tracks/track.html b/trace-viewer/trace_viewer/core/tracks/track.html
new file mode 100644
index 0000000..87464e0
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/track.html
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2013 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<link rel="stylesheet" href="/core/tracks/track.css">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/container_that_decorates_its_children.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Renders an array of slices into the provided div,
+ * using a child canvas element. Uses a FastRectRenderer to draw only
+ * the visible slices.
+ */
+tv.exportTo('tv.c.tracks', function() {
+  /**
+   * The base class for all tracks.
+   * @constructor
+   */
+  var Track = tv.b.ui.define('track',
+                             tv.b.ui.ContainerThatDecoratesItsChildren);
+  Track.prototype = {
+    __proto__: tv.b.ui.ContainerThatDecoratesItsChildren.prototype,
+
+    decorate: function(viewport) {
+      tv.b.ui.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+      if (viewport === undefined)
+        throw new Error('viewport is required when creating a Track.');
+
+      this.viewport_ = viewport;
+      this.classList.add('track');
+    },
+
+    get viewport() {
+      return this.viewport_;
+    },
+
+    get drawingContainer() {
+      var cur = this;
+      while (cur) {
+        if (cur instanceof tv.c.tracks.DrawingContainer)
+          return cur;
+        cur = cur.parentElement;
+      }
+      return undefined;
+    },
+
+    get eventContainer() {
+    },
+
+    invalidateDrawingContainer: function() {
+      var dc = this.drawingContainer;
+      if (dc)
+        dc.invalidate();
+    },
+
+    context: function() {
+      // This is a little weird here, but we have to be able to walk up the
+      // parent tree to get the context.
+      if (!this.parentNode)
+        return undefined;
+      if (!this.parentNode.context)
+        throw new Error('Parent container does not support context() method.');
+      return this.parentNode.context();
+    },
+
+    decorateChild_: function(childTrack) {
+    },
+
+    undecorateChild_: function(childTrack) {
+      if (childTrack.detach)
+        childTrack.detach();
+    },
+
+    updateContents_: function() {
+    },
+
+    drawTrack: function(type) {
+      var ctx = this.context();
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var bounds = this.getBoundingClientRect();
+      var canvasBounds = ctx.canvas.getBoundingClientRect();
+
+      ctx.save();
+      ctx.translate(0, pixelRatio * (bounds.top - canvasBounds.top));
+
+      var dt = this.viewport.currentDisplayTransform;
+      var viewLWorld = dt.xViewToWorld(0);
+      var viewRWorld = dt.xViewToWorld(bounds.width * pixelRatio);
+
+      this.draw(type, viewLWorld, viewRWorld);
+      ctx.restore();
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+    },
+
+    addEventsToTrackMap: function(eventToTrackMap) {
+    },
+
+    addContainersToTrackMap: function(containerToTrackMap) {
+    },
+
+    addIntersectingItemsInRangeToSelection: function(
+        loVX, hiVX, loVY, hiVY, selection) {
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var dt = this.viewport.currentDisplayTransform;
+      var viewPixWidthWorld = dt.xViewVectorToWorld(1);
+      var loWX = dt.xViewToWorld(loVX * pixelRatio);
+      var hiWX = dt.xViewToWorld(hiVX * pixelRatio);
+
+      var clientRect = this.getBoundingClientRect();
+      var a = Math.max(loVY, clientRect.top);
+      var b = Math.min(hiVY, clientRect.bottom);
+      if (a > b)
+        return;
+
+      this.addIntersectingItemsInRangeToSelectionInWorldSpace(
+          loWX, hiWX, viewPixWidthWorld, selection);
+    },
+
+    addIntersectingItemsInRangeToSelectionInWorldSpace: function(
+        loWX, hiWX, viewPixWidthWorld, selection) {
+    },
+
+    /**
+     * Gets implemented by supporting track types. The method adds the event
+     * closest to worldX to the selection.
+     *
+     * @param {number} worldX The position that is looked for.
+     * @param {number} worldMaxDist The maximum distance allowed from worldX to
+     *     the event.
+     * @param {number} loY Lower Y bound of the search interval in view space.
+     * @param {number} hiY Upper Y bound of the search interval in view space.
+     * @param {Selection} selection Selection to which to add hits.
+     */
+    addClosestEventToSelection: function(
+        worldX, worldMaxDist, loY, hiY, selection) {
+    },
+
+    addClosestInstantEventToSelection: function(instantEvents, worldX,
+                                                worldMaxDist, selection) {
+      var instantEvent = tv.b.findClosestElementInSortedArray(
+          instantEvents,
+          function(x) { return x.start; },
+          worldX,
+          worldMaxDist);
+
+      if (!instantEvent)
+        return;
+
+      selection.push(instantEvent);
+    }
+  };
+
+  return {
+    Track: Track
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/tracks/x_marker_annotation_view.html b/trace-viewer/trace_viewer/core/tracks/x_marker_annotation_view.html
new file mode 100644
index 0000000..74e354b
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/tracks/x_marker_annotation_view.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/tracks/annotation_view.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c.annotations', function() {
+  /**
+   * A view that draws a vertical line on the timeline at a specific timestamp.
+   * @extends {AnnotationView}
+   * @constructor
+   */
+  function XMarkerAnnotationView(viewport, annotation) {
+    this.viewport_ = viewport;
+    this.annotation_ = annotation;
+  }
+
+  XMarkerAnnotationView.prototype = {
+    __proto__: tv.c.annotations.AnnotationView.prototype,
+
+    draw: function(ctx) {
+      var dt = this.viewport_.currentDisplayTransform;
+      var viewX = dt.xWorldToView(this.annotation_.timestamp);
+
+      ctx.beginPath();
+      tv.c.drawLine(ctx, viewX, 0, viewX, ctx.canvas.height);
+      ctx.strokeStyle = this.annotation_.strokeStyle;
+      ctx.stroke();
+    }
+  };
+
+  return {
+    XMarkerAnnotationView: XMarkerAnnotationView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/ui_state.html b/trace-viewer/trace_viewer/core/ui_state.html
new file mode 100644
index 0000000..162c20f
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/ui_state.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/location.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.c', function() {
+  var Location = tv.c.Location;
+
+  /**
+   * UIState is a class that represents the current state of the timeline by
+   * the Location of the point of interest and the current scaleX of the
+   * timeline.
+   *
+   * @constructor
+   */
+  function UIState(location, scaleX) {
+    this.location_ = location;
+    this.scaleX_ = scaleX;
+  };
+
+  /**
+   * Accepts a UIState string in the format of (timestamp)@(pid).(tid)x(scaleX)
+   * Returns undefined if string is not in this format, or throws an Error if
+   * variables in a syntactically-correct stateString does not produce a valid
+   * UIState. Otherwise returns a constructed UIState instance.
+   */
+  UIState.fromUserFriendlyString = function(model, viewport, stateString) {
+    var navByFinderPattern = /^(-?\d+(\.\d+)?)@(\d+)\.(\d+)x(\d+(\.\d+)?)$/g;
+    var match = navByFinderPattern.exec(stateString);
+    if (!match)
+      return;
+
+    var timestamp = parseFloat(match[1]);
+    var pid = match[3];
+    var tid = match[4];
+    var scaleX = parseFloat(match[5]);
+
+    if (scaleX <= 0)
+      throw new Error('Invalid ScaleX value in UI State string.');
+
+    var processFromModel = model.processes[pid];
+    if (!processFromModel)
+      throw new Error('Invalid PID value in UI State string.');
+    var threadFromModel = processFromModel.threads[tid];
+    if (!threadFromModel)
+      throw new Error('Invalid TID value in UI State string.');
+
+    var loc = tv.c.Location.fromStableIdAndTimestamp(
+        viewport, threadFromModel.stableId, timestamp);
+    return new UIState(loc, scaleX);
+  }
+
+  UIState.prototype = {
+
+    get location() {
+      return this.location_;
+    },
+
+    get scaleX() {
+      return this.scaleX_;
+    },
+
+    toUserFriendlyString: function(viewport) {
+      var timestamp = this.location_.xWorld;
+      var stableId =
+          this.location_.getContainingTrack(viewport).eventContainer.stableId;
+      var scaleX = this.scaleX_;
+      return timestamp + '@' + stableId + 'x' + scaleX;
+    },
+
+    toDict: function() {
+      return {
+        location: this.location_.toDict(),
+        scaleX: this.scaleX_
+      };
+    }
+  };
+
+  return {
+    UIState: UIState
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/core/ui_state_test.html b/trace-viewer/trace_viewer/core/ui_state_test.html
new file mode 100644
index 0000000..a679c79
--- /dev/null
+++ b/trace-viewer/trace_viewer/core/ui_state_test.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<!--
+Copyright (c) 2015 The Chromium Authors. All rights reserved.
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/core/tracks/track.html">
+<link rel="import" href="/core/ui_state.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var UIState = tv.c.UIState;
+
+  function FakeModel() {
+    this.processes = { 1: { threads: { 2: { stableId: '1.2' } } } };
+  }
+
+  // FakeTrack needs to be an instance of tv.c.tracks.Track because a
+  // location is constructed in terms of Track instances.
+  function FakeTrack() { }
+  FakeTrack.prototype = {
+    __proto__: tv.c.tracks.Track.prototype,
+
+    get eventContainer() {
+      return { stableId: '1.2' };
+    },
+
+    getBoundingClientRect: function() {
+      return { top: 5, height: 2 };
+    },
+
+    get parentElement() {
+      return null;
+    }
+  };
+
+  function FakeViewPort() {
+    this.containerToTrackObj = {
+      // "1.2" is the only valid stableId this test function accepts.
+      getTrackByStableId: function(stableId) {
+        if (stableId === '1.2')
+          return new FakeTrack;
+        return undefined;
+      }
+    };
+  }
+
+  test('invalidStableId', function() {
+    var vp = new FakeViewPort;
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '15@1.3x6'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '15@2.2x6'));
+  });
+
+  test('invalidScaleX', function() {
+    var vp = new FakeViewPort;
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '1@1.2x-1'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '1@1.2x0'));
+  });
+
+  test('invalidSyntax', function() {
+    var vp = new FakeViewPort;
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '5'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '505@1x5'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '505@1.x5'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '5@x5'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, '1@1.2.3x5'));
+    assert.isUndefined(UIState.fromUserFriendlyString(vp, 'ab@1.2x5'));
+  });
+
+  test('validString', function() {
+    var model = new FakeModel;
+    var vp = new FakeViewPort;
+    var str = '-50125.512@1.2x1.1';
+    var uiState = UIState.fromUserFriendlyString(model, vp, str);
+
+    assert.isDefined(uiState);
+    assert.equal(uiState.location.xWorld, -50125.512);
+    assert.equal(
+        uiState.location.getContainingTrack(vp).eventContainer.stableId,
+        '1.2');
+    assert.equal(uiState.scaleX, 1.1);
+
+    assert.equal(uiState.toUserFriendlyString(vp), str);
+  });
+});
+</script>