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/__init__.py b/trace-viewer/trace_viewer/__init__.py
new file mode 100644
index 0000000..6c04f2b
--- /dev/null
+++ b/trace-viewer/trace_viewer/__init__.py
@@ -0,0 +1,12 @@
+# 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.
+import os
+import sys
+def _SetupTVCMPath():
+  tvcm_path = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                           '..', 'third_party', 'tvcm'))
+  if tvcm_path not in sys.path:
+    sys.path.append(tvcm_path)
+
+_SetupTVCMPath()
diff --git a/trace-viewer/trace_viewer/base/base.html b/trace-viewer/trace_viewer/base/base.html
new file mode 100644
index 0000000..10da6c2
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/base.html
@@ -0,0 +1,183 @@
+<!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.
+-->
+<script>
+'use strict';
+
+
+/**
+ * The global object.
+ * @type {!Object}
+ * @const
+ */
+var global = this;
+
+/** Platform, package, object property, and Event support. */
+this.tv = (function() {
+  if (window.tv) {
+    console.warn('Base was multiply initialized. First init wins.');
+    return window.tv;
+  }
+
+  /**
+   * Builds an object structure for the provided namespace path,
+   * ensuring that names that already exist are not overwritten. For
+   * example:
+   * 'a.b.c' -> a = {};a.b={};a.b.c={};
+   * @param {string} name Name of the object that this file defines.
+   * @param {*=} opt_object The object to expose at the end of the path.
+   * @param {Object=} opt_objectToExportTo The object to add the path to;
+   *     default is {@code global}.
+   * @private
+   */
+  function exportPath(name, opt_object, opt_objectToExportTo) {
+    var parts = name.split('.');
+    var cur = opt_objectToExportTo || global;
+
+    for (var part; parts.length && (part = parts.shift());) {
+      if (!parts.length && opt_object !== undefined) {
+        // last part and we have an object; use it
+        cur[part] = opt_object;
+      } else if (part in cur) {
+        cur = cur[part];
+      } else {
+        cur = cur[part] = {};
+      }
+    }
+    return cur;
+  };
+
+  function isDefined(name, opt_globalObject) {
+    var parts = name.split('.');
+
+    var curObject = opt_globalObject || global;
+
+    for (var i = 0; i < parts.length; i++) {
+      var partName = parts[i];
+      var nextObject = curObject[partName];
+      if (nextObject === undefined)
+        return false;
+      curObject = nextObject;
+    }
+    return true;
+  }
+
+  var panicElement = undefined;
+  var rawPanicMessages = [];
+  function showPanicElementIfNeeded() {
+    if (panicElement)
+      return;
+
+    var panicOverlay = document.createElement('div');
+    panicOverlay.style.backgroundColor = 'white';
+    panicOverlay.style.border = '3px solid red';
+    panicOverlay.style.boxSizing = 'border-box';
+    panicOverlay.style.color = 'black';
+    panicOverlay.style.display = '-webkit-flex';
+    panicOverlay.style.height = '100%';
+    panicOverlay.style.left = 0;
+    panicOverlay.style.padding = '8px';
+    panicOverlay.style.position = 'fixed';
+    panicOverlay.style.top = 0;
+    panicOverlay.style.webkitFlexDirection = 'column';
+    panicOverlay.style.width = '100%';
+
+    panicElement = document.createElement('div');
+    panicElement.style.webkitFlex = '1 1 auto';
+    panicElement.style.overflow = 'auto';
+    panicOverlay.appendChild(panicElement);
+
+    if (!document.body) {
+      setTimeout(function() {
+        document.body.appendChild(panicOverlay);
+      }, 150);
+    } else {
+      document.body.appendChild(panicOverlay);
+    }
+  }
+
+  function showPanic(panicTitle, panicDetails) {
+
+    if (panicDetails instanceof Error)
+      panicDetails = panicDetails.stack;
+
+    showPanicElementIfNeeded();
+    var panicMessageEl = document.createElement('div');
+    panicMessageEl.innerHTML =
+        '<h2 id="message"></h2>' +
+        '<pre id="details"></pre>';
+    panicMessageEl.querySelector('#message').textContent = panicTitle;
+    panicMessageEl.querySelector('#details').textContent = panicDetails;
+    panicElement.appendChild(panicMessageEl);
+
+    rawPanicMessages.push({
+      title: panicTitle,
+      details: panicDetails
+    });
+  }
+
+  function hasPanic() {
+    return rawPanicMessages.length !== 0;
+  }
+  function getPanicText() {
+    return rawPanicMessages.map(function(msg) {
+      return msg.title;
+    }).join(', ');
+  }
+
+  function exportTo(namespace, fn) {
+    var obj = exportPath(namespace);
+    var exports = fn();
+
+    for (var propertyName in exports) {
+      // Maybe we should check the prototype chain here? The current usage
+      // pattern is always using an object literal so we only care about own
+      // properties.
+      var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
+                                                               propertyName);
+      if (propertyDescriptor)
+        Object.defineProperty(obj, propertyName, propertyDescriptor);
+    }
+  };
+
+  /**
+   * Initialization which must be deferred until run-time.
+   */
+  function initialize() {
+    if (!window._TRACE_VIEWER_IS_COMPILED) {
+      var ver = parseInt(
+          window.navigator.appVersion.match(/Chrome\/(\d+)\./)[1], 10);
+      var support_content_shell = window.navigator.appVersion.match('77.34.5');
+      if (ver < 36 && !support_content_shell) {
+        var msg = 'A Chrome version of 36 or higher is required for ' +
+            'trace-viewer development. Please upgrade your version of Chrome ' +
+            'and try again.';
+        showPanic('Invalid Chrome version', msg);
+      }
+    }
+
+    tv.doc = document;
+
+    tv.isMac = /Mac/.test(navigator.platform);
+    tv.isWindows = /Win/.test(navigator.platform);
+    tv.isChromeOS = /CrOS/.test(navigator.userAgent);
+    tv.isLinux = /Linux/.test(navigator.userAgent);
+  }
+
+  return {
+    initialize: initialize,
+
+    exportTo: exportTo,
+    isDefined: isDefined,
+
+    showPanic: showPanic,
+    hasPanic: hasPanic,
+    getPanicText: getPanicText
+  };
+})();
+
+tv.initialize();
+</script>
diff --git a/trace-viewer/trace_viewer/base/base64.html b/trace-viewer/trace_viewer/base/base64.html
new file mode 100644
index 0000000..691b4b4
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/base64.html
@@ -0,0 +1,64 @@
+<!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.b', function() {
+
+  function Base64() {
+  }
+
+  function b64ToUint6(nChr) {
+    if (nChr > 64 && nChr < 91)
+      return nChr - 65;
+    if (nChr > 96 && nChr < 123)
+      return nChr - 71;
+    if (nChr > 47 && nChr < 58)
+      return nChr + 4;
+    if (nChr === 43)
+      return 62;
+    if (nChr === 47)
+      return 63;
+    return 0;
+  }
+
+  Base64.getDecodedBufferLength = function(input) {
+    return input.length * 3 + 1 >> 2;
+  }
+
+  Base64.DecodeToTypedArray = function(input, output) {
+
+    var nInLen = input.length;
+    var nOutLen = nInLen * 3 + 1 >> 2;
+    var nMod3 = 0;
+    var nMod4 = 0;
+    var nUint24 = 0;
+    var nOutIdx = 0;
+
+    if (nOutLen > output.byteLength)
+      throw new Error('Output buffer too small to decode.');
+
+    for (var nInIdx = 0; nInIdx < nInLen; nInIdx++) {
+      nMod4 = nInIdx & 3;
+      nUint24 |= b64ToUint6(input.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
+      if (nMod4 === 3 || nInLen - nInIdx === 1) {
+        for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
+          output.setUint8(nOutIdx, nUint24 >>> (16 >>> nMod3 & 24) & 255);
+        }
+        nUint24 = 0;
+      }
+    }
+    return nOutIdx - 1;
+  }
+
+  return {
+    Base64: Base64
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/base64_test.html b/trace-viewer/trace_viewer/base/base64_test.html
new file mode 100644
index 0000000..61587c5
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/base64_test.html
@@ -0,0 +1,38 @@
+<!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/base64.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('getDecodedLength', function() {
+    assert.isTrue(tv.b.Base64.getDecodedBufferLength('YQ==') >= 1);
+    assert.isTrue(tv.b.Base64.getDecodedBufferLength('YWJjZA==') >= 4);
+    assert.isTrue(tv.b.Base64.getDecodedBufferLength('YWJjZGVm') >= 6);
+  });
+
+  test('DecodeToTypedArray', function() {
+    var buffer = new DataView(new ArrayBuffer(256));
+    tv.b.Base64.DecodeToTypedArray('YQ==', buffer);
+    assert.equal(buffer.getInt8(0), 97);
+
+    tv.b.Base64.DecodeToTypedArray('YWJjZA==', buffer);
+    for (var i = 0; i < 4; i++)
+      assert.equal(buffer.getInt8(i), 97 + i);
+
+    tv.b.Base64.DecodeToTypedArray('YWJjZGVm', buffer);
+    for (var i = 0; i < 4; i++)
+      assert.equal(buffer.getInt8(i), 97 + i);
+  });
+
+  test('DecodeLengthReturn', function() {
+    var buffer = new DataView(new ArrayBuffer(256));
+    var len = tv.b.Base64.DecodeToTypedArray(btoa('hello'), buffer);
+    assert.equal(len, 5);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/bbox2.html b/trace-viewer/trace_viewer/base/bbox2.html
new file mode 100644
index 0000000..e57b779
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/bbox2.html
@@ -0,0 +1,158 @@
+<!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/gl_matrix.html">
+<link rel="import" href="/base/rect.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview 2D bounding box computations.
+ */
+tv.exportTo('tv.b', function() {
+
+  /**
+   * Tracks a 2D bounding box.
+   * @constructor
+   */
+  function BBox2() {
+    this.isEmpty_ = true;
+    this.min_ = undefined;
+    this.max_ = undefined;
+  };
+
+  BBox2.prototype = {
+    __proto__: Object.prototype,
+
+    reset: function() {
+      this.isEmpty_ = true;
+      this.min_ = undefined;
+      this.max_ = undefined;
+    },
+
+    get isEmpty() {
+      return this.isEmpty_;
+    },
+
+    addBBox2: function(bbox2) {
+      if (bbox2.isEmpty)
+        return;
+      this.addVec2(bbox2.min_);
+      this.addVec2(bbox2.max_);
+    },
+
+    clone: function() {
+      var bbox = new BBox2();
+      bbox.addBBox2(this);
+      return bbox;
+    },
+
+    /**
+     * Adds x, y to the range.
+     */
+    addXY: function(x, y) {
+      if (this.isEmpty_) {
+        this.max_ = vec2.create();
+        this.min_ = vec2.create();
+        vec2.set(this.max_, x, y);
+        vec2.set(this.min_, x, y);
+        this.isEmpty_ = false;
+        return;
+      }
+      this.max_[0] = Math.max(this.max_[0], x);
+      this.max_[1] = Math.max(this.max_[1], y);
+      this.min_[0] = Math.min(this.min_[0], x);
+      this.min_[1] = Math.min(this.min_[1], y);
+    },
+
+    /**
+     * Adds value_x, value_y in the form [value_x,value_y] to the range.
+     */
+    addVec2: function(value) {
+      if (this.isEmpty_) {
+        this.max_ = vec2.create();
+        this.min_ = vec2.create();
+        vec2.set(this.max_, value[0], value[1]);
+        vec2.set(this.min_, value[0], value[1]);
+        this.isEmpty_ = false;
+        return;
+      }
+      this.max_[0] = Math.max(this.max_[0], value[0]);
+      this.max_[1] = Math.max(this.max_[1], value[1]);
+      this.min_[0] = Math.min(this.min_[0], value[0]);
+      this.min_[1] = Math.min(this.min_[1], value[1]);
+    },
+
+    addQuad: function(quad) {
+      this.addVec2(quad.p1);
+      this.addVec2(quad.p2);
+      this.addVec2(quad.p3);
+      this.addVec2(quad.p4);
+    },
+
+    get minVec2() {
+      if (this.isEmpty_)
+        return undefined;
+      return this.min_;
+    },
+
+    get maxVec2() {
+      if (this.isEmpty_)
+        return undefined;
+      return this.max_;
+    },
+
+    get sizeAsVec2() {
+      if (this.isEmpty_)
+        throw new Error('Empty BBox2 has no size');
+      var size = vec2.create();
+      vec2.subtract(size, this.max_, this.min_);
+      return size;
+    },
+
+    get size() {
+      if (this.isEmpty_)
+        throw new Error('Empty BBox2 has no size');
+      return {width: this.max_[0] - this.min_[0],
+        height: this.max_[1] - this.min_[1]};
+    },
+
+    get width() {
+      if (this.isEmpty_)
+        throw new Error('Empty BBox2 has no width');
+      return this.max_[0] - this.min_[0];
+    },
+
+    get height() {
+      if (this.isEmpty_)
+        throw new Error('Empty BBox2 has no width');
+      return this.max_[1] - this.min_[1];
+    },
+
+    toString: function() {
+      if (this.isEmpty_)
+        return 'empty';
+      return 'min=(' + this.min_[0] + ',' + this.min_[1] + ') ' +
+          'max=(' + this.max_[0] + ',' + this.max_[1] + ')';
+    },
+
+    asRect: function() {
+      return tv.b.Rect.fromXYWH(
+          this.min_[0],
+          this.min_[1],
+          this.max_[0] - this.min_[0],
+          this.max_[1] - this.min_[1]);
+    }
+  };
+
+  return {
+    BBox2: BBox2
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/bbox2_test.html b/trace-viewer/trace_viewer/base/bbox2_test.html
new file mode 100644
index 0000000..8c39b2e
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/bbox2_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="/base/bbox2.html">
+<script>
+// 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.
+
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('addVec2', function() {
+    var bbox = new tv.b.BBox2();
+    var x = vec2.create();
+    vec2.set(x, 10, 10);
+    bbox.addVec2(x);
+    assert.equal(bbox.minVec2[0], 10);
+    assert.equal(bbox.minVec2[1], 10);
+    assert.equal(bbox.maxVec2[0], 10);
+    assert.equal(bbox.maxVec2[1], 10);
+
+    // Mutate x.
+    vec2.set(x, 11, 11);
+
+    // Bbox shouldn't have changed.
+    assert.equal(bbox.minVec2[0], 10);
+    assert.equal(bbox.minVec2[1], 10);
+    assert.equal(bbox.maxVec2[0], 10);
+    assert.equal(bbox.maxVec2[1], 10);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/category_util.html b/trace-viewer/trace_viewer/base/category_util.html
new file mode 100644
index 0000000..c4ccef8
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/category_util.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+gCopyright (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 code for working with tracing categories.
+ *
+ */
+tv.exportTo('tv.b', function() {
+
+  // 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;
+  }
+
+  return {
+    getCategoryParts: getCategoryParts
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/color.html b/trace-viewer/trace_viewer/base/color.html
new file mode 100644
index 0000000..b94c9b9
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/color.html
@@ -0,0 +1,134 @@
+<!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.b', function() {
+  function Color(opt_r, opt_g, opt_b, opt_a) {
+    this.r = Math.floor(opt_r) || 0;
+    this.g = Math.floor(opt_g) || 0;
+    this.b = Math.floor(opt_b) || 0;
+    this.a = opt_a;
+  }
+
+  Color.fromString = function(str) {
+    var tmp;
+    var values;
+    if (str.substr(0, 4) == 'rgb(') {
+      tmp = str.substr(4, str.length - 5);
+      values = tmp.split(',').map(function(v) {
+        return v.replace(/^\s+/, '', 'g');
+      });
+      if (values.length != 3)
+        throw new Error('Malformatted rgb-expression');
+      return new Color(
+          parseInt(values[0]),
+          parseInt(values[1]),
+          parseInt(values[2]));
+    } else if (str.substr(0, 5) == 'rgba(') {
+      tmp = str.substr(5, str.length - 6);
+      values = tmp.split(',').map(function(v) {
+        return v.replace(/^\s+/, '', 'g');
+      });
+      if (values.length != 4)
+        throw new Error('Malformatted rgb-expression');
+      return new Color(
+          parseInt(values[0]),
+          parseInt(values[1]),
+          parseInt(values[2]),
+          parseFloat(values[3]));
+    } else if (str[0] == '#' && str.length == 7) {
+      return new Color(
+          parseInt(str.substr(1, 2), 16),
+          parseInt(str.substr(3, 2), 16),
+          parseInt(str.substr(5, 2), 16));
+    } else {
+      throw new Error('Unrecognized string format.');
+    }
+  };
+
+  Color.lerp = function(a, b, percent) {
+    if (a.a !== undefined && b.a !== undefined)
+      return Color.lerpRGBA(a, b, percent);
+    return Color.lerpRGB(a, b, percent);
+  }
+  Color.lerpRGB = function(a, b, percent) {
+    return new Color(
+        ((b.r - a.r) * percent) + a.r,
+        ((b.g - a.g) * percent) + a.g,
+        ((b.b - a.b) * percent) + a.b);
+  }
+
+  Color.lerpRGBA = function(a, b, percent) {
+    return new Color(
+        ((b.r - a.r) * percent) + a.r,
+        ((b.g - a.g) * percent) + a.g,
+        ((b.b - a.b) * percent) + a.b,
+        ((b.a - a.a) * percent) + a.a);
+  }
+
+  Color.prototype = {
+    clone: function() {
+      var c = new Color();
+      c.r = this.r;
+      c.g = this.g;
+      c.b = this.b;
+      c.a = this.a;
+      return c;
+    },
+
+    blendOver: function(bgColor) {
+      var oneMinusThisAlpha = 1 - this.a;
+      var outA = this.a + bgColor.a * oneMinusThisAlpha;
+      var bgBlend = (bgColor.a * oneMinusThisAlpha) / bgColor.a;
+      return new Color(
+          this.r * this.a + bgColor.r * bgBlend,
+          this.g * this.a + bgColor.g * bgBlend,
+          this.b * this.a + bgColor.b * bgBlend,
+          outA);
+    },
+
+    brighten: function(opt_k) {
+      var k;
+      k = opt_k || 0.45;
+
+      return new Color(
+          Math.min(255, this.r + Math.floor(this.r * k)),
+          Math.min(255, this.g + Math.floor(this.g * k)),
+          Math.min(255, this.b + Math.floor(this.b * k)));
+    },
+
+    darken: function(opt_k) {
+      var k;
+      k = opt_k || 0.45;
+
+      return new Color(
+          Math.min(255, this.r - Math.floor(this.r * k)),
+          Math.min(255, this.g - Math.floor(this.g * k)),
+          Math.min(255, this.b - Math.floor(this.b * k)));
+    },
+
+    withAlpha: function(a) {
+      return new Color(this.r, this.g, this.b, a);
+    },
+
+    toString: function() {
+      if (this.a !== undefined) {
+        return 'rgba(' +
+            this.r + ',' + this.g + ',' +
+            this.b + ',' + this.a + ')';
+      }
+      return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';
+    }
+  };
+
+  return {
+    Color: Color
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/color_test.html b/trace-viewer/trace_viewer/base/color_test.html
new file mode 100644
index 0000000..28a484c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/color_test.html
@@ -0,0 +1,75 @@
+<!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/color.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('fromRGB', function() {
+    var c = tv.b.Color.fromString('rgb(1, 2, 3)');
+    assert.equal(c.r, 1);
+    assert.equal(c.g, 2);
+    assert.equal(c.b, 3);
+    assert.isUndefined(c.a);
+  });
+
+  test('FromRGBA', function() {
+    var c = tv.b.Color.fromString('rgba(1, 2, 3, 0.5)');
+    assert.equal(c.r, 1);
+    assert.equal(c.g, 2);
+    assert.equal(c.b, 3);
+    assert.equal(c.a, 0.5);
+  });
+
+  test('fromHex', function() {
+    var c = tv.b.Color.fromString('#010203');
+    assert.equal(c.r, 1);
+    assert.equal(c.g, 2);
+    assert.equal(c.b, 3);
+    assert.isUndefined(c.a);
+  });
+
+  test('toStringRGB', function() {
+    var c = new tv.b.Color(1, 2, 3);
+    assert.equal(c.toString(), 'rgb(1,2,3)');
+  });
+
+  test('toStringRGBA', function() {
+    var c = new tv.b.Color(1, 2, 3, 0.5);
+    assert.equal(c.toString(), 'rgba(1,2,3,0.5)');
+  });
+
+  test('lerpRGB', function() {
+    var a = new tv.b.Color(0, 127, 191);
+    var b = new tv.b.Color(255, 255, 255);
+    var x = tv.b.Color.lerpRGB(a, b, 0.25);
+    assert.equal(x.r, 63);
+    assert.equal(x.g, 159);
+    assert.equal(x.b, 207);
+  });
+
+  test('lerpRGBA', function() {
+    var a = new tv.b.Color(0, 127, 191, 0.5);
+    var b = new tv.b.Color(255, 255, 255, 1);
+    var x = tv.b.Color.lerpRGBA(a, b, 0.25);
+    assert.equal(x.r, 63);
+    assert.equal(x.g, 159);
+    assert.equal(x.b, 207);
+    assert.equal(x.a, 0.625);
+  });
+
+  test('blendRGBA', function() {
+    var red = new tv.b.Color(255, 0, 0, 0.5);
+    var white = new tv.b.Color(255, 255, 255, 1);
+    var x = red.blendOver(white);
+    assert.equal(x.r, 255);
+    assert.equal(x.g, 127);
+    assert.equal(x.b, 127);
+    assert.equal(x.a, 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/deep_utils.html b/trace-viewer/trace_viewer/base/deep_utils.html
new file mode 100644
index 0000000..1e52413
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/deep_utils.html
@@ -0,0 +1,64 @@
+<!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.b', function() {
+  function _iterateElementDeeplyImpl(element, cb, thisArg, includeElement) {
+    if (includeElement) {
+      if (cb.call(thisArg, element))
+        return true;
+    }
+
+    if (element.shadowRoot) {
+      if (_iterateElementDeeplyImpl(element.shadowRoot, cb, thisArg, false))
+        return true;
+    }
+    for (var i = 0; i < element.children.length; i++) {
+      if (_iterateElementDeeplyImpl(element.children[i], cb, thisArg, true))
+        return true;
+    }
+  }
+  function iterateElementDeeply(element, cb, thisArg) {
+    _iterateElementDeeplyImpl(element, cb, thisArg, false);
+  }
+
+  function findDeepElementMatchingPredicate(element, predicate) {
+    var foundElement = undefined;
+    function matches(element) {
+      var match = predicate(element);
+      if (!match)
+        return false;
+      foundElement = element;
+      return true;
+    }
+    iterateElementDeeply(element, matches);
+    return foundElement;
+  }
+
+  function findDeepElementMatching(element, selector) {
+    return findDeepElementMatchingPredicate(element, function(element) {
+      return element.matches(selector);
+    });
+  }
+  function findDeepElementWithTextContent(element, re) {
+    return findDeepElementMatchingPredicate(element, function(element) {
+      if (element.children.length !== 0)
+        return false;
+      return re.test(element.textContent);
+    });
+  }
+  return {
+    iterateElementDeeply: iterateElementDeeply,
+    findDeepElementMatching: findDeepElementMatching,
+    findDeepElementMatchingPredicate: findDeepElementMatchingPredicate,
+    findDeepElementWithTextContent: findDeepElementWithTextContent
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/base/deep_utils_test.html b/trace-viewer/trace_viewer/base/deep_utils_test.html
new file mode 100644
index 0000000..3c1bd2a
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/deep_utils_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="/base/deep_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function createElement(tagName, opt_class) {
+    var el = document.createElement(tagName);
+    if (opt_class)
+      el.className = opt_class;
+    return el;
+  }
+
+  test('testFindDeepMatching', function() {
+    var a = createElement('a');
+    var a_ = a.createShadowRoot();
+
+    var b = createElement('b');
+    a_.appendChild(b);
+
+    var b_ = b.createShadowRoot();
+    b_.appendChild(createElement('c', 'x'));
+
+    var m = tv.b.findDeepElementMatching(a, 'c.x');
+    assert.equal(m, b_.children[0]);
+  });
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/base/event_target.html b/trace-viewer/trace_viewer/base/event_target.html
new file mode 100644
index 0000000..281d00a
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/event_target.html
@@ -0,0 +1,159 @@
+<!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';
+
+/**
+ * @fileoverview This contains an implementation of the EventTarget interface
+ * as defined by DOM Level 2 Events.
+ */
+tv.exportTo('tv.b', function() {
+
+  /**
+   * Creates a new EventTarget. This class implements the DOM level 2
+   * EventTarget interface and can be used wherever those are used.
+   * @constructor
+   */
+  function EventTarget() {
+  }
+  EventTarget.decorate = function(target) {
+    for (var k in EventTarget.prototype) {
+      if (k == 'decorate')
+        continue;
+      var v = EventTarget.prototype[k];
+      if (typeof v !== 'function')
+        continue;
+      target[k] = v;
+    }
+  };
+
+  EventTarget.prototype = {
+
+    /**
+     * Adds an event listener to the target.
+     * @param {string} type The name of the event.
+     * @param {!Function|{handleEvent:Function}} handler The handler for the
+     *     event. This is called when the event is dispatched.
+     */
+    addEventListener: function(type, handler) {
+      if (!this.listeners_)
+        this.listeners_ = Object.create(null);
+      if (!(type in this.listeners_)) {
+        this.listeners_[type] = [handler];
+      } else {
+        var handlers = this.listeners_[type];
+        if (handlers.indexOf(handler) < 0)
+          handlers.push(handler);
+      }
+    },
+
+    /**
+     * Removes an event listener from the target.
+     * @param {string} type The name of the event.
+     * @param {!Function|{handleEvent:Function}} handler The handler for the
+     *     event.
+     */
+    removeEventListener: function(type, handler) {
+      if (!this.listeners_)
+        return;
+      if (type in this.listeners_) {
+        var handlers = this.listeners_[type];
+        var index = handlers.indexOf(handler);
+        if (index >= 0) {
+          // Clean up if this was the last listener.
+          if (handlers.length == 1)
+            delete this.listeners_[type];
+          else
+            handlers.splice(index, 1);
+        }
+      }
+    },
+
+    /**
+     * Dispatches an event and calls all the listeners that are listening to
+     * the type of the event.
+     * @param {!cr.event.Event} event The event to dispatch.
+     * @return {boolean} Whether the default action was prevented. If someone
+     *     calls preventDefault on the event object then this returns false.
+     */
+    dispatchEvent: function(event) {
+      if (!this.listeners_)
+        return true;
+
+      // Since we are using DOM Event objects we need to override some of the
+      // properties and methods so that we can emulate this correctly.
+      var self = this;
+      event.__defineGetter__('target', function() {
+        return self;
+      });
+      var realPreventDefault = event.preventDefault;
+      event.preventDefault = function() {
+        realPreventDefault.call(this);
+        this.rawReturnValue = false;
+      };
+
+      var type = event.type;
+      var prevented = 0;
+      if (type in this.listeners_) {
+        // Clone to prevent removal during dispatch
+        var handlers = this.listeners_[type].concat();
+        for (var i = 0, handler; handler = handlers[i]; i++) {
+          if (handler.handleEvent)
+            prevented |= handler.handleEvent.call(handler, event) === false;
+          else
+            prevented |= handler.call(this, event) === false;
+        }
+      }
+
+      return !prevented && event.rawReturnValue;
+    },
+
+    hasEventListener: function(type) {
+      return this.listeners_[type] !== undefined;
+    }
+  };
+
+  var EventTargetHelper = {
+    decorate: function(target) {
+      for (var k in EventTargetHelper) {
+        if (k == 'decorate')
+          continue;
+        var v = EventTargetHelper[k];
+        if (typeof v !== 'function')
+          continue;
+        target[k] = v;
+      }
+      target.listenerCounts_ = {};
+    },
+
+    addEventListener: function(type, listener, useCapture) {
+      this.__proto__.addEventListener.call(
+          this, type, listener, useCapture);
+      if (this.listenerCounts_[type] === undefined)
+        this.listenerCounts_[type] = 0;
+      this.listenerCounts_[type]++;
+    },
+
+    removeEventListener: function(type, listener, useCapture) {
+      this.__proto__.removeEventListener.call(
+          this, type, listener, useCapture);
+      this.listenerCounts_[type]--;
+    },
+
+    hasEventListener: function(type) {
+      return this.listenerCounts_[type] > 0;
+    }
+  };
+
+  // Export
+  return {
+    EventTarget: EventTarget,
+    EventTargetHelper: EventTargetHelper
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/event_target_test.html b/trace-viewer/trace_viewer/base/event_target_test.html
new file mode 100644
index 0000000..b26a479
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/event_target_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="/base/event_target.html">
+<link rel="import" href="/base/events.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('eventTargetHelper', function() {
+    var listenerCallCount = 0;
+    function listener() { listenerCallCount++; }
+
+    var div = document.createElement('div');
+    tv.b.EventTargetHelper.decorate(div);
+
+    assert.isFalse(div.hasEventListener('foo'));
+
+    div.addEventListener('foo', listener);
+    assert.isTrue(div.hasEventListener('foo'));
+
+    tv.b.dispatchSimpleEvent(div, 'foo');
+    assert.equal(listenerCallCount, 1);
+
+    div.removeEventListener('foo', listener);
+
+    tv.b.dispatchSimpleEvent(div, 'foo');
+    assert.equal(listenerCallCount, 1);
+
+    assert.isFalse(div.hasEventListener('foo'));
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/events.html b/trace-viewer/trace_viewer/base/events.html
new file mode 100644
index 0000000..46e02a9
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/events.html
@@ -0,0 +1,54 @@
+<!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/event_target.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  /**
+   * Creates a new event to be used with tv.b.EventTarget or DOM EventTarget
+   * objects.
+   * @param {string} type The name of the event.
+   * @param {boolean=} opt_bubbles Whether the event bubbles.
+   *     Default is false.
+   * @param {boolean=} opt_preventable Whether the default action of the event
+   *     can be prevented.
+   * @constructor
+   * @extends {Event}
+   */
+  function Event(type, opt_bubbles, opt_preventable) {
+    var e = tv.doc.createEvent('Event');
+    e.initEvent(type, !!opt_bubbles, !!opt_preventable);
+    e.__proto__ = global.Event.prototype;
+    return e;
+  };
+
+  Event.prototype = {
+    __proto__: global.Event.prototype
+  };
+
+  /**
+   * Dispatches a simple event on an event target.
+   * @param {!EventTarget} target The event target to dispatch the event on.
+   * @param {string} type The type of the event.
+   * @param {boolean=} opt_bubbles Whether the event bubbles or not.
+   * @param {boolean=} opt_cancelable Whether the default action of the event
+   *     can be prevented.
+   * @return {boolean} If any of the listeners called {@code preventDefault}
+   *     during the dispatch this will return false.
+   */
+  function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
+    var e = new Event(type, opt_bubbles, opt_cancelable);
+    return target.dispatchEvent(e);
+  }
+
+  return {
+    Event: Event,
+    dispatchSimpleEvent: dispatchSimpleEvent
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/extension_registry.html b/trace-viewer/trace_viewer/base/extension_registry.html
new file mode 100644
index 0000000..d4b5e69
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/extension_registry.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+gCopyright (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/iteration_helpers.html">
+<link rel="import" href="/base/event_target.html">
+<link rel="import" href="/base/extension_registry_basic.html">
+<link rel="import" href="/base/extension_registry_type_based.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Helper code for defining extension registries, which can be
+ * used to make a part of trace-viewer extensible.
+ *
+ * This file provides two basic types of extension registries:
+ * - Generic: register a type with metadata, query for those types based on
+ *            a predicate
+ *
+ * - TypeName-based: register a type that handles some combination
+ *                   of tracing categories or typeNames, then query
+ *                   for it based on a category, typeName or both.
+ *
+ * Use these for pure-JS classes or ui.define'd classes. For polymer element
+ * related registries, consult base/polymer_utils.html.
+ *
+ * When you register subtypes, you pass the constructor for the
+ * subtype, and any metadata you want associated with the subtype. Use metadata
+ * instead of stuffing fields onto the constructor. E.g.:
+ *     registry.register(MySubclass, {titleWhenShownInTabStrip: 'MySub'})
+ *
+ * Some registries want a default object that is returned when a more precise
+ * subtype has been registered. To provide one, set the defaultConstructor
+ * option on the registry options.
+ *
+ * If you want to enforce that a registry only manages types of a given subtype,
+ * then set mandatoryBaseType field on the registry.
+ *
+ */
+tv.exportTo('tv.b', function() {
+
+  function decorateExtensionRegistry(registry, registryOptions) {
+    if (registry.register)
+      throw new Error('Already has registry');
+
+    registryOptions.freeze();
+    if (registryOptions.mode == tv.b.BASIC_REGISTRY_MODE) {
+      tv.b._decorateBasicExtensionRegistry(registry, registryOptions);
+    } else if (registryOptions.mode == tv.b.TYPE_BASED_REGISTRY_MODE) {
+      tv.b._decorateTypeBasedExtensionRegistry(registry, registryOptions);
+    } else {
+      throw new Error('Unrecognized mode');
+    }
+
+    // Make it an event target.
+    if (registry.addEventListener === undefined)
+      tv.b.EventTarget.decorate(registry);
+  }
+
+  return {
+    decorateExtensionRegistry: decorateExtensionRegistry
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/extension_registry_base.html b/trace-viewer/trace_viewer/base/extension_registry_base.html
new file mode 100644
index 0000000..455a365
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/extension_registry_base.html
@@ -0,0 +1,102 @@
+<!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.b', function() {
+  function RegisteredTypeInfo(constructor, metadata) {
+    this.constructor = constructor;
+    this.metadata = metadata;
+  };
+
+  var BASIC_REGISTRY_MODE = 'BASIC_REGISTRY_MODE';
+  var TYPE_BASED_REGISTRY_MODE = 'TYPE_BASED_REGISTRY_MODE';
+  var ALL_MODES = {BASIC_REGISTRY_MODE: true, TYPE_BASED_REGISTRY_MODE: true};
+
+  function ExtensionRegistryOptions(mode) {
+    if (mode === undefined)
+      throw new Error('Mode is required');
+    if (!ALL_MODES[mode])
+      throw new Error('Not a mode.');
+
+    this.mode_ = mode;
+    this.defaultMetadata_ = {};
+    this.defaultConstructor_ = undefined;
+    this.mandatoryBaseClass_ = undefined;
+    this.defaultTypeInfo_ = undefined;
+    this.frozen_ = false;
+  }
+  ExtensionRegistryOptions.prototype = {
+    freeze: function() {
+      if (this.frozen_)
+        throw new Error('Frozen');
+      this.frozen_ = true;
+    },
+
+    get mode() {
+      return this.mode_;
+    },
+
+    get defaultMetadata() {
+      return this.defaultMetadata_;
+    },
+
+    set defaultMetadata(defaultMetadata) {
+      if (this.frozen_)
+        throw new Error('Frozen');
+      this.defaultMetadata_ = defaultMetadata;
+      this.defaultTypeInfo_ = undefined;
+    },
+
+    get defaultConstructor() {
+      return this.defaultConstructor_;
+    },
+
+    set defaultConstructor(defaultConstructor) {
+      if (this.frozen_)
+        throw new Error('Frozen');
+      this.defaultConstructor_ = defaultConstructor;
+      this.defaultTypeInfo_ = undefined;
+    },
+
+    get defaultTypeInfo() {
+      if (this.defaultTypeInfo_ === undefined && this.defaultConstructor_) {
+        this.defaultTypeInfo_ = new RegisteredTypeInfo(
+            this.defaultConstructor,
+            this.defaultMetadata);
+      }
+      return this.defaultTypeInfo_;
+    },
+
+    validateConstructor: function(constructor) {
+      if (!this.mandatoryBaseClass)
+        return;
+      var curProto = constructor.prototype.__proto__;
+      var ok = false;
+      while (curProto) {
+        if (curProto === this.mandatoryBaseClass.prototype) {
+          ok = true;
+          break;
+        }
+        curProto = curProto.__proto__;
+      }
+      if (!ok)
+        throw new Error(constructor + 'must be subclass of ' + registry);
+    }
+  };
+
+  return {
+    BASIC_REGISTRY_MODE: BASIC_REGISTRY_MODE,
+    TYPE_BASED_REGISTRY_MODE: TYPE_BASED_REGISTRY_MODE,
+
+    ExtensionRegistryOptions: ExtensionRegistryOptions,
+    RegisteredTypeInfo: RegisteredTypeInfo
+  };
+});
+
+</script>
diff --git a/trace-viewer/trace_viewer/base/extension_registry_basic.html b/trace-viewer/trace_viewer/base/extension_registry_basic.html
new file mode 100644
index 0000000..fb961dd
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/extension_registry_basic.html
@@ -0,0 +1,108 @@
+<!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/extension_registry_base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+
+  var RegisteredTypeInfo = tv.b.RegisteredTypeInfo;
+  var ExtensionRegistryOptions = tv.b.ExtensionRegistryOptions;
+
+  function decorateBasicExtensionRegistry(registry, extensionRegistryOptions) {
+    var savedStateStack = [];
+    registry.registeredTypeInfos_ = [];
+
+    registry.register = function(constructor,
+                                 opt_metadata) {
+      if (registry.findIndexOfRegisteredConstructor(
+          constructor) !== undefined)
+        throw new Error('Handler already registered for ' + constructor);
+
+      extensionRegistryOptions.validateConstructor(constructor);
+
+      var metadata = {};
+      for (var k in extensionRegistryOptions.defaultMetadata)
+        metadata[k] = extensionRegistryOptions.defaultMetadata[k];
+      if (opt_metadata) {
+        for (var k in opt_metadata)
+          metadata[k] = opt_metadata[k];
+      }
+
+      var typeInfo = new RegisteredTypeInfo(
+          constructor,
+          metadata);
+
+      var e = new Event('will-register');
+      e.typeInfo = typeInfo;
+      registry.dispatchEvent(e);
+
+      registry.registeredTypeInfos_.push(typeInfo);
+
+      e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.pushCleanStateBeforeTest = function() {
+      savedStateStack.push(registry.registeredTypeInfos_);
+      registry.registeredTypeInfos_ = [];
+
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+    registry.popCleanStateAfterTest = function() {
+      registry.registeredTypeInfos_ = savedStateStack[0];
+      savedStateStack.splice(0, 1);
+
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.findIndexOfRegisteredConstructor = function(constructor) {
+      for (var i = 0; i < registry.registeredTypeInfos_.length; i++)
+        if (registry.registeredTypeInfos_[i].constructor == constructor)
+          return i;
+      return undefined;
+    };
+
+    registry.unregister = function(constructor) {
+      var foundIndex = registry.findIndexOfRegisteredConstructor(constructor);
+      if (foundIndex === undefined)
+        throw new Error(constructor + ' not registered');
+      registry.registeredTypeInfos_.splice(foundIndex, 1);
+
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.getAllRegisteredTypeInfos = function() {
+      return registry.registeredTypeInfos_;
+    };
+
+    registry.findTypeInfo = function(constructor) {
+      var foundIndex = this.findIndexOfRegisteredConstructor(constructor);
+      if (foundIndex !== undefined)
+        return this.registeredTypeInfos_[foundIndex];
+      return undefined;
+    };
+
+    registry.findTypeInfoMatching = function(predicate, opt_this) {
+      opt_this = opt_this ? opt_this : undefined;
+      for (var i = 0; i < registry.registeredTypeInfos_.length; ++i) {
+        var typeInfo = registry.registeredTypeInfos_[i];
+        if (predicate.call(opt_this, typeInfo))
+          return typeInfo;
+      }
+      return extensionRegistryOptions.defaultTypeInfo;
+    };
+  }
+
+  return {
+    _decorateBasicExtensionRegistry: decorateBasicExtensionRegistry
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/extension_registry_test.html b/trace-viewer/trace_viewer/base/extension_registry_test.html
new file mode 100644
index 0000000..fe673b6
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/extension_registry_test.html
@@ -0,0 +1,132 @@
+<!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">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('tberSimpleNamedRegistration', function() {
+    function DummyEvent() {
+    }
+    DummyEvent.prototype = {
+    };
+
+    function DummyEventSubclass() {
+    }
+    DummyEventSubclass.prototype = {
+      __proto__: DummyEvent.prototype
+    };
+
+    var options = new tv.b.ExtensionRegistryOptions(
+        tv.b.TYPE_BASED_REGISTRY_MODE);
+    options.mandatoryBaseClass = DummyEvent;
+    options.defaultConstructor = DummyEvent;
+    tv.b.decorateExtensionRegistry(
+        DummyEvent, options);
+
+    DummyEvent.register(DummyEventSubclass, {typeName: 'dummy-name'});
+    assert.equal(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+    assert.equal(DummyEvent.getConstructor('dummy', 'dummy-name'),
+                 DummyEventSubclass);
+    DummyEvent.unregister(DummyEventSubclass);
+    assert.equal(DummyEvent, DummyEvent.getConstructor('dummy', 'dummy-name'));
+  });
+
+  test('tberSimpleCategoryRegistration', function() {
+    function DummyEvent() {
+    }
+    DummyEvent.prototype = {
+    };
+
+    function DummyEventSubclass() {
+    }
+    DummyEventSubclass.prototype = {
+      __proto__: DummyEvent.prototype
+    };
+
+    var options = new tv.b.ExtensionRegistryOptions(
+        tv.b.TYPE_BASED_REGISTRY_MODE);
+    options.mandatoryBaseClass = DummyEvent;
+    options.defaultConstructor = DummyEvent;
+    tv.b.decorateExtensionRegistry(
+        DummyEvent, options);
+
+    DummyEvent.register(
+      DummyEventSubclass,
+      {categoryParts: ['dummy']
+    });
+    assert.equal(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+    assert.equal(DummyEvent.getConstructor('dummy', 'dummy-name'),
+                 DummyEventSubclass);
+    DummyEvent.unregister(DummyEventSubclass);
+    assert.equal(DummyEvent, DummyEvent.getConstructor('dummy', 'dummy-name'));
+  });
+
+  test('tberSimpleCompoundCategory', function() {
+    function DummyEvent() {
+    }
+    DummyEvent.prototype = {
+    };
+
+    function DummyEventSubclass() {
+    }
+    DummyEventSubclass.prototype = {
+      __proto__: DummyEvent.prototype
+    };
+
+    var options = new tv.b.ExtensionRegistryOptions(
+        tv.b.TYPE_BASED_REGISTRY_MODE);
+    options.mandatoryBaseClass = DummyEvent;
+    options.defaultConstructor = DummyEvent;
+    tv.b.decorateExtensionRegistry(
+        DummyEvent, options);
+
+    DummyEvent.register(
+        DummyEventSubclass,
+        {
+          categoryParts: ['dummy']
+        });
+    assert.equal(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+    assert.equal(DummyEventSubclass,
+                 DummyEvent.getConstructor('dummy,something-else',
+                                           'dummy-name'));
+  });
+
+  test('tberDefaultType', function() {
+    function DummyEvent() {
+    }
+    DummyEvent.prototype = {
+    };
+
+    function DummyEventSubclass() {
+    }
+    DummyEventSubclass.prototype = {
+      __proto__: DummyEvent.prototype
+    };
+
+    var options = new tv.b.ExtensionRegistryOptions(
+        tv.b.TYPE_BASED_REGISTRY_MODE);
+    options.mandatoryBaseClass = DummyEvent;
+    options.defaultConstructor = DummyEvent;
+    tv.b.decorateExtensionRegistry(
+        DummyEvent, options);
+
+    DummyEvent.register(
+        DummyEventSubclass,
+        {
+          categoryParts: ['dummy']
+        });
+    assert.equal(DummyEvent, DummyEvent.getConstructor('cat', 'name'));
+    assert.equal(DummyEventSubclass,
+                 DummyEvent.getConstructor('dummy,something-else',
+                                           'dummy-name'));
+  });
+
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/extension_registry_type_based.html b/trace-viewer/trace_viewer/base/extension_registry_type_based.html
new file mode 100644
index 0000000..26fbc0c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/extension_registry_type_based.html
@@ -0,0 +1,158 @@
+<!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/extension_registry_base.html">
+<link rel="import" href="/base/category_util.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  var getCategoryParts = tv.b.getCategoryParts;
+
+  var RegisteredTypeInfo = tv.b.RegisteredTypeInfo;
+  var ExtensionRegistryOptions = tv.b.ExtensionRegistryOptions;
+
+
+  function decorateTypeBasedExtensionRegistry(registry,
+                                              extensionRegistryOptions) {
+    var savedStateStack = [];
+
+    registry.registeredTypeInfos_ = [];
+
+    registry.categoryPartToTypeInfoMap_ = {};
+    registry.typeNameToTypeInfoMap_ = {};
+
+    registry.register = function(constructor,
+                                 metadata) {
+
+      extensionRegistryOptions.validateConstructor(constructor);
+
+      var typeInfo = new RegisteredTypeInfo(
+          constructor,
+          metadata || extensionRegistryOptions.defaultMetadata);
+
+      typeInfo.typeNames = [];
+      typeInfo.categoryParts = [];
+      if (metadata && metadata.typeName)
+        typeInfo.typeNames.push(metadata.typeName);
+      if (metadata && metadata.typeNames) {
+        typeInfo.typeNames.push.apply(
+          typeInfo.typeNames, metadata.typeNames);
+      }
+      if (metadata && metadata.categoryParts) {
+        typeInfo.categoryParts.push.apply(
+          typeInfo.categoryParts, metadata.categoryParts);
+      }
+
+      if (typeInfo.typeNames.length === 0 &&
+          typeInfo.categoryParts.length === 0)
+        throw new Error('typeName or typeNames must be provided');
+
+      // Sanity checks...
+      typeInfo.typeNames.forEach(function(typeName) {
+        if (registry.typeNameToTypeInfoMap_[typeName])
+          throw new Error('typeName ' + typeName + ' already registered');
+      });
+      typeInfo.categoryParts.forEach(function(categoryPart) {
+        if (registry.categoryPartToTypeInfoMap_[categoryPart]) {
+          throw new Error('categoryPart ' + categoryPart +
+                          ' already registered');
+        }
+      });
+
+      var e = new Event('will-register');
+      e.typeInfo = typeInfo;
+      registry.dispatchEvent(e);
+
+      // Actual registration.
+      typeInfo.typeNames.forEach(function(typeName) {
+        registry.typeNameToTypeInfoMap_[typeName] = typeInfo;
+      });
+      typeInfo.categoryParts.forEach(function(categoryPart) {
+        registry.categoryPartToTypeInfoMap_[categoryPart] = typeInfo;
+      });
+      registry.registeredTypeInfos_.push(typeInfo);
+
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.pushCleanStateBeforeTest = function() {
+      savedStateStack.push({
+        registeredTypeInfos: registry.registeredTypeInfos_,
+        typeNameToTypeInfoMap: registry.typeNameToTypeInfoMap_,
+        categoryPartToTypeInfoMap: registry.categoryPartToTypeInfoMap_
+      });
+      registry.registeredTypeInfos_ = [];
+      registry.typeNameToTypeInfoMap_ = {};
+      registry.categoryPartToTypeInfoMap_ = {};
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.popCleanStateAfterTest = function() {
+      var state = savedStateStack[0];
+      savedStateStack.splice(0, 1);
+
+      registry.registeredTypeInfos_ = state.registeredTypeInfos;
+      registry.typeNameToTypeInfoMap_ = state.typeNameToTypeInfoMap;
+      registry.categoryPartToTypeInfoMap_ = state.categoryPartToTypeInfoMap;
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.unregister = function(constructor) {
+      var typeInfoIndex = -1;
+      for (var i = 0; i < registry.registeredTypeInfos_.length; i++) {
+        if (registry.registeredTypeInfos_[i].constructor == constructor) {
+          typeInfoIndex = i;
+          break;
+        }
+      }
+      if (typeInfoIndex === -1)
+        throw new Error(constructor + ' not registered');
+
+      var typeInfo = registry.registeredTypeInfos_[typeInfoIndex];
+      registry.registeredTypeInfos_.splice(typeInfoIndex, 1);
+      typeInfo.typeNames.forEach(function(typeName) {
+        delete registry.typeNameToTypeInfoMap_[typeName];
+      });
+      typeInfo.categoryParts.forEach(function(categoryPart) {
+        delete registry.categoryPartToTypeInfoMap_[categoryPart];
+      });
+      var e = new Event('registry-changed');
+      registry.dispatchEvent(e);
+    };
+
+    registry.getTypeInfo = function(category, typeName) {
+      if (category) {
+        var categoryParts = getCategoryParts(category);
+        for (var i = 0; i < categoryParts.length; i++) {
+          var categoryPart = categoryParts[i];
+          if (registry.categoryPartToTypeInfoMap_[categoryPart])
+            return registry.categoryPartToTypeInfoMap_[categoryPart];
+        }
+      }
+      if (registry.typeNameToTypeInfoMap_[typeName])
+        return registry.typeNameToTypeInfoMap_[typeName];
+
+      return extensionRegistryOptions.defaultTypeInfo;
+    };
+
+    // TODO(nduca): Remove or rename.
+    registry.getConstructor = function(category, typeName) {
+      var typeInfo = registry.getTypeInfo(category, typeName);
+      if (typeInfo)
+        return typeInfo.constructor;
+      return undefined;
+    };
+  }
+
+  return {
+    _decorateTypeBasedExtensionRegistry: decorateTypeBasedExtensionRegistry
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/gl_matrix.html b/trace-viewer/trace_viewer/base/gl_matrix.html
new file mode 100644
index 0000000..88034b9
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/gl_matrix.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="/base/base.html">
+<script src="/gl-matrix-min.js"></script>
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  var tmp_vec2 = vec2.create();
+  var tmp_vec2b = vec2.create();
+  var tmp_vec4 = vec4.create();
+  var tmp_mat2d = mat2d.create();
+
+  vec2.createFromArray = function(arr) {
+    if (arr.length != 2)
+      throw new Error('Should be length 2');
+    var v = vec2.create();
+    vec2.set(v, arr[0], arr[1]);
+    return v;
+  };
+
+  vec2.createXY = function(x, y) {
+    var v = vec2.create();
+    vec2.set(v, x, y);
+    return v;
+  };
+
+  vec2.toString = function(a) {
+    return '[' + a[0] + ', ' + a[1] + ']';
+  };
+
+  vec2.addTwoScaledUnitVectors = function(out, u1, scale1, u2, scale2) {
+    // out = u1 * scale1 + u2 * scale2
+    vec2.scale(tmp_vec2, u1, scale1);
+    vec2.scale(tmp_vec2b, u2, scale2);
+    vec2.add(out, tmp_vec2, tmp_vec2b);
+  }
+
+  vec3.createXYZ = function(x, y, z) {
+    var v = vec3.create();
+    vec3.set(v, x, y, z);
+    return v;
+  };
+
+  vec3.toString = function(a) {
+    return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')';
+  }
+
+  mat2d.translateXY = function(out, x, y) {
+    vec2.set(tmp_vec2, x, y);
+    mat2d.translate(out, out, tmp_vec2);
+  }
+
+  mat2d.scaleXY = function(out, x, y) {
+    vec2.set(tmp_vec2, x, y);
+    mat2d.scale(out, out, tmp_vec2);
+  }
+
+  vec4.unitize = function(out, a) {
+    out[0] = a[0] / a[3];
+    out[1] = a[1] / a[3];
+    out[2] = a[2] / a[3];
+    out[3] = 1;
+    return out;
+  }
+
+  vec2.copyFromVec4 = function(out, a) {
+    vec4.unitize(tmp_vec4, a);
+    vec2.copy(out, tmp_vec4);
+  }
+
+  return {};
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/guid.html b/trace-viewer/trace_viewer/base/guid.html
new file mode 100644
index 0000000..f984ff6
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/guid.html
@@ -0,0 +1,23 @@
+<!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.b', function() {
+  var nextGUID = 1;
+  var GUID = {
+    allocate: function() {
+      return nextGUID++;
+    }
+  };
+
+  return {
+    GUID: GUID
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/images/chrome-left.png b/trace-viewer/trace_viewer/base/images/chrome-left.png
new file mode 100644
index 0000000..8eef2bf
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/images/chrome-left.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/base/images/chrome-mid.png b/trace-viewer/trace_viewer/base/images/chrome-mid.png
new file mode 100644
index 0000000..c67e697
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/images/chrome-mid.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/base/images/chrome-right.png b/trace-viewer/trace_viewer/base/images/chrome-right.png
new file mode 100644
index 0000000..834004a
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/images/chrome-right.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/base/images/ui-states.png b/trace-viewer/trace_viewer/base/images/ui-states.png
new file mode 100644
index 0000000..83d0917
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/images/ui-states.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/base/interval_tree.html b/trace-viewer/trace_viewer/base/interval_tree.html
new file mode 100644
index 0000000..b29b637
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/interval_tree.html
@@ -0,0 +1,349 @@
+<!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.b', function() {
+  function max(a, b) {
+    if (a === undefined)
+      return b;
+    if (b === undefined)
+      return a;
+    return Math.max(a, b);
+  }
+
+  /**
+   * This class implements an interval tree.
+   *    See: http://wikipedia.org/wiki/Interval_tree
+   *
+   * Internally the tree is a Red-Black tree. The insertion/colour is done using
+   * the Left-leaning Red-Black Trees algorithm as described in:
+   *       http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf
+   *
+   * @param {function} beginPositionCb Callback to retrieve the begin position.
+   * @param {function} endPositionCb Callback to retrieve the end position.
+   *
+   * @constructor
+   */
+  function IntervalTree(beginPositionCb, endPositionCb) {
+    this.beginPositionCb_ = beginPositionCb;
+    this.endPositionCb_ = endPositionCb;
+
+    this.root_ = undefined;
+    this.size_ = 0;
+  }
+
+  IntervalTree.prototype = {
+    /**
+     * Insert events into the interval tree.
+     *
+     * @param {Object} datum The object to insert.
+     */
+    insert: function(datum) {
+      var startPosition = this.beginPositionCb_(datum);
+      var endPosition = this.endPositionCb_(datum);
+
+      var node = new IntervalTreeNode(datum,
+                                      startPosition, endPosition);
+      this.size_++;
+
+      this.root_ = this.insertNode_(this.root_, node);
+      this.root_.colour = Colour.BLACK;
+      return datum;
+    },
+
+    insertNode_: function(root, node) {
+      if (root === undefined)
+        return node;
+
+      if (root.leftNode && root.leftNode.isRed &&
+          root.rightNode && root.rightNode.isRed)
+        this.flipNodeColour_(root);
+
+      if (node.key < root.key)
+        root.leftNode = this.insertNode_(root.leftNode, node);
+      else if (node.key === root.key)
+        root.merge(node);
+      else
+        root.rightNode = this.insertNode_(root.rightNode, node);
+
+      if (root.rightNode && root.rightNode.isRed &&
+          (root.leftNode === undefined || !root.leftNode.isRed))
+        root = this.rotateLeft_(root);
+
+      if (root.leftNode && root.leftNode.isRed &&
+          root.leftNode.leftNode && root.leftNode.leftNode.isRed)
+        root = this.rotateRight_(root);
+
+      return root;
+    },
+
+    rotateRight_: function(node) {
+      var sibling = node.leftNode;
+      node.leftNode = sibling.rightNode;
+      sibling.rightNode = node;
+      sibling.colour = node.colour;
+      node.colour = Colour.RED;
+      return sibling;
+    },
+
+    rotateLeft_: function(node) {
+      var sibling = node.rightNode;
+      node.rightNode = sibling.leftNode;
+      sibling.leftNode = node;
+      sibling.colour = node.colour;
+      node.colour = Colour.RED;
+      return sibling;
+    },
+
+    flipNodeColour_: function(node) {
+      node.colour = this.flipColour_(node.colour);
+      node.leftNode.colour = this.flipColour_(node.leftNode.colour);
+      node.rightNode.colour = this.flipColour_(node.rightNode.colour);
+    },
+
+    flipColour_: function(colour) {
+      return colour === Colour.RED ? Colour.BLACK : Colour.RED;
+    },
+
+    /* The high values are used to find intersection. It should be called after
+     * all of the nodes are inserted. Doing it each insert is _slow_. */
+    updateHighValues: function() {
+      this.updateHighValues_(this.root_);
+    },
+
+    /* There is probably a smarter way to do this by starting from the inserted
+     * node, but need to handle the rotations correctly. Went the easy route
+     * for now. */
+    updateHighValues_: function(node) {
+      if (node === undefined)
+        return undefined;
+
+      node.maxHighLeft = this.updateHighValues_(node.leftNode);
+      node.maxHighRight = this.updateHighValues_(node.rightNode);
+
+      return max(max(node.maxHighLeft, node.highValue), node.maxHighRight);
+    },
+
+    validateFindArguments_: function(queryLow, queryHigh) {
+      if (queryLow === undefined || queryHigh === undefined)
+        throw new Error('queryLow and queryHigh must be defined');
+      if ((typeof queryLow !== 'number') || (typeof queryHigh !== 'number'))
+        throw new Error('queryLow and queryHigh must be numbers');
+    },
+
+    /**
+     * Retrieve all overlapping intervals.
+     *
+     * @param {number} queryLow The low value for the intersection interval.
+     * @param {number} queryHigh The high value for the intersection interval.
+     * @return {Array} All [begin, end] pairs inside intersecting intervals.
+     */
+    findIntersection: function(queryLow, queryHigh) {
+      this.validateFindArguments_(queryLow, queryHigh);
+      if (this.root_ === undefined)
+        return [];
+
+      var ret = [];
+      this.root_.appendIntersectionsInto_(ret, queryLow, queryHigh);
+      return ret;
+    },
+
+    /**
+     * Returns the number of nodes in the tree.
+     */
+    get size() {
+      return this.size_;
+    },
+
+    /**
+     * Returns the root node in the tree.
+     */
+    get root() {
+      return this.root_;
+    },
+
+    /**
+     * Dumps out the [lowValue, highValue] pairs for each node in depth-first
+     * order.
+     */
+    dump_: function() {
+      if (this.root_ === undefined)
+        return [];
+      return this.root_.dump();
+    }
+  };
+
+  var Colour = {
+    RED: 'red',
+    BLACK: 'black'
+  };
+
+  function IntervalTreeNode(datum, lowValue, highValue) {
+    this.lowValue_ = lowValue;
+
+    this.data_ = [{
+      datum: datum,
+      high: highValue,
+      low: lowValue
+    }];
+
+    this.colour_ = Colour.RED;
+
+    this.parentNode_ = undefined;
+    this.leftNode_ = undefined;
+    this.rightNode_ = undefined;
+
+    this.maxHighLeft_ = undefined;
+    this.maxHighRight_ = undefined;
+  }
+
+  IntervalTreeNode.prototype = {
+    appendIntersectionsInto_: function(ret, queryLow, queryHigh) {
+      /* This node starts has a start point at or further right then queryHigh
+       * so we know this node is out and all right children are out. Just need
+       * to check left */
+      if (this.lowValue_ >= queryHigh) {
+        if (!this.leftNode_)
+          return;
+        return this.leftNode_.appendIntersectionsInto_(
+            ret, queryLow, queryHigh);
+      }
+
+      /* If we have a maximum left high value that is bigger then queryLow we
+       * need to check left for matches */
+      if (this.maxHighLeft_ > queryLow) {
+        this.leftNode_.appendIntersectionsInto_(ret, queryLow, queryHigh);
+      }
+
+      /* We know that this node starts before queryHigh, if any of it's data
+       * ends after queryLow we need to add those nodes */
+      if (this.highValue > queryLow) {
+        for (var i = (this.data.length - 1); i >= 0; --i) {
+          /* data nodes are sorted by high value, so as soon as we see one
+           * before low value we're done. */
+          if (this.data[i].high < queryLow)
+            break;
+
+          ret.push(this.data[i].datum);
+        }
+      }
+
+      /* check for matches in the right tree */
+      if (this.rightNode_) {
+        this.rightNode_.appendIntersectionsInto_(ret, queryLow, queryHigh);
+      }
+    },
+
+    get colour() {
+      return this.colour_;
+    },
+
+    set colour(colour) {
+      this.colour_ = colour;
+    },
+
+    get key() {
+      return this.lowValue_;
+    },
+
+    get lowValue() {
+      return this.lowValue_;
+    },
+
+    get highValue() {
+      return this.data_[this.data_.length - 1].high;
+    },
+
+    set leftNode(left) {
+      this.leftNode_ = left;
+    },
+
+    get leftNode() {
+      return this.leftNode_;
+    },
+
+    get hasLeftNode() {
+      return this.leftNode_ !== undefined;
+    },
+
+    set rightNode(right) {
+      this.rightNode_ = right;
+    },
+
+    get rightNode() {
+      return this.rightNode_;
+    },
+
+    get hasRightNode() {
+      return this.rightNode_ !== undefined;
+    },
+
+    set parentNode(parent) {
+      this.parentNode_ = parent;
+    },
+
+    get parentNode() {
+      return this.parentNode_;
+    },
+
+    get isRootNode() {
+      return this.parentNode_ === undefined;
+    },
+
+    set maxHighLeft(high) {
+      this.maxHighLeft_ = high;
+    },
+
+    get maxHighLeft() {
+      return this.maxHighLeft_;
+    },
+
+    set maxHighRight(high) {
+      this.maxHighRight_ = high;
+    },
+
+    get maxHighRight() {
+      return this.maxHighRight_;
+    },
+
+    get data() {
+      return this.data_;
+    },
+
+    get isRed() {
+      return this.colour_ === Colour.RED;
+    },
+
+    merge: function(node) {
+      for (var i = 0; i < node.data.length; i++)
+        this.data_.push(node.data[i]);
+      this.data_.sort(function(a, b) {
+        return a.high - b.high;
+      });
+    },
+
+    dump: function() {
+      var ret = {};
+      if (this.leftNode_)
+        ret['left'] = this.leftNode_.dump();
+
+      ret['data'] = this.data_.map(function(d) { return [d.low, d.high]; });
+
+      if (this.rightNode_)
+        ret['right'] = this.rightNode_.dump();
+
+      return ret;
+    }
+  };
+
+  return {
+    IntervalTree: IntervalTree
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/interval_tree_test.html b/trace-viewer/trace_viewer/base/interval_tree_test.html
new file mode 100644
index 0000000..a33e16a
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/interval_tree_test.html
@@ -0,0 +1,236 @@
+<!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/interval_tree.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function SimpleIntervalTree() {
+    tv.b.IntervalTree.call(this,
+        function(s) { return s.start; },
+        function(s) { return s.end; });
+    return this;
+  }
+  SimpleIntervalTree.prototype = {
+    __proto__: tv.b.IntervalTree.prototype
+  };
+
+  function buildSimpleTree() {
+    var tree = new SimpleIntervalTree();
+    tree.v0 = tree.insert({start: 2, end: 6});
+    tree.v1 = tree.insert({start: 1, end: 3});
+    tree.v2 = tree.insert({start: 5, end: 7});
+    tree.v3 = tree.insert({start: 1, end: 5});
+    tree.v4 = tree.insert({start: 3, end: 5});
+    tree.v5 = tree.insert({start: 3, end: 5});
+    tree.v6 = tree.insert({start: 3, end: 6});
+    tree.v7 = tree.insert({start: 1, end: 1});
+    tree.v8 = tree.insert({start: 4, end: 8});
+    tree.v9 = tree.insert({start: 0, end: 2});
+
+    tree.updateHighValues();
+
+    return tree;
+  }
+
+  function sortSimpleResults(intersection) {
+    intersection.sort(function(a, b) {
+      if (a.start === b.start)
+        return a.end - b.end;
+      return a.start - b.start;
+    });
+  }
+
+  test('findIntersection', function() {
+    var tree = buildSimpleTree();
+    var intersection = tree.findIntersection(2, 4);
+    sortSimpleResults(intersection);
+
+    var expected = [tree.v1, tree.v3, tree.v0, tree.v4, tree.v5, tree.v6];
+    assert.equal(intersection.length, 6);
+    assert.deepEqual(intersection, expected);
+  });
+
+  test('findIntersection_zeroDuration', function() {
+    var tree = buildSimpleTree();
+    var intersection = tree.findIntersection(2, 2);
+    sortSimpleResults(intersection);
+
+    var expected = [tree.v1, tree.v3];
+    assert.equal(intersection.length, 2);
+    assert.deepEqual(intersection, expected);
+  });
+
+  test('findIntersection_noMatching', function() {
+    var tree = buildSimpleTree();
+    var intersection = tree.findIntersection(9, 10);
+    assert.deepEqual(intersection, []);
+  });
+
+  test('findIntersection_emptyTree', function() {
+    var tree = new tv.b.IntervalTree();
+    tree.updateHighValues();
+
+    var intersection = tree.findIntersection(2, 4);
+    assert.deepEqual(intersection, []);
+  });
+
+  test('findIntersection_emptyInterval', function() {
+    var tree = new tv.b.IntervalTree();
+    tree.updateHighValues();
+
+    assert.throws(function() {
+      tree.findIntersection();
+    });
+    assert.throws(function() {
+      tree.findIntersection(1);
+    });
+    assert.throws(function() {
+      tree.findIntersection('a', 'b');
+    });
+  });
+
+  test('insert', function() {
+    var tree = new tv.b.IntervalTree(
+        function(s) { return s.start; },
+        function(s) { return s.end; });
+
+    assert.equal(tree.size, 0);
+
+    tree.insert({start: 1, end: 4});
+    tree.insert({start: 3, end: 5});
+    tree.updateHighValues();
+
+    var outTree = {
+      'left': {
+        'data': [[1, 4]]
+      },
+      'data': [[3, 5]]
+    };
+
+    assert.equal(tree.size, 2);
+    assert.deepEqual(tree.dump_(), outTree);
+  });
+
+  test('insert_withoutEnd', function() {
+    var tree = new tv.b.IntervalTree(
+        function(s) { return s.start; },
+        function(s) { return s.end; });
+
+    assert.equal(tree.size, 0);
+
+    tree.insert({start: 3, end: 5});
+    tree.insert({start: 1, end: 4});
+    tree.updateHighValues();
+
+    var outTree = {
+      'left': {
+        'data': [[1, 4]]
+      },
+      'data': [[3, 5]]
+    };
+
+    assert.equal(tree.size, 2);
+    assert.deepEqual(tree.dump_(), outTree);
+  });
+
+  test('insert_balancesTree', function() {
+    var tree = new tv.b.IntervalTree(
+        function(s) { return s.start; },
+        function(s) { return s.end; });
+
+    assert.equal(tree.size, 0);
+
+    for (var i = 0; i < 10; ++i)
+      tree.insert({start: i, end: 5});
+    tree.updateHighValues();
+
+    var outTree = {
+      'left': {
+        'left': {
+          'data': [[0, 5]]
+        },
+        'data': [[1, 5]],
+        'right': {
+          'data': [[2, 5]]
+        }
+      },
+      'data': [[3, 5]],
+      'right': {
+        'left': {
+          'left': {
+            'data': [[4, 5]]
+          },
+          'data': [[5, 5]],
+          'right': {
+            'data': [[6, 5]]
+          }
+        },
+        'data': [[7, 5]],
+        'right': {
+          'left': {
+            'data': [[8, 5]]
+          },
+          'data': [[9, 5]]
+        }
+      }
+    };
+
+    assert.deepEqual(tree.dump_(), outTree);
+  });
+
+  test('insert_withDuplicateIntervals', function() {
+    var tree = new tv.b.IntervalTree(
+        function(s) { return s.start; },
+        function(s) { return s.end; });
+
+    assert.equal(tree.size, 0);
+
+    tree.insert({start: 1, end: 4});
+    tree.insert({start: 3, end: 5});
+    tree.insert({start: 3, end: 5});
+    tree.insert({start: 3, end: 6});
+    tree.updateHighValues();
+
+    var outTree = {
+      'left': {
+        'data': [[1, 4]]
+      },
+      'data': [[3, 5], [3, 5], [3, 6]]
+    };
+
+    assert.equal(tree.size, 4);
+    assert.deepEqual(tree.dump_(), outTree);
+  });
+
+  test('insert_updatesHighValues', function() {
+    var tree = buildSimpleTree();
+
+    var expected = [
+      [undefined, undefined],
+      [2, undefined],
+      [5, 8],
+      [undefined, undefined],
+      [6, 7],
+      [undefined, undefined]
+    ];
+
+    var result = [];
+    function walk(node) {
+      if (node === undefined)
+        return;
+
+      walk(node.leftNode);
+      result.push([node.maxHighLeft, node.maxHighRight]);
+      walk(node.rightNode);
+    }
+    walk(tree.root);
+
+    assert.deepEqual(result, expected);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/iteration_helpers.html b/trace-viewer/trace_viewer/base/iteration_helpers.html
new file mode 100644
index 0000000..7120953
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/iteration_helpers.html
@@ -0,0 +1,151 @@
+<!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.b', function() {
+  function asArray(arrayish) {
+    var values = [];
+    for (var i = 0; i < arrayish.length; i++)
+      values.push(arrayish[i]);
+    return values;
+  }
+
+  function compareArrays(x, y, elementCmp) {
+    var minLength = Math.min(x.length, y.length);
+    for (var i = 0; i < minLength; i++) {
+      var tmp = elementCmp(x[i], y[i]);
+      if (tmp)
+        return tmp;
+    }
+    if (x.length == y.length)
+      return 0;
+
+    if (x[i] === undefined)
+      return -1;
+
+    return 1;
+  }
+
+  /**
+   * Compares two values when one or both might be undefined. Undefined
+   * values are sorted after defined.
+   */
+  function comparePossiblyUndefinedValues(x, y, cmp) {
+    if (x !== undefined && y !== undefined)
+      return cmp(x, y);
+    if (x !== undefined)
+      return -1;
+    if (y !== undefined)
+      return 1;
+    return 0;
+  }
+
+  function concatenateArrays(/*arguments*/) {
+    var values = [];
+    for (var i = 0; i < arguments.length; i++) {
+      if (!(arguments[i] instanceof Array))
+        throw new Error('Arguments ' + i + 'is not an array');
+      values.push.apply(values, arguments[i]);
+    }
+    return values;
+  }
+
+  function concatenateObjects(/*arguments*/) {
+    var result = {};
+    for (var i = 0; i < arguments.length; i++) {
+      var object = arguments[i];
+      for (var j in object) {
+        result[j] = object[j];
+      }
+    }
+    return result;
+  }
+
+  function dictionaryKeys(dict) {
+    var keys = [];
+    for (var key in dict)
+      keys.push(key);
+    return keys;
+  }
+
+  function dictionaryValues(dict) {
+    var values = [];
+    for (var key in dict)
+      values.push(dict[key]);
+    return values;
+  }
+
+  function dictionaryLength(dict) {
+    var n = 0;
+    for (var key in dict)
+      n++;
+    return n;
+  }
+
+  function iterItems(dict, fn, opt_this) {
+    opt_this = opt_this || this;
+    for (var key in dict)
+      fn.call(opt_this, key, dict[key]);
+  }
+
+  function iterObjectFieldsRecursively(object, func) {
+    if (!(object instanceof Object))
+      return;
+
+    if (object instanceof Array) {
+      for (var i = 0; i < object.length; i++) {
+        func(object, i, object[i]);
+        iterObjectFieldsRecursively(object[i], func);
+      }
+      return;
+    }
+
+    for (var key in object) {
+      var value = object[key];
+      func(object, key, value);
+      iterObjectFieldsRecursively(value, func);
+    }
+  }
+
+  function identity(d) {
+    return d;
+  }
+
+  function findFirstIndexInArray(ary, opt_func, opt_this) {
+    var func = opt_func || identity;
+    for (var i = 0; i < ary.length; i++) {
+      if (func.call(opt_this, ary[i], i))
+        return i;
+    }
+    return -1;
+  }
+
+  function findFirstInArray(ary, opt_func, opt_this) {
+    var i = findFirstIndexInArray(ary, opt_func, opt_func);
+    if (i === -1)
+      return undefined;
+    return ary[i];
+  }
+
+  return {
+    asArray: asArray,
+    concatenateArrays: concatenateArrays,
+    concatenateObjects: concatenateObjects,
+    compareArrays: compareArrays,
+    comparePossiblyUndefinedValues: comparePossiblyUndefinedValues,
+    dictionaryLength: dictionaryLength,
+    dictionaryKeys: dictionaryKeys,
+    dictionaryValues: dictionaryValues,
+    iterItems: iterItems,
+    iterObjectFieldsRecursively: iterObjectFieldsRecursively,
+    findFirstIndexInArray: findFirstIndexInArray,
+    findFirstInArray: findFirstInArray
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/iteration_helpers_test.html b/trace-viewer/trace_viewer/base/iteration_helpers_test.html
new file mode 100644
index 0000000..dce969d
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/iteration_helpers_test.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/iteration_helpers.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var comparePossiblyUndefinedValues = tv.b.comparePossiblyUndefinedValues;
+  var compareArrays = tv.b.compareArrays;
+
+  test('comparePossiblyUndefinedValues', function() {
+    function cmp(x, y) {
+      assert.isDefined(x);
+      assert.isDefined(y);
+      return x - y;
+    }
+
+    assert.isBelow(comparePossiblyUndefinedValues(0, 1, cmp), 0);
+    assert.isAbove(comparePossiblyUndefinedValues(1, 0, cmp), 0);
+    assert.equal(comparePossiblyUndefinedValues(1, 1, cmp), 0);
+
+    assert.isBelow(comparePossiblyUndefinedValues(0, undefined, cmp), 0);
+    assert.isAbove(comparePossiblyUndefinedValues(undefined, 0, cmp), 0);
+    assert.equal(comparePossiblyUndefinedValues(undefined, undefined, cmp), 0);
+  });
+
+  test('compareArrays', function() {
+    function cmp(x, y) {
+      assert.isDefined(x);
+      assert.isDefined(y);
+      return x - y;
+    }
+
+    assert.isBelow(compareArrays([1], [2], cmp), 0);
+    assert.isAbove(compareArrays([2], [1], cmp), 0);
+
+    assert.isBelow(compareArrays([1], [1, 2], cmp), 0);
+    assert.isAbove(compareArrays([1, 2], [1], cmp), 0);
+
+    assert.isBelow(compareArrays([], [1], cmp), 0);
+    assert.isAbove(compareArrays([1], [], cmp), 0);
+
+    assert.isAbove(compareArrays([2], [1], cmp), 0);
+
+    assert.equal(compareArrays([], [], cmp), 0);
+    assert.equal(compareArrays([1], [1], cmp), 0);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/key_event_manager.html b/trace-viewer/trace_viewer/base/key_event_manager.html
new file mode 100644
index 0000000..9641050
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/key_event_manager.html
@@ -0,0 +1,167 @@
+<!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">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+
+  /**
+   * KeyEventManager avoids leaks when listening for keys.
+   *
+   * A common but leaky pattern is:
+   *   document.addEventListener('key*', function().bind(this))
+   * This leaks.
+   *
+   * Instead do this:
+   *   KeyEventManager.instance.addListener('keyDown', func, this);
+   *
+   * This will not leak. BUT, note, if "this" is not attached to the document,
+   * it will NOT receive input events.
+   *
+   * Conceptually, KeyEventManager works by making the this refrence "weak",
+   * which is actually accomplished by putting a guid on the thisArg. When keys
+   * are received, we look for elements with that guid and dispatch the keys to
+   * them.
+   */
+  function KeyEventManager(opt_document) {
+    this.document_ = opt_document || document;
+    if (KeyEventManager.instance)
+      throw new Error('KeyEventManager is a singleton.');
+    this.onEvent_ = this.onEvent_.bind(this);
+    this.document_.addEventListener('keydown', this.onEvent_);
+    this.document_.addEventListener('keypress', this.onEvent_);
+    this.document_.addEventListener('keyup', this.onEvent_);
+    this.listeners_ = [];
+  }
+  KeyEventManager.instance = undefined;
+
+  document.head.addEventListener('tv-unittest-will-run', function() {
+    if (KeyEventManager.instance) {
+      KeyEventManager.instance.destroy();
+      KeyEventManager.instance = undefined;
+    }
+    KeyEventManager.instance = new KeyEventManager();
+  });
+
+  KeyEventManager.prototype = {
+    addListener: function(type, handler, thisArg) {
+      if (!thisArg.keyEventManagerGuid_) {
+        thisArg.keyEventManagerGuid_ = tv.b.GUID.allocate();
+        thisArg.keyEventManagerRefCount_ = 0;
+      }
+      thisArg.classList.add('key-event-manager-target');
+      thisArg.keyEventManagerRefCount_++;
+
+      var guid = thisArg.keyEventManagerGuid_;
+      this.listeners_.push({
+        guid: guid,
+        type: type,
+        handler: handler
+      });
+    },
+
+    onEvent_: function(event) {
+      // This does standard DOM event propagation of the given event, but using
+      // guids to locate the thisArg for each listener. See event_target.js for
+      // notes on how this works.
+      var preventDefaultState = undefined;
+      var stopPropagationCalled = false;
+
+      var oldPreventDefault = event.preventDefault;
+      event.preventDefault = function() {
+        preventDefaultState = false;
+        oldPreventDefault.call(this);
+      };
+
+      var oldStopPropagation = event.stopPropagation;
+      event.stopPropagation = function() {
+        stopPropagationCalled = true;
+        oldStopPropagation.call(this);
+      };
+
+      event.stopImmediatePropagation = function() {
+        throw new Error('Not implemented');
+      };
+
+      var possibleThisArgs = this.document_.querySelectorAll(
+          '.key-event-manager-target');
+      var possibleThisArgsByGUID = {};
+      for (var i = 0; i < possibleThisArgs.length; i++) {
+        possibleThisArgsByGUID[possibleThisArgs[i].keyEventManagerGuid_] =
+            possibleThisArgs[i];
+      }
+
+      // We need to copy listeners_ and verify the thisArgs exists on each loop
+      // iteration because the event callbacks can change the DOM and listener
+      // list.
+      var listeners = this.listeners_.concat();
+      var type = event.type;
+      var prevented = 0;
+      for (var i = 0; i < listeners.length; i++) {
+        var listener = listeners[i];
+        if (listener.type !== type)
+          continue;
+        // thisArg went away.
+        var thisArg = possibleThisArgsByGUID[listener.guid];
+        if (!thisArg)
+          continue;
+
+        var handler = listener.handler;
+        if (handler.handleEvent)
+          prevented |= handler.handleEvent.call(handler, event) === false;
+        else
+          prevented |= handler.call(thisArg, event) === false;
+        if (stopPropagationCalled)
+          break;
+      }
+
+      // We want to return false if preventDefaulted, or one of the handlers
+      // return false. But otherwise, we want to return undefiend.
+      return !prevented && preventDefaultState;
+    },
+
+    removeListener: function(type, handler, thisArg) {
+      if (thisArg.keyEventManagerGuid_ === undefined)
+        throw new Error('Was not registered with KeyEventManager');
+      if (thisArg.keyEventManagerRefCount_ === 0)
+        throw new Error('No events were registered on the provided thisArg');
+      for (var i = 0; i < this.listeners_.length; i++) {
+        var listener = this.listeners_[i];
+        if (listener.type == type &&
+            listener.handler == handler &&
+            listener.guid == thisArg.keyEventManagerGuid_) {
+          thisArg.keyEventManagerRefCount_--;
+          if (thisArg.keyEventManagerRefCount_ === 0)
+            thisArg.classList.remove('key-event-manager-target');
+          this.listeners_.splice(i, 1);
+          return;
+        }
+      }
+      throw new Error('Listener not found');
+    },
+
+    destroy: function() {
+      this.listeners_.splice(0);
+      this.document_.removeEventListener('keydown', this.onEvent_);
+      this.document_.removeEventListener('keypress', this.onEvent_);
+      this.document_.removeEventListener('keyup', this.onEvent_);
+    },
+
+    dispatchFakeEvent: function(type, args) {
+      var e = new KeyboardEvent(type, args);
+      return KeyEventManager.instance.onEvent_.call(undefined, e);
+    }
+  };
+
+  KeyEventManager.instance = new KeyEventManager();
+
+  return {
+    KeyEventManager: KeyEventManager
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/key_event_manager_test.html b/trace-viewer/trace_viewer/base/key_event_manager_test.html
new file mode 100644
index 0000000..0ba68c7
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/key_event_manager_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="/base/key_event_manager.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var KeyEventManager = tv.b.KeyEventManager;
+
+  function withElementAttachedToChild(element, callback) {
+    document.body.appendChild(element);
+    try {
+      callback();
+    } finally {
+      document.body.removeChild(element);
+    }
+  }
+
+
+  test('simpleDispatch', function() {
+    var kem = KeyEventManager.instance;
+    var div = document.createElement('div');
+
+    var fireCount = 0;
+    kem.addListener('keydown', function(e) {
+      fireCount++;
+    }, div);
+
+    // Send an event while its attached to the document.
+    withElementAttachedToChild(div, function() {
+      var ret = kem.dispatchFakeEvent('keydown', {keyCode: 73});
+      assert.isUndefined(ret);
+      assert.equal(fireCount, 1);
+    });
+    fireCount = 0;
+
+    // Send an event while it is detached.
+    var ret = kem.dispatchFakeEvent('keydown', {keyCode: 73});
+    assert.isUndefined(ret);
+    assert.equal(fireCount, 0);
+  });
+
+  test('preventDefault', function() {
+    var kem = KeyEventManager.instance;
+    var div = document.createElement('div');
+
+    var fireCount = 0;
+    kem.addListener('keydown', function(e) {
+      fireCount++;
+      e.preventDefault();
+    }, div);
+
+    withElementAttachedToChild(div, function() {
+      var ret = kem.dispatchFakeEvent('keydown', {keyCode: 73});
+      assert.isFalse(ret);
+      assert.equal(fireCount, 1);
+    });
+
+  });
+
+  test('stopPropagation', function() {
+    var kem = KeyEventManager.instance;
+    var div1 = document.createElement('div');
+    var div2 = document.createElement('div');
+
+    var didFire = false;
+    kem.addListener('keydown', function(e) {
+      e.stopPropagation();
+    }, div1);
+    kem.addListener('keydown', function(e) {
+      throw new Error('Should never get called');
+    }, div2);
+
+    withElementAttachedToChild(div1, function() {
+      withElementAttachedToChild(div2, function() {
+        var ret = kem.dispatchFakeEvent('keydown', {keyCode: 73});
+        assert.isUndefined(ret);
+      });
+    });
+  });
+
+  test('removeListener', function() {
+    var kem = KeyEventManager.instance;
+    var div = document.createElement('div');
+
+    var handlerFired = false;
+    function handler(e) {
+      handlerFired = true;
+    }
+    kem.addListener('keydown', handler, div);
+    kem.removeListener('keydown', handler, div);
+    assert.equal('', div.className);
+
+    withElementAttachedToChild(div, function() {
+      var ret = kem.dispatchFakeEvent('keydown', {keyCode: 73});
+      assert.isFalse(handlerFired);
+    });
+  });
+
+  test('removeOneListener', function() {
+    var kem = KeyEventManager.instance;
+    var div = document.createElement('div');
+
+    var handlerAFired = false;
+    function handlerA(e) {
+      handlerAFired = true;
+    }
+    var handlerBFired = false;
+    function handlerB(e) {
+      handlerBFired = true;
+    }
+    kem.addListener('keydown', handlerA, div);
+    kem.addListener('keydown', handlerB, div);
+    kem.removeListener('keydown', handlerA, div);
+    assert.isTrue(div.classList.contains('key-event-manager-target'));
+
+    withElementAttachedToChild(div, function() {
+      var ret = kem.dispatchFakeEvent('keydown', {keyCode: 73});
+      assert.isFalse(handlerAFired);
+      assert.isTrue(handlerBFired);
+    });
+  });
+
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/polymer_utils.html b/trace-viewer/trace_viewer/base/polymer_utils.html
new file mode 100644
index 0000000..9c98b53
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/polymer_utils.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 Helper code for working with Polymer.
+ */
+tv.exportTo('tv.b', function() {
+
+  Object.observe(Polymer.elements, clearPolymerElementCaches);
+
+  var elementsByName = undefined;
+  var elementsThatExtend = undefined;
+  var elementSubclasses = undefined;
+  function clearPolymerElementCaches() {
+    elementsByName = {};
+    elementsThatExtend = undefined;
+    elementSubclasses = {};
+  }
+
+  function buildElementMapsIfNeeded() {
+    if (elementsThatExtend !== undefined && elementsByName !== undefined)
+      return;
+    elementsByName = {};
+    elementsThatExtend = {};
+    Polymer.elements.forEach(function(element) {
+      if (elementsByName[element.name])
+        throw new Error('Something is strange: dupe polymer element names');
+
+      elementsByName[element.name] = element;
+
+      if (element.extends) {
+        if (elementsThatExtend[element.extends] === undefined)
+          elementsThatExtend[element.extends] = [];
+        elementsThatExtend[element.extends].push(element.name);
+      }
+    });
+  }
+
+  function getPolymerElementNamed(tagName) {
+    buildElementMapsIfNeeded();
+    return elementsByName[tagName];
+  }
+
+  function getPolymerElementsThatSubclass(tagName) {
+    if (Polymer.waitingFor().length) {
+      throw new Error('There are unresolved polymer elements. ' +
+        'Wait until Polymer.whenReady');
+    }
+
+    buildElementMapsIfNeeded();
+
+    var element = getPolymerElementNamed(tagName);
+    if (!element)
+      throw new Error(tagName + ' is not a polymer element');
+
+    if (elementSubclasses === undefined)
+      elementSubclasses = {};
+
+    if (elementSubclasses[tagName] === undefined) {
+      var immediateSubElements = elementsThatExtend[element.name];
+      var allSubElements = [];
+      if (immediateSubElements !== undefined && immediateSubElements.length) {
+        immediateSubElements.forEach(function(subElement) {
+          allSubElements.push(subElement);
+          allSubElements.push.apply(
+            allSubElements, getPolymerElementsThatSubclass(subElement));
+        });
+      }
+      elementSubclasses[tagName] = allSubElements;
+    }
+    return elementSubclasses[tagName];
+  }
+
+  function getPolymerSubclassingDepthFrom(tagName, tagBase) {
+    if (Polymer.waitingFor().length) {
+      throw new Error('There are unresolved polymer elements. ' +
+        'Wait until Polymer.whenReady');
+    }
+
+    if (!Polymer.elements[tagBase])
+      throw new Error(tagBase + ' is not a polymer element');
+
+    if (!Polymer.elements[tagName])
+      throw new Error(tagName + ' is not a polymer element');
+
+    var steps = 0;
+
+    var tagCur = tagName;
+    while (tagCur != tagBase) {
+      steps += 1;
+      tagCur = Polymer.elements[tagCur].extends;
+      if (!tagCur)
+        throw new Error(tagName + ' does not subclass ' + tagBase);
+    }
+    return steps;
+  }
+
+  return {
+    getPolymerElementNamed: getPolymerElementNamed,
+
+    getPolymerElementsThatSubclass: getPolymerElementsThatSubclass,
+    getPolymerSubclassingDepthFrom: getPolymerSubclassingDepthFrom
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/properties.html b/trace-viewer/trace_viewer/base/properties.html
new file mode 100644
index 0000000..51a54e7
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/properties.html
@@ -0,0 +1,214 @@
+<!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/events.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  /**
+   * Fires a property change event on the target.
+   * @param {EventTarget} target The target to dispatch the event on.
+   * @param {string} propertyName The name of the property that changed.
+   * @param {*} newValue The new value for the property.
+   * @param {*} oldValue The old value for the property.
+   */
+  function dispatchPropertyChange(target, propertyName, newValue, oldValue,
+                                  opt_bubbles, opt_cancelable) {
+    var e = new tv.b.Event(propertyName + 'Change',
+                           opt_bubbles, opt_cancelable);
+    e.propertyName = propertyName;
+    e.newValue = newValue;
+    e.oldValue = oldValue;
+
+    var error;
+    e.throwError = function(err) {  // workaround CR 239648
+      error = err;
+    };
+
+    target.dispatchEvent(e);
+    if (error)
+      throw error;
+  }
+
+  function setPropertyAndDispatchChange(obj, propertyName, newValue) {
+    var privateName = propertyName + '_';
+    var oldValue = obj[propertyName];
+    obj[privateName] = newValue;
+    if (oldValue !== newValue)
+      tv.b.dispatchPropertyChange(obj, propertyName,
+          newValue, oldValue, true, false);
+  }
+
+  /**
+   * Converts a camelCase javascript property name to a hyphenated-lower-case
+   * attribute name.
+   * @param {string} jsName The javascript camelCase property name.
+   * @return {string} The equivalent hyphenated-lower-case attribute name.
+   */
+  function getAttributeName(jsName) {
+    return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
+  }
+
+  /* Creates a private name unlikely to collide with object properties names
+   * @param {string} name The defineProperty name
+   * @return {string} an obfuscated name
+   */
+  function getPrivateName(name) {
+    return name + '_tv_';
+  }
+
+  /**
+   * The kind of property to define in {@code defineProperty}.
+   * @enum {number}
+   * @const
+   */
+  var PropertyKind = {
+    /**
+     * Plain old JS property where the backing data is stored as a 'private'
+     * field on the object.
+     */
+    JS: 'js',
+
+    /**
+     * The property backing data is stored as an attribute on an element.
+     */
+    ATTR: 'attr',
+
+    /**
+     * The property backing data is stored as an attribute on an element. If the
+     * element has the attribute then the value is true.
+     */
+    BOOL_ATTR: 'boolAttr'
+  };
+
+  /**
+   * Helper function for defineProperty that returns the getter to use for the
+   * property.
+   * @param {string} name The name of the property.
+   * @param {tv.b.PropertyKind} kind The kind of the property.
+   * @return {function():*} The getter for the property.
+   */
+  function getGetter(name, kind) {
+    switch (kind) {
+      case PropertyKind.JS:
+        var privateName = getPrivateName(name);
+        return function() {
+          return this[privateName];
+        };
+      case PropertyKind.ATTR:
+        var attributeName = getAttributeName(name);
+        return function() {
+          return this.getAttribute(attributeName);
+        };
+      case PropertyKind.BOOL_ATTR:
+        var attributeName = getAttributeName(name);
+        return function() {
+          return this.hasAttribute(attributeName);
+        };
+    }
+  }
+
+  /**
+   * Helper function for defineProperty that returns the setter of the right
+   * kind.
+   * @param {string} name The name of the property we are defining the setter
+   *     for.
+   * @param {tv.b.PropertyKind} kind The kind of property we are getting the
+   *     setter for.
+   * @param {function(*):void=} opt_setHook A function to run after the property
+   *     is set, but before the propertyChange event is fired.
+   * @param {boolean=} opt_bubbles Whether the event bubbles or not.
+   * @param {boolean=} opt_cancelable Whether the default action of the event
+   *     can be prevented.
+   * @return {function(*):void} The function to use as a setter.
+   */
+  function getSetter(name, kind, opt_setHook, opt_bubbles, opt_cancelable) {
+    switch (kind) {
+      case PropertyKind.JS:
+        var privateName = getPrivateName(name);
+        return function(value) {
+          var oldValue = this[privateName];
+          if (value !== oldValue) {
+            this[privateName] = value;
+            if (opt_setHook)
+              opt_setHook.call(this, value, oldValue);
+            dispatchPropertyChange(this, name, value, oldValue,
+                opt_bubbles, opt_cancelable);
+          }
+        };
+
+      case PropertyKind.ATTR:
+        var attributeName = getAttributeName(name);
+        return function(value) {
+          var oldValue = this.getAttribute(attributeName);
+          if (value !== oldValue) {
+            if (value == undefined)
+              this.removeAttribute(attributeName);
+            else
+              this.setAttribute(attributeName, value);
+            if (opt_setHook)
+              opt_setHook.call(this, value, oldValue);
+            dispatchPropertyChange(this, name, value, oldValue,
+                opt_bubbles, opt_cancelable);
+          }
+        };
+
+      case PropertyKind.BOOL_ATTR:
+        var attributeName = getAttributeName(name);
+        return function(value) {
+          var oldValue = (this.getAttribute(attributeName) === name);
+          if (value !== oldValue) {
+            if (value)
+              this.setAttribute(attributeName, name);
+            else
+              this.removeAttribute(attributeName);
+            if (opt_setHook)
+              opt_setHook.call(this, value, oldValue);
+            dispatchPropertyChange(this, name, value, oldValue,
+                opt_bubbles, opt_cancelable);
+          }
+        };
+    }
+  }
+
+  /**
+   * Defines a property on an object. When the setter changes the value a
+   * property change event with the type {@code name + 'Change'} is fired.
+   * @param {!Object} obj The object to define the property for.
+   * @param {string} name The name of the property.
+   * @param {tv.b.PropertyKind=} opt_kind What kind of underlying storage to
+   * use.
+   * @param {function(*):void=} opt_setHook A function to run after the
+   *     property is set, but before the propertyChange event is fired.
+   * @param {boolean=} opt_bubbles Whether the event bubbles or not.
+   * @param {boolean=} opt_cancelable Whether the default action of the event
+   *     can be prevented.
+   */
+  function defineProperty(obj, name, opt_kind, opt_setHook,
+                          opt_bubbles, opt_cancelable) {
+    console.error("Don't use tv.b.defineProperty");
+    if (typeof obj == 'function')
+      obj = obj.prototype;
+
+    var kind = opt_kind || PropertyKind.JS;
+
+    if (!obj.__lookupGetter__(name))
+      obj.__defineGetter__(name, getGetter(name, kind));
+
+    if (!obj.__lookupSetter__(name))
+      obj.__defineSetter__(name, getSetter(name, kind, opt_setHook,
+          opt_bubbles, opt_cancelable));
+  }
+
+  return {
+    PropertyKind: PropertyKind,
+    defineProperty: defineProperty,
+    dispatchPropertyChange: dispatchPropertyChange,
+    setPropertyAndDispatchChange: setPropertyAndDispatchChange
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/properties_test.html b/trace-viewer/trace_viewer/base/properties_test.html
new file mode 100644
index 0000000..8d446b8
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/properties_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="/base/properties.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('defineProperties', function() {
+    var stateChanges = [];
+
+    var ASpan = tv.b.ui.define('span');
+    ASpan.prototype = {
+      __proto__: HTMLSpanElement.prototype,
+
+      jsProp_: [],
+
+      decorate: function() {
+        this.prop_ = false;
+        this.addEventListener('propChange', function(event) {
+          stateChanges.push('Internal ' + event.oldValue +
+              ' to ' + event.newValue);
+        }, true);
+      },
+
+      get prop() {
+        return this.prop_;
+      },
+
+      set prop(newValue) {
+        tv.b.setPropertyAndDispatchChange(this, 'prop', newValue);
+      }
+    };
+
+    var aSpan = new ASpan();
+
+    aSpan.addEventListener('propChange', function(event) {
+      stateChanges.push(event.oldValue + ' to ' + event.newValue);
+    });
+
+    assert.isFalse(aSpan.prop);
+
+    aSpan.prop = true;
+    assert.isTrue(aSpan.prop);
+    assert.strictEqual(stateChanges.length, 2);
+    assert.strictEqual(stateChanges[0], 'Internal false to true');
+    assert.strictEqual(stateChanges[1], 'false to true');
+
+    aSpan.prop = false;
+    assert.isFalse(aSpan.prop);
+    assert.strictEqual(stateChanges[3], 'true to false');
+  });
+});
+
+</script>
diff --git a/trace-viewer/trace_viewer/base/quad.html b/trace-viewer/trace_viewer/base/quad.html
new file mode 100644
index 0000000..81d03de
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/quad.html
@@ -0,0 +1,232 @@
+<!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/gl_matrix.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  var tmpVec2s = [];
+  for (var i = 0; i < 8; i++)
+    tmpVec2s[i] = vec2.create();
+
+  var tmpVec2a = vec4.create();
+  var tmpVec4a = vec4.create();
+  var tmpVec4b = vec4.create();
+  var tmpMat4 = mat4.create();
+  var tmpMat4b = mat4.create();
+
+  var p00 = vec2.createXY(0, 0);
+  var p10 = vec2.createXY(1, 0);
+  var p01 = vec2.createXY(0, 1);
+  var p11 = vec2.createXY(1, 1);
+
+  var lerpingVecA = vec2.create();
+  var lerpingVecB = vec2.create();
+  function lerpVec2(out, a, b, amt) {
+    vec2.scale(lerpingVecA, a, amt);
+    vec2.scale(lerpingVecB, b, 1 - amt);
+    vec2.add(out, lerpingVecA, lerpingVecB);
+    vec2.normalize(out, out);
+    return out;
+  }
+
+  /**
+   * @constructor
+   */
+  function Quad() {
+    this.p1 = vec2.create();
+    this.p2 = vec2.create();
+    this.p3 = vec2.create();
+    this.p4 = vec2.create();
+  }
+
+  Quad.fromXYWH = function(x, y, w, h) {
+    var q = new Quad();
+    vec2.set(q.p1, x, y);
+    vec2.set(q.p2, x + w, y);
+    vec2.set(q.p3, x + w, y + h);
+    vec2.set(q.p4, x, y + h);
+    return q;
+  }
+
+  Quad.fromRect = function(r) {
+    return new Quad.fromXYWH(
+        r.x, r.y,
+        r.width, r.height);
+  }
+
+  Quad.from4Vecs = function(p1, p2, p3, p4) {
+    var q = new Quad();
+    vec2.set(q.p1, p1[0], p1[1]);
+    vec2.set(q.p2, p2[0], p2[1]);
+    vec2.set(q.p3, p3[0], p3[1]);
+    vec2.set(q.p4, p4[0], p4[1]);
+    return q;
+  }
+
+  Quad.from8Array = function(arr) {
+    if (arr.length != 8)
+      throw new Error('Array must be 8 long');
+    var q = new Quad();
+    q.p1[0] = arr[0];
+    q.p1[1] = arr[1];
+    q.p2[0] = arr[2];
+    q.p2[1] = arr[3];
+    q.p3[0] = arr[4];
+    q.p3[1] = arr[5];
+    q.p4[0] = arr[6];
+    q.p4[1] = arr[7];
+    return q;
+  };
+
+  Quad.prototype = {
+    pointInside: function(point) {
+      return pointInImplicitQuad(point,
+                                 this.p1, this.p2, this.p3, this.p4);
+    },
+
+    boundingRect: function() {
+      var x0 = Math.min(this.p1[0], this.p2[0], this.p3[0], this.p4[0]);
+      var y0 = Math.min(this.p1[1], this.p2[1], this.p3[1], this.p4[1]);
+
+      var x1 = Math.max(this.p1[0], this.p2[0], this.p3[0], this.p4[0]);
+      var y1 = Math.max(this.p1[1], this.p2[1], this.p3[1], this.p4[1]);
+
+      return new tv.b.Rect.fromXYWH(x0, y0, x1 - x0, y1 - y0);
+    },
+
+    clone: function() {
+      var q = new Quad();
+      vec2.copy(q.p1, this.p1);
+      vec2.copy(q.p2, this.p2);
+      vec2.copy(q.p3, this.p3);
+      vec2.copy(q.p4, this.p4);
+      return q;
+    },
+
+    scale: function(s) {
+      var q = new Quad();
+      this.scaleFast(q, s);
+      return q;
+    },
+
+    scaleFast: function(dstQuad, s) {
+      vec2.copy(dstQuad.p1, this.p1, s);
+      vec2.copy(dstQuad.p2, this.p2, s);
+      vec2.copy(dstQuad.p3, this.p3, s);
+      vec2.copy(dstQuad.p3, this.p3, s);
+    },
+
+    isRectangle: function() {
+      // Simple rectangle check. Note: will not handle out-of-order components.
+      var bounds = this.boundingRect();
+      return (
+          bounds.x == this.p1[0] &&
+          bounds.y == this.p1[1] &&
+          bounds.width == this.p2[0] - this.p1[0] &&
+          bounds.y == this.p2[1] &&
+          bounds.width == this.p3[0] - this.p1[0] &&
+          bounds.height == this.p3[1] - this.p2[1] &&
+          bounds.x == this.p4[0] &&
+          bounds.height == this.p4[1] - this.p2[1]
+      );
+    },
+
+    projectUnitRect: function(rect) {
+      var q = new Quad();
+      this.projectUnitRectFast(q, rect);
+      return q;
+    },
+
+    projectUnitRectFast: function(dstQuad, rect) {
+      var v12 = tmpVec2s[0];
+      var v14 = tmpVec2s[1];
+      var v23 = tmpVec2s[2];
+      var v43 = tmpVec2s[3];
+      var l12, l14, l23, l43;
+
+      vec2.sub(v12, this.p2, this.p1);
+      l12 = vec2.length(v12);
+      vec2.scale(v12, v12, 1 / l12);
+
+      vec2.sub(v14, this.p4, this.p1);
+      l14 = vec2.length(v14);
+      vec2.scale(v14, v14, 1 / l14);
+
+      vec2.sub(v23, this.p3, this.p2);
+      l23 = vec2.length(v23);
+      vec2.scale(v23, v23, 1 / l23);
+
+      vec2.sub(v43, this.p3, this.p4);
+      l43 = vec2.length(v43);
+      vec2.scale(v43, v43, 1 / l43);
+
+      var b12 = tmpVec2s[0];
+      var b14 = tmpVec2s[1];
+      var b23 = tmpVec2s[2];
+      var b43 = tmpVec2s[3];
+      lerpVec2(b12, v12, v43, rect.y);
+      lerpVec2(b43, v12, v43, 1 - rect.bottom);
+      lerpVec2(b14, v14, v23, rect.x);
+      lerpVec2(b23, v14, v23, 1 - rect.right);
+
+      vec2.addTwoScaledUnitVectors(tmpVec2a,
+                                   b12, l12 * rect.x,
+                                   b14, l14 * rect.y);
+      vec2.add(dstQuad.p1, this.p1, tmpVec2a);
+
+      vec2.addTwoScaledUnitVectors(tmpVec2a,
+                                   b12, l12 * -(1.0 - rect.right),
+                                   b23, l23 * rect.y);
+      vec2.add(dstQuad.p2, this.p2, tmpVec2a);
+
+
+      vec2.addTwoScaledUnitVectors(tmpVec2a,
+                                   b43, l43 * -(1.0 - rect.right),
+                                   b23, l23 * -(1.0 - rect.bottom));
+      vec2.add(dstQuad.p3, this.p3, tmpVec2a);
+
+      vec2.addTwoScaledUnitVectors(tmpVec2a,
+                                   b43, l43 * rect.left,
+                                   b14, l14 * -(1.0 - rect.bottom));
+      vec2.add(dstQuad.p4, this.p4, tmpVec2a);
+    },
+
+    toString: function() {
+      return 'Quad(' +
+          vec2.toString(this.p1) + ', ' +
+          vec2.toString(this.p2) + ', ' +
+          vec2.toString(this.p3) + ', ' +
+          vec2.toString(this.p4) + ')';
+    }
+  };
+
+  function sign(p1, p2, p3) {
+    return (p1[0] - p3[0]) * (p2[1] - p3[1]) -
+        (p2[0] - p3[0]) * (p1[1] - p3[1]);
+  }
+
+  function pointInTriangle2(pt, p1, p2, p3) {
+    var b1 = sign(pt, p1, p2) < 0.0;
+    var b2 = sign(pt, p2, p3) < 0.0;
+    var b3 = sign(pt, p3, p1) < 0.0;
+    return ((b1 == b2) && (b2 == b3));
+  }
+
+  function pointInImplicitQuad(point, p1, p2, p3, p4) {
+    return pointInTriangle2(point, p1, p2, p3) ||
+        pointInTriangle2(point, p1, p3, p4);
+  }
+
+  return {
+    pointInTriangle2: pointInTriangle2,
+    pointInImplicitQuad: pointInImplicitQuad,
+    Quad: Quad
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/quad_test.html b/trace-viewer/trace_viewer/base/quad_test.html
new file mode 100644
index 0000000..01d705b
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/quad_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="/base/rect.html">
+<link rel="import" href="/base/quad.html">
+<script>
+'use strict';
+
+function assertQuadEquals(a, b, opt_message) {
+  var ok = true;
+  ok &= a.p1[0] === b.p1[0] && a.p1[1] === b.p1[1];
+  ok &= a.p2[0] === b.p2[0] && a.p2[1] === b.p2[1];
+  ok &= a.p3[0] === b.p3[0] && a.p3[1] === b.p3[1];
+  ok &= a.p4[0] === b.p4[0] && a.p4[1] === b.p4[1];
+  if (ok)
+    return;
+  var message = opt_message || 'Expected "' + a.toString() +
+      '", got "' + b.toString() + '"';
+  throw new tv.b.unittest.TestError(message);
+}
+
+tv.b.unittest.testSuite(function() {
+  test('pointInTri', function() {
+    var res = tv.b.pointInTriangle2(
+        [0.25, 0.25],
+        [0, 0],
+        [1, 0],
+        [0, 1]);
+    assert.isTrue(res);
+  });
+
+  test('pointNotInTri', function() {
+    var res = tv.b.pointInTriangle2(
+        [0.75, 0.75],
+        [0, 0],
+        [1, 0],
+        [0, 1]);
+    assert.isFalse(res);
+  });
+
+  test('pointInside', function() {
+    var q = tv.b.Quad.from4Vecs([0, 0],
+                                [1, 0],
+                                [1, 1],
+                                [0, 1]);
+    var res = q.pointInside([0.5, 0.5]);
+    assert.isTrue(res);
+  });
+
+  test('pointNotInQuad', function() {
+    var q = tv.b.Quad.from4Vecs([0, 0],
+                                [1, 0],
+                                [1, 1],
+                                [0, 1]);
+    var res = q.pointInside([1.5, 0.5]);
+    assert.isFalse(res);
+  });
+
+  test('isRectangle', function() {
+    assert.isTrue(tv.b.Quad.fromXYWH(0, 0, 10, 10).isRectangle());
+    assert.isTrue(tv.b.Quad.fromXYWH(-10, -10, 5, 5).isRectangle());
+    assert.isTrue(tv.b.Quad.fromXYWH(-10, -10, 20, 20).isRectangle());
+    assert.isTrue(tv.b.Quad.fromXYWH(-10, 10, 5, 5).isRectangle());
+
+    assert.isFalse(tv.b.Quad.fromXYWH(0, 0, -10, -10).isRectangle());
+    assert.isFalse(
+        tv.b.Quad.from8Array([0, 1, 2, 3, 4, 5, 6, 7]).isRectangle());
+    assert.isFalse(
+        tv.b.Quad.from8Array([0, 0, 0, 5, 5, 5, 0, 0]).isRectangle());
+  });
+
+  test('projectUnitRect', function() {
+    var container = tv.b.Quad.fromXYWH(0, 0, 10, 10);
+    var srcRect = tv.b.Rect.fromXYWH(0.1, 0.8, 0.8, 0.1);
+    var expectedRect = srcRect.scale(10);
+
+    var q = new tv.b.Quad();
+    container.projectUnitRectFast(q, srcRect);
+
+    assertQuadEquals(tv.b.Quad.fromRect(expectedRect), q);
+  });
+
+  test('projectUnitRectOntoUnitQuad', function() {
+    var container = tv.b.Quad.fromXYWH(0, 0, 1, 1);
+    var srcRect = tv.b.Rect.fromXYWH(0.0, 0, 1, 1);
+    var expectedRect = srcRect;
+
+    var q = new tv.b.Quad();
+    container.projectUnitRectFast(q, srcRect);
+
+    assertQuadEquals(tv.b.Quad.fromRect(expectedRect), q);
+  });
+
+  test('projectUnitRectOntoSizeTwoQuad', function() {
+    var container = tv.b.Quad.fromXYWH(0, 0, 2, 2);
+    var srcRect = tv.b.Rect.fromXYWH(0.0, 0, 1, 1);
+    var expectedRect = srcRect.scale(2);
+
+    var q = new tv.b.Quad();
+    container.projectUnitRectFast(q, srcRect);
+
+    assertQuadEquals(tv.b.Quad.fromRect(expectedRect), q);
+  });
+
+  test('projectUnitRectOntoTranslatedQuad', function() {
+    var container = tv.b.Quad.fromXYWH(1, 1, 1, 1);
+    var srcRect = tv.b.Rect.fromXYWH(0.0, 0, 1, 1);
+    var expectedRect = srcRect.translate([1, 1]);
+
+    var q = new tv.b.Quad();
+    container.projectUnitRectFast(q, srcRect);
+
+    assertQuadEquals(tv.b.Quad.fromRect(expectedRect), q);
+  });
+
+  test('projectShrunkUnitRectOntoUnitQuad', function() {
+    var container = tv.b.Quad.fromXYWH(0, 0, 1, 1);
+    var srcRect = tv.b.Rect.fromXYWH(0.1, 0.1, 0.8, 0.8);
+    var expectedRect = srcRect;
+
+    var q = new tv.b.Quad();
+    container.projectUnitRectFast(q, srcRect);
+
+    assertQuadEquals(tv.b.Quad.fromRect(expectedRect), q);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/raf.html b/trace-viewer/trace_viewer/base/raf.html
new file mode 100644
index 0000000..e642de2
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/raf.html
@@ -0,0 +1,154 @@
+<!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/utils.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  // Setting this to true will cause stack traces to get dumped into the
+  // tasks. When an exception happens the original stack will be printed.
+  //
+  // NOTE: This should never be set committed as true.
+  var recordRAFStacks = false;
+
+  var pendingPreAFs = [];
+  var pendingRAFs = [];
+  var pendingIdleCallbacks = [];
+  var currentRAFDispatchList = undefined;
+
+  var rafScheduled = false;
+
+  function scheduleRAF() {
+    if (rafScheduled)
+      return;
+    rafScheduled = true;
+    if (window.requestAnimationFrame) {
+      window.requestAnimationFrame(processRequests);
+    } else {
+      var delta = Date.now() - window.performance.now();
+      window.webkitRequestAnimationFrame(function(domTimeStamp) {
+        processRequests(domTimeStamp - delta);
+      });
+    }
+  }
+
+  function onAnimationFrameError(e, opt_stack) {
+    if (opt_stack)
+      console.log(opt_stack);
+
+    if (e.message)
+      console.error(e.message, e.stack);
+    else
+      console.error(e);
+  }
+
+  function runTask(task, frameBeginTime) {
+    try {
+      task.callback.call(task.context, frameBeginTime);
+    } catch (e) {
+      tv.b.onAnimationFrameError(e, task.stack);
+    }
+  }
+
+  function processRequests(frameBeginTime) {
+    // We assume that we want to do a maximum of 10ms optional work per frame.
+    // Hopefully rAF will eventually pass this in for us.
+    var rafCompletionDeadline = frameBeginTime + 10;
+
+    rafScheduled = false;
+
+    var currentPreAFs = pendingPreAFs;
+    currentRAFDispatchList = pendingRAFs;
+    pendingPreAFs = [];
+    pendingRAFs = [];
+    var hasRAFTasks = currentPreAFs.length || currentRAFDispatchList.length;
+
+    for (var i = 0; i < currentPreAFs.length; i++)
+      runTask(currentPreAFs[i], frameBeginTime);
+
+    while (currentRAFDispatchList.length > 0)
+      runTask(currentRAFDispatchList.shift(), frameBeginTime);
+    currentRAFDispatchList = undefined;
+
+    if (!hasRAFTasks) {
+      while (pendingIdleCallbacks.length > 0) {
+        runTask(pendingIdleCallbacks.shift());
+        // Check timer after running at least one idle task to avoid buggy
+        // window.performance.now() on some platforms from blocking the idle
+        // task queue.
+        if (window.performance.now() >= rafCompletionDeadline)
+          break;
+      }
+    }
+
+    if (pendingIdleCallbacks.length > 0)
+      scheduleRAF();
+  }
+
+  function getStack_() {
+    if (!recordRAFStacks)
+      return '';
+
+    var stackLines = tv.b.stackTrace();
+    // Strip off getStack_.
+    stackLines.shift();
+    return stackLines.join('\n');
+  }
+
+  function requestPreAnimationFrame(callback, opt_this) {
+    pendingPreAFs.push({
+      callback: callback,
+      context: opt_this || window,
+      stack: getStack_()});
+    scheduleRAF();
+  }
+
+  function requestAnimationFrameInThisFrameIfPossible(callback, opt_this) {
+    if (!currentRAFDispatchList) {
+      requestAnimationFrame(callback, opt_this);
+      return;
+    }
+    currentRAFDispatchList.push({
+      callback: callback,
+      context: opt_this || window,
+      stack: getStack_()});
+    return;
+  }
+
+  function requestAnimationFrame(callback, opt_this) {
+    pendingRAFs.push({
+      callback: callback,
+      context: opt_this || window,
+      stack: getStack_()});
+    scheduleRAF();
+  }
+
+  function requestIdleCallback(callback, opt_this) {
+    pendingIdleCallbacks.push({
+      callback: callback,
+      context: opt_this || window,
+      stack: getStack_()});
+    scheduleRAF();
+  }
+
+  function forcePendingRAFTasksToRun(frameBeginTime) {
+    if (!rafScheduled)
+      return;
+    processRequests(frameBeginTime);
+  }
+
+  return {
+    onAnimationFrameError: onAnimationFrameError,
+    requestPreAnimationFrame: requestPreAnimationFrame,
+    requestAnimationFrame: requestAnimationFrame,
+    requestAnimationFrameInThisFrameIfPossible:
+        requestAnimationFrameInThisFrameIfPossible,
+    requestIdleCallback: requestIdleCallback,
+    forcePendingRAFTasksToRun: forcePendingRAFTasksToRun
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/raf_test.html b/trace-viewer/trace_viewer/base/raf_test.html
new file mode 100644
index 0000000..341b832
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/raf_test.html
@@ -0,0 +1,125 @@
+<!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/raf.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var fakeNow = undefined;
+  function withFakeWindowPerformanceNow(func) {
+    var oldNow = window.performance.now;
+    try {
+      window.performance.now = function() { return fakeNow; };
+      func();
+    } finally {
+      window.performance.now = oldNow;
+    }
+  }
+
+  test('runIdleTaskWhileIdle', function() {
+    withFakeWindowPerformanceNow(function() {
+      tv.b.forcePendingRAFTasksToRun(100000);  // Clear current RAF task queue.
+
+      var rafRan = false;
+      tv.b.requestAnimationFrame(function() {
+        rafRan = true;
+      });
+      var idleRan = false;
+      tv.b.requestIdleCallback(function() {
+        idleRan = true;
+      });
+      fakeNow = 0;
+      tv.b.forcePendingRAFTasksToRun(fakeNow);
+      assert.isFalse(idleRan);
+      assert.isTrue(rafRan);
+      tv.b.forcePendingRAFTasksToRun(fakeNow);
+      assert.isTrue(idleRan);
+    });
+  });
+
+  test('twoShortIdleCallbacks', function() {
+    withFakeWindowPerformanceNow(function() {
+      tv.b.forcePendingRAFTasksToRun(100000);  // Clear current RAF task queue.
+
+      var idle1Ran = false;
+      var idle2Ran = false;
+      tv.b.requestIdleCallback(function() {
+        fakeNow += 1;
+        idle1Ran = true;
+      });
+      tv.b.requestIdleCallback(function() {
+        fakeNow += 1;
+        idle2Ran = true;
+      });
+      fakeNow = 0;
+      tv.b.forcePendingRAFTasksToRun(fakeNow);
+      assert.isTrue(idle1Ran);
+      assert.isTrue(idle2Ran);
+    });
+  });
+
+
+  test('oneLongOneShortIdleCallback', function() {
+    withFakeWindowPerformanceNow(function() {
+      tv.b.forcePendingRAFTasksToRun(100000);  // Clear current RAF task queue.
+
+      var idle1Ran = false;
+      var idle2Ran = false;
+      tv.b.requestIdleCallback(function() {
+        fakeNow += 100;
+        idle1Ran = true;
+      });
+      tv.b.requestIdleCallback(function() {
+        fakeNow += 1;
+        idle2Ran = true;
+      });
+      fakeNow = 0;
+      tv.b.forcePendingRAFTasksToRun(fakeNow);
+      assert.isTrue(idle1Ran);
+      assert.isFalse(idle2Ran);
+
+      // Reset idle1Ran to verify that it dosn't run again.
+      idle1Ran = false;
+
+      // Now run. idle2 should now run.
+      tv.b.forcePendingRAFTasksToRun(fakeNow);
+      assert.isFalse(idle1Ran);
+      assert.isTrue(idle2Ran);
+    });
+  });
+
+  test('buggyPerformanceNowDoesNotBlockIdleTasks', function() {
+    withFakeWindowPerformanceNow(function() {
+      tv.b.forcePendingRAFTasksToRun();  // Clear current RAF task queue.
+
+      var idle1Ran = false;
+      var idle2Ran = false;
+      tv.b.requestIdleCallback(function() {
+        fakeNow += 100;
+        idle1Ran = true;
+      });
+      tv.b.requestIdleCallback(function() {
+        fakeNow += 1;
+        idle2Ran = true;
+      });
+      fakeNow = 10000;
+      tv.b.forcePendingRAFTasksToRun(0);
+      assert.isTrue(idle1Ran);
+      assert.isFalse(idle2Ran);
+
+      // Reset idle1Ran to verify that it dosn't run again.
+      idle1Ran = false;
+
+      // Now run. idle2 should now run.
+      tv.b.forcePendingRAFTasksToRun(0);
+      assert.isFalse(idle1Ran);
+      assert.isTrue(idle2Ran);
+    });
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/range.html b/trace-viewer/trace_viewer/base/range.html
new file mode 100644
index 0000000..7115940
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/range.html
@@ -0,0 +1,139 @@
+<!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';
+
+/**
+ * @fileoverview Quick range computations.
+ */
+tv.exportTo('tv.b', function() {
+
+  function Range() {
+    this.isEmpty_ = true;
+    this.min_ = undefined;
+    this.max_ = undefined;
+  };
+
+  Range.prototype = {
+    __proto__: Object.prototype,
+
+    reset: function() {
+      this.isEmpty_ = true;
+      this.min_ = undefined;
+      this.max_ = undefined;
+    },
+
+    get isEmpty() {
+      return this.isEmpty_;
+    },
+
+    addRange: function(range) {
+      if (range.isEmpty)
+        return;
+      this.addValue(range.min);
+      this.addValue(range.max);
+    },
+
+    addValue: function(value) {
+      if (this.isEmpty_) {
+        this.max_ = value;
+        this.min_ = value;
+        this.isEmpty_ = false;
+        return;
+      }
+      this.max_ = Math.max(this.max_, value);
+      this.min_ = Math.min(this.min_, value);
+    },
+
+    set min(min) {
+      this.isEmpty_ = false;
+      this.min_ = min;
+    },
+
+    get min() {
+      if (this.isEmpty_)
+        return undefined;
+      return this.min_;
+    },
+
+    get max() {
+      if (this.isEmpty_)
+        return undefined;
+      return this.max_;
+    },
+
+    set max(max) {
+      this.isEmpty_ = false;
+      this.max_ = max;
+    },
+
+    get range() {
+      if (this.isEmpty_)
+        return undefined;
+      return this.max_ - this.min_;
+    },
+
+    get center() {
+      return (this.min_ + this.max_) * 0.5;
+    },
+
+    equals: function(that) {
+      if (this.isEmpty && that.isEmpty)
+        return true;
+      if (this.isEmpty != that.isEmpty)
+        return false;
+      return this.min === that.min &&
+          this.max === that.max;
+    },
+
+    containsRange: function(range) {
+      if (this.isEmpty || range.isEmpty)
+        return false;
+      return this.min <= range.min && this.max >= range.max;
+    },
+
+    containsExplicitRange: function(min, max) {
+      if (this.isEmpty)
+        return false;
+      return this.min <= min && this.max >= max;
+    },
+
+    intersectsRange: function(range) {
+      if (this.isEmpty || range.isEmpty)
+        return false;
+      return !(range.max < this.min ||
+               range.min > this.max);
+    },
+
+    intersectsExplicitRange: function(min, max) {
+      if (this.isEmpty)
+        return false;
+      return !(max < this.min ||
+               min > this.max);
+    }
+  };
+
+  Range.compareByMinTimes = function(a, b) {
+    if (!a.isEmpty && !b.isEmpty)
+      return a.min_ - b.min_;
+
+    if (a.isEmpty && !b.isEmpty)
+      return -1;
+
+    if (!a.isEmpty && b.isEmpty)
+      return 1;
+
+    return 0;
+  };
+
+  return {
+    Range: Range
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/range_test.html b/trace-viewer/trace_viewer/base/range_test.html
new file mode 100644
index 0000000..e69fe0c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/range_test.html
@@ -0,0 +1,61 @@
+<!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.b.unittest.testSuite(function() {
+  test('addValue', function() {
+    var range = new tv.b.Range();
+    assert.isTrue(range.isEmpty);
+    range.addValue(1);
+    assert.isFalse(range.isEmpty);
+    assert.equal(1, range.min);
+    assert.equal(1, range.max);
+
+    range.addValue(2);
+    assert.isFalse(range.isEmpty);
+    assert.equal(1, range.min);
+    assert.equal(2, range.max);
+  });
+
+  test('addNonEmptyRange', function() {
+    var r1 = new tv.b.Range();
+    r1.addValue(1);
+    r1.addValue(2);
+
+    var r = new tv.b.Range();
+    r.addRange(r1);
+    assert.equal(1, r.min);
+    assert.equal(2, r.max);
+  });
+
+  test('addEmptyRange', function() {
+    var r1 = new tv.b.Range();
+
+    var r = new tv.b.Range();
+    r.addRange(r1);
+    assert.isTrue(r.isEmpty);
+    assert.isUndefined(r.min);
+    assert.isUndefined(r.max);
+  });
+
+  test('addRangeToRange', function() {
+    var r1 = new tv.b.Range();
+    r1.addValue(1);
+    r1.addValue(2);
+
+    var r = new tv.b.Range();
+    r.addValue(3);
+    r.addRange(r1);
+
+    assert.isFalse(r.isEmpty);
+    assert.equal(1, r.min);
+    assert.equal(3, r.max);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/rect.html b/trace-viewer/trace_viewer/base/rect.html
new file mode 100644
index 0000000..d2e628c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/rect.html
@@ -0,0 +1,166 @@
+<!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/gl_matrix.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview 2D Rectangle math.
+ */
+
+tv.exportTo('tv.b', function() {
+
+  /**
+   * Tracks a 2D bounding box.
+   * @constructor
+   */
+  function Rect() {
+    this.x = 0;
+    this.y = 0;
+    this.width = 0;
+    this.height = 0;
+  };
+  Rect.fromXYWH = function(x, y, w, h) {
+    var rect = new Rect();
+    rect.x = x;
+    rect.y = y;
+    rect.width = w;
+    rect.height = h;
+    return rect;
+  }
+  Rect.fromArray = function(ary) {
+    if (ary.length != 4)
+      throw new Error('ary.length must be 4');
+    var rect = new Rect();
+    rect.x = ary[0];
+    rect.y = ary[1];
+    rect.width = ary[2];
+    rect.height = ary[3];
+    return rect;
+  }
+
+  Rect.prototype = {
+    __proto__: Object.prototype,
+
+    get left() {
+      return this.x;
+    },
+
+    get top() {
+      return this.y;
+    },
+
+    get right() {
+      return this.x + this.width;
+    },
+
+    get bottom() {
+      return this.y + this.height;
+    },
+
+    toString: function() {
+      return 'Rect(' + this.x + ', ' + this.y + ', ' +
+          this.width + ', ' + this.height + ')';
+    },
+
+    toArray: function() {
+      return [this.x, this.y, this.width, this.height];
+    },
+
+    clone: function() {
+      var rect = new Rect();
+      rect.x = this.x;
+      rect.y = this.y;
+      rect.width = this.width;
+      rect.height = this.height;
+      return rect;
+    },
+
+    enlarge: function(pad) {
+      var rect = new Rect();
+      this.enlargeFast(rect, pad);
+      return rect;
+    },
+
+    enlargeFast: function(out, pad) {
+      out.x = this.x - pad;
+      out.y = this.y - pad;
+      out.width = this.width + 2 * pad;
+      out.height = this.height + 2 * pad;
+      return out;
+    },
+
+    size: function() {
+      return {width: this.width, height: this.height};
+    },
+
+    scale: function(s) {
+      var rect = new Rect();
+      this.scaleFast(rect, s);
+      return rect;
+    },
+
+    scaleSize: function(s) {
+      return Rect.fromXYWH(this.x, this.y, this.width * s, this.height * s);
+    },
+
+    scaleFast: function(out, s) {
+      out.x = this.x * s;
+      out.y = this.y * s;
+      out.width = this.width * s;
+      out.height = this.height * s;
+      return out;
+    },
+
+    translate: function(v) {
+      var rect = new Rect();
+      this.translateFast(rect, v);
+      return rect;
+    },
+
+    translateFast: function(out, v) {
+      out.x = this.x + v[0];
+      out.y = this.x + v[1];
+      out.width = this.width;
+      out.height = this.height;
+      return out;
+    },
+
+    asUVRectInside: function(containingRect) {
+      var rect = new Rect();
+      rect.x = (this.x - containingRect.x) / containingRect.width;
+      rect.y = (this.y - containingRect.y) / containingRect.height;
+      rect.width = this.width / containingRect.width;
+      rect.height = this.height / containingRect.height;
+      return rect;
+    },
+
+    intersects: function(that) {
+      var ok = true;
+      ok &= this.x < that.right;
+      ok &= this.right > that.x;
+      ok &= this.y < that.bottom;
+      ok &= this.bottom > that.y;
+      return ok;
+    },
+
+    equalTo: function(rect) {
+      return rect &&
+             (this.x === rect.x) &&
+             (this.y === rect.y) &&
+             (this.width === rect.width) &&
+             (this.height === rect.height);
+    }
+  };
+
+  return {
+    Rect: Rect
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/rect_test.html b/trace-viewer/trace_viewer/base/rect_test.html
new file mode 100644
index 0000000..f9bffb0
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/rect_test.html
@@ -0,0 +1,31 @@
+<!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/rect.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('UVRectBasic', function() {
+    function assertRectEquals(a, b, opt_message) {
+      var ok = true;
+      if (a.x === b.x && a.y === b.y &&
+          a.width === b.width && a.height === b.height) {
+        return;
+      }
+      var message = opt_message || 'Expected "' + a.toString() +
+          '", got "' + b.toString() + '"';
+      throw new tv.b.unittest.TestError(message);
+    }
+    var container = tv.b.Rect.fromXYWH(0, 0, 10, 10);
+    var inner = tv.b.Rect.fromXYWH(1, 1, 8, 8);
+    var uv = inner.asUVRectInside(container);
+    assertRectEquals(uv, tv.b.Rect.fromXYWH(0.1, 0.1, .8, .8));
+    assert.equal(10, container.size().width);
+    assert.equal(10, container.size().height);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/settings.html b/trace-viewer/trace_viewer/base/settings.html
new file mode 100644
index 0000000..c0c6726
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/settings.html
@@ -0,0 +1,142 @@
+<!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';
+
+/**
+ * @fileoverview Provides the Settings object.
+ */
+tv.exportTo('tv.b', function() {
+  /**
+   * Settings is a simple wrapper around local storage, to make it easier
+   * to test classes that have settings.
+   *
+   * May be called as new tv.b.Settings() or simply tv.b.Settings()
+   * @constructor
+   */
+  function Settings() {
+    return Settings;
+  };
+
+  document.head.addEventListener('tv-unittest-will-run', function() {
+    Settings.setAlternativeStorageInstance(global.sessionStorage);
+  });
+
+  function SessionSettings() {
+    return SessionSettings;
+  }
+
+  function AddStaticStorageFunctionsToClass_(input_class, storage) {
+    input_class.storage_ = storage;
+
+    /**
+     * Get the setting with the given name.
+     *
+     * @param {string} key The name of the setting.
+     * @param {string=} opt_default The default value to return if not set.
+     * @param {string=} opt_namespace If set, the setting name will be prefixed
+     * with this namespace, e.g. "categories.settingName". This is useful for
+     * a set of related settings.
+     */
+    input_class.get = function(key, opt_default, opt_namespace) {
+      key = input_class.namespace_(key, opt_namespace);
+      var rawVal = input_class.storage_.getItem(key);
+      if (rawVal === null || rawVal === undefined)
+        return opt_default;
+
+      // Old settings versions used to stringify objects instead of putting them
+      // into JSON. If those are encountered, parse will fail. In that case,
+      // "upgrade" the setting to the default value.
+      try {
+        return JSON.parse(rawVal).value;
+      } catch (e) {
+        input_class.storage_.removeItem(
+            input_class.namespace_(key, opt_namespace));
+        return opt_default;
+      }
+    };
+
+    /**
+     * Set the setting with the given name to the given value.
+     *
+     * @param {string} key The name of the setting.
+     * @param {string} value The value of the setting.
+     * @param {string=} opt_namespace If set, the setting name will be prefixed
+     * with this namespace, e.g. "categories.settingName". This is useful for
+     * a set of related settings.
+     */
+    input_class.set = function(key, value, opt_namespace) {
+      if (value === undefined)
+        throw new Error('Settings.set: value must not be undefined');
+      var v = JSON.stringify({value: value});
+      input_class.storage_.setItem(
+          input_class.namespace_(key, opt_namespace), v);
+    };
+
+    /**
+     * Return a list of all the keys, or all the keys in the given namespace
+     * if one is provided.
+     *
+     * @param {string=} opt_namespace If set, only return settings which
+     * begin with this prefix.
+     */
+    input_class.keys = function(opt_namespace) {
+      var result = [];
+      opt_namespace = opt_namespace || '';
+      for (var i = 0; i < input_class.storage_.length; i++) {
+        var key = input_class.storage_.key(i);
+        if (input_class.isnamespaced_(key, opt_namespace))
+          result.push(input_class.unnamespace_(key, opt_namespace));
+      }
+      return result;
+    };
+
+    input_class.isnamespaced_ = function(key, opt_namespace) {
+      return key.indexOf(input_class.normalize_(opt_namespace)) == 0;
+    };
+
+    input_class.namespace_ = function(key, opt_namespace) {
+      return input_class.normalize_(opt_namespace) + key;
+    };
+
+    input_class.unnamespace_ = function(key, opt_namespace) {
+      return key.replace(input_class.normalize_(opt_namespace), '');
+    };
+
+    /**
+     * All settings are prefixed with a global namespace to avoid collisions.
+     * input_class may also be namespaced with an additional prefix passed into
+     * the get, set, and keys methods in order to group related settings.
+     * This method makes sure the two namespaces are always set properly.
+     */
+    input_class.normalize_ = function(opt_namespace) {
+      return input_class.NAMESPACE + (opt_namespace ? opt_namespace + '.' : '');
+    };
+
+    input_class.setAlternativeStorageInstance = function(instance) {
+      input_class.storage_ = instance;
+    };
+
+    input_class.getAlternativeStorageInstance = function() {
+      if (input_class.storage_ === localStorage)
+        return undefined;
+      return input_class.storage_;
+    };
+
+    input_class.NAMESPACE = 'trace-viewer';
+  };
+
+  AddStaticStorageFunctionsToClass_(Settings, localStorage);
+  AddStaticStorageFunctionsToClass_(SessionSettings, sessionStorage);
+
+  return {
+    Settings: Settings,
+    SessionSettings: SessionSettings
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/settings_test.html b/trace-viewer/trace_viewer/base/settings_test.html
new file mode 100644
index 0000000..2d3d4f1
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/settings_test.html
@@ -0,0 +1,113 @@
+<!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/settings.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function assertSettingIs(expectedValue, key) {
+    assert.equal(tv.b.Settings.get(key), expectedValue);
+  }
+
+  // Old settings versions used to stringify objects instead of putting them
+  // into JSON. This test makes sure that these old settings yield the default
+  // value instead of strings.
+  test('oldStyleSettingYieldsDefaultValue', function() {
+    var storage = tv.b.Settings.getAlternativeStorageInstance();
+    storage.setItem(tv.b.Settings.namespace_('key'), 'hello world');
+
+    assert.equal(tv.b.Settings.get('key', 'value'), 'value');
+  });
+
+  test('setGetString', function() {
+    var settings = new tv.b.Settings();
+    settings.set('my_key', 'my_val');
+    assert.equal(settings.get('my_key'), 'my_val');
+    // tv.b.Settings() is a singleton
+    assert.equal(tv.b.Settings().get('my_key'), 'my_val');
+  });
+
+  test('setGetNumber', function() {
+    var settings = new tv.b.Settings();
+    settings.set('my_key', 5);
+    assertSettingIs(5, 'my_key');
+  });
+
+  test('setGetBool', function() {
+    var settings = new tv.b.Settings();
+    settings.set('my_key', false);
+    assertSettingIs(false, 'my_key');
+  });
+
+  test('setGetObject', function() {
+    var settings = new tv.b.Settings();
+    settings.set('my_key', {'hello': 5});
+    assert.deepEqual(settings.get('my_key'), {'hello': 5});
+  });
+
+  test('setInvalidObject', function() {
+    var settings = new tv.b.Settings();
+    var obj = {'hello': undefined};
+    obj.hello = obj;
+    assert.throws(function() {
+      settings.set('my_key', obj);
+    });
+  });
+
+  test('setUndefined', function() {
+    var settings = new tv.b.Settings();
+    assert.throws(function() {
+      settings.set('my_key', undefined);
+    });
+  });
+
+  test('getUnset', function() {
+    var settings = new tv.b.Settings();
+    // Undefined should be returned if value isn't set.
+    assertSettingIs(undefined, 'my_key');
+  });
+
+  test('getDefault', function() {
+    var settings = new tv.b.Settings();
+    // default_val should be returned if value isn't set.
+    assert.equal(settings.get('my_key', 'default_val'), 'default_val');
+  });
+
+  test('setGetPrefix', function() {
+    var settings = new tv.b.Settings();
+    settings.set('key_a', 'foo', 'my_prefix');
+    assert.equal(settings.get('key_a', undefined, 'my_prefix'), 'foo');
+    assert.equal(settings.get('key_a', 'bar', 'my_prefix'), 'foo');
+    assert.isUndefined(settings.get('key_a'));
+    assert.equal(settings.get('key_a', 'bar'), 'bar');
+  });
+
+  test('keys', function() {
+    var settings = new tv.b.Settings();
+    settings.set('key_a', 'foo');
+    settings.set('key_b', 'bar');
+    settings.set('key_c', 'baz');
+    assert.deepEqual(settings.keys(), ['key_a', 'key_b', 'key_c']);
+  });
+
+  test('keysPrefix', function() {
+    var settings = new tv.b.Settings();
+    settings.set('key_a', 'foo', 'prefix1');
+    settings.set('key_b', 'bar', 'prefix1');
+    settings.set('key_c', 'baz', 'prefix1');
+    settings.set('key_a', 'foo', 'prefix2');
+    settings.set('key_b', 'bar', 'prefix2');
+    settings.set('key_C', 'baz', 'prefix2');
+    assert.deepEqual(settings.keys('prefix1'), ['key_a', 'key_b', 'key_c']);
+    assert.deepEqual(settings.keys('prefix2'), ['key_C', 'key_a', 'key_b']);
+    assert.deepEqual(
+        settings.keys(),
+        ['prefix1.key_a', 'prefix1.key_b', 'prefix1.key_c',
+         'prefix2.key_C', 'prefix2.key_a', 'prefix2.key_b']);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/sorted_array_utils.html b/trace-viewer/trace_viewer/base/sorted_array_utils.html
new file mode 100644
index 0000000..649c53c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/sorted_array_utils.html
@@ -0,0 +1,264 @@
+<!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';
+
+/**
+ * @fileoverview Helper functions for doing intersections and iteration
+ * over sorted arrays and intervals.
+ *
+ */
+tv.exportTo('tv.b', function() {
+  /**
+   * Finds the first index in the array whose value is >= loVal.
+   *
+   * The key for the search is defined by the mapFn. This array must
+   * be prearranged such that ary.map(mapFn) would also be sorted in
+   * ascending order.
+   *
+   * @param {Array} ary An array of arbitrary objects.
+   * @param {function():*} mapFn Callback that produces a key value
+   *     from an element in ary.
+   * @param {number} loVal Value for which to search.
+   * @return {Number} Offset o into ary where all ary[i] for i <= o
+   *     are < loVal, or ary.length if loVal is greater than all elements in
+   *     the array.
+   */
+  function findLowIndexInSortedArray(ary, mapFn, loVal) {
+    if (ary.length == 0)
+      return 1;
+
+    var low = 0;
+    var high = ary.length - 1;
+    var i, comparison;
+    var hitPos = -1;
+    while (low <= high) {
+      i = Math.floor((low + high) / 2);
+      comparison = mapFn(ary[i]) - loVal;
+      if (comparison < 0) {
+        low = i + 1; continue;
+      } else if (comparison > 0) {
+        high = i - 1; continue;
+      } else {
+        hitPos = i;
+        high = i - 1;
+      }
+    }
+    // return where we hit, or failing that the low pos
+    return hitPos != -1 ? hitPos : low;
+  }
+
+  /**
+   * Finds an index in an array of intervals that either
+   * intersects the provided loVal, or if no intersection is found,
+   * the index of the first interval whose start is > loVal.
+   *
+   * The array of intervals is defined implicitly via two mapping functions
+   * over the provided ary. mapLoFn determines the lower value of the interval,
+   * mapWidthFn the width. Intersection is lower-inclusive, e.g. [lo,lo+w).
+   *
+   * The array of intervals formed by this mapping must be non-overlapping and
+   * sorted in ascending order by loVal.
+   *
+   * @param {Array} ary An array of objects that can be converted into sorted
+   *     nonoverlapping ranges [x,y) using the mapLoFn and mapWidth.
+   * @param {function():*} mapLoFn Callback that produces the low value for the
+   *     interval represented by an  element in the array.
+   * @param {function():*} mapWidthFn Callback that produces the width for the
+   *     interval represented by an  element in the array.
+   * @param {number} loVal The low value for the search.
+   * @return {Number} An index in the array that intersects or is first-above
+   *     loVal, -1 if none found and loVal is below than all the intervals,
+   *     ary.length if loVal is greater than all the intervals.
+   */
+  function findLowIndexInSortedIntervals(ary, mapLoFn, mapWidthFn, loVal) {
+    var first = findLowIndexInSortedArray(ary, mapLoFn, loVal);
+    if (first == 0) {
+      if (loVal >= mapLoFn(ary[0]) &&
+          loVal < mapLoFn(ary[0]) + mapWidthFn(ary[0], 0)) {
+        return 0;
+      } else {
+        return -1;
+      }
+    } else if (first < ary.length) {
+      if (loVal >= mapLoFn(ary[first]) &&
+          loVal < mapLoFn(ary[first]) + mapWidthFn(ary[first], first)) {
+        return first;
+      } else if (loVal >= mapLoFn(ary[first - 1]) &&
+                 loVal < mapLoFn(ary[first - 1]) +
+                 mapWidthFn(ary[first - 1], first - 1)) {
+        return first - 1;
+      } else {
+        return ary.length;
+      }
+    } else if (first == ary.length) {
+      if (loVal >= mapLoFn(ary[first - 1]) &&
+          loVal < mapLoFn(ary[first - 1]) +
+          mapWidthFn(ary[first - 1], first - 1)) {
+        return first - 1;
+      } else {
+        return ary.length;
+      }
+    } else {
+      return ary.length;
+    }
+  }
+
+  /**
+   * Calls cb for all intervals in the implicit array of intervals
+   * defnied by ary, mapLoFn and mapHiFn that intersect the range
+   * [loVal,hiVal)
+   *
+   * This function uses the same scheme as findLowIndexInSortedArray
+   * to define the intervals. The same restrictions on sortedness and
+   * non-overlappingness apply.
+   *
+   * @param {Array} ary An array of objects that can be converted into sorted
+   * nonoverlapping ranges [x,y) using the mapLoFn and mapWidth.
+   * @param {function():*} mapLoFn Callback that produces the low value for the
+   * interval represented by an element in the array.
+   * @param {function():*} mapLoFn Callback that produces the width for the
+   * interval represented by an element in the array.
+   * @param {number} The low value for the search, inclusive.
+   * @param {number} loVal The high value for the search, non inclusive.
+   * @param {function():*} cb The function to run for intersecting intervals.
+   */
+  function iterateOverIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal,
+                                            hiVal, cb) {
+    if (ary.length == 0)
+      return;
+
+    if (loVal > hiVal) return;
+
+    var i = findLowIndexInSortedArray(ary, mapLoFn, loVal);
+    if (i == -1) {
+      return;
+    }
+    if (i > 0) {
+      var hi = mapLoFn(ary[i - 1]) + mapWidthFn(ary[i - 1], i - 1);
+      if (hi >= loVal) {
+        cb(ary[i - 1]);
+      }
+    }
+    if (i == ary.length) {
+      return;
+    }
+
+    for (var n = ary.length; i < n; i++) {
+      var lo = mapLoFn(ary[i]);
+      if (lo >= hiVal)
+        break;
+      cb(ary[i]);
+    }
+  }
+
+  /**
+   * Non iterative version of iterateOverIntersectingIntervals.
+   *
+   * @return {Array} Array of elements in ary that intersect loVal, hiVal.
+   */
+  function getIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal, hiVal) {
+    var tmp = [];
+    iterateOverIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal, hiVal,
+                                     function(d) {
+                                       tmp.push(d);
+                                     });
+    return tmp;
+  }
+
+  /**
+   * Finds the element in the array whose value is closest to |val|.
+   *
+   * The same restrictions on sortedness as for findLowIndexInSortedArray apply.
+   *
+   * @param {Array} ary An array of arbitrary objects.
+   * @param {function():*} mapFn Callback that produces a key value
+   *     from an element in ary.
+   * @param {number} val Value for which to search.
+   * @param {number} maxDiff Maximum allowed difference in value between |val|
+   *     and an element's value.
+   * @return {object} Object in the array whose value is closest to |val|, or
+   *     null if no object is within range.
+   */
+  function findClosestElementInSortedArray(ary, mapFn, val, maxDiff) {
+    if (ary.length === 0)
+      return null;
+
+    var aftIdx = findLowIndexInSortedArray(ary, mapFn, val);
+    var befIdx = aftIdx > 0 ? aftIdx - 1 : 0;
+
+    if (aftIdx === ary.length)
+      aftIdx -= 1;
+
+    var befDiff = Math.abs(val - mapFn(ary[befIdx]));
+    var aftDiff = Math.abs(val - mapFn(ary[aftIdx]));
+
+    if (befDiff > maxDiff && aftDiff > maxDiff)
+      return null;
+
+    var idx = befDiff < aftDiff ? befIdx : aftIdx;
+    return ary[idx];
+  }
+
+  /**
+   * Finds the closest interval in the implicit array of intervals
+   * defined by ary, mapLoFn and mapHiFn.
+   *
+   * This function uses the same scheme as findLowIndexInSortedArray
+   * to define the intervals. The same restrictions on sortedness and
+   * non-overlappingness apply.
+   *
+   * @param {Array} ary An array of objects that can be converted into sorted
+   *     nonoverlapping ranges [x,y) using the mapLoFn and mapHiFn.
+   * @param {function():*} mapLoFn Callback that produces the low value for the
+   *     interval represented by an element in the array.
+   * @param {function():*} mapHiFn Callback that produces the high for the
+   *     interval represented by an element in the array.
+   * @param {number} val The value for the search.
+   * @param {number} maxDiff Maximum allowed difference in value between |val|
+   *     and an interval's low or high value.
+   * @return {interval} Interval in the array whose high or low value is closest
+   *     to |val|, or null if no interval is within range.
+   */
+  function findClosestIntervalInSortedIntervals(ary, mapLoFn, mapHiFn, val,
+                                                maxDiff) {
+    if (ary.length === 0)
+      return null;
+
+    var idx = findLowIndexInSortedArray(ary, mapLoFn, val);
+    if (idx > 0)
+      idx -= 1;
+
+    var hiInt = ary[idx];
+    var loInt = hiInt;
+
+    if (val > mapHiFn(hiInt) && idx + 1 < ary.length)
+      loInt = ary[idx + 1];
+
+    var loDiff = Math.abs(val - mapLoFn(loInt));
+    var hiDiff = Math.abs(val - mapHiFn(hiInt));
+
+    if (loDiff > maxDiff && hiDiff > maxDiff)
+      return null;
+
+    if (loDiff < hiDiff)
+      return loInt;
+    else
+      return hiInt;
+  }
+
+  return {
+    findLowIndexInSortedArray: findLowIndexInSortedArray,
+    findLowIndexInSortedIntervals: findLowIndexInSortedIntervals,
+    iterateOverIntersectingIntervals: iterateOverIntersectingIntervals,
+    getIntersectingIntervals: getIntersectingIntervals,
+    findClosestElementInSortedArray: findClosestElementInSortedArray,
+    findClosestIntervalInSortedIntervals: findClosestIntervalInSortedIntervals
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/sorted_array_utils_test.html b/trace-viewer/trace_viewer/base/sorted_array_utils_test.html
new file mode 100644
index 0000000..6db2184
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/sorted_array_utils_test.html
@@ -0,0 +1,207 @@
+<!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/sorted_array_utils.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ArrayOfIntervals = function(array) {
+    this.array = array;
+  }
+
+  ArrayOfIntervals.prototype = {
+
+    get: function(index) {
+      return this.array[index];
+    },
+
+    findLowElementIndex: function(ts) {
+      return tv.b.findLowIndexInSortedArray(
+          this.array,
+          function(x) { return x.lo; },
+          ts);
+    },
+
+    findLowIntervalIndex: function(ts) {
+      return tv.b.findLowIndexInSortedIntervals(
+          this.array,
+          function(x) { return x.lo; },
+          function(x) { return x.hi - x.lo; },
+          ts);
+    },
+
+    findIntersectingIntervals: function(tsA, tsB) {
+      var array = this.array;
+      var result = [];
+      tv.b.iterateOverIntersectingIntervals(
+          this.array,
+          function(x) { return x.lo; },
+          function(x) { return x.hi - x.lo; },
+          tsA,
+          tsB,
+          function(x) { result.push(array.indexOf(x)); });
+      return result;
+    },
+
+    findClosestElement: function(ts, tsDiff) {
+      return tv.b.findClosestElementInSortedArray(
+          this.array,
+          function(x) { return x.lo; },
+          ts,
+          tsDiff);
+    },
+
+    findClosestInterval: function(ts, tsDiff) {
+      return tv.b.findClosestIntervalInSortedIntervals(
+          this.array,
+          function(x) { return x.lo; },
+          function(x) { return x.hi; },
+          ts,
+          tsDiff);
+    }
+  };
+
+  test('findLowElementIndex', function() {
+    var array = new ArrayOfIntervals([
+      {lo: 10, hi: 15},
+      {lo: 20, hi: 30}
+    ]);
+
+    assert.equal(array.findLowElementIndex(-100), 0);
+    assert.equal(array.findLowElementIndex(0), 0);
+    assert.equal(array.findLowElementIndex(10), 0);
+
+    assert.equal(array.findLowElementIndex(10.1), 1);
+    assert.equal(array.findLowElementIndex(15), 1);
+    assert.equal(array.findLowElementIndex(20), 1);
+
+    assert.equal(array.findLowElementIndex(20.1), 2);
+    assert.equal(array.findLowElementIndex(21), 2);
+    assert.equal(array.findLowElementIndex(100), 2);
+  });
+
+  test('findLowIntervalIndex', function() {
+    var array = new ArrayOfIntervals([
+      {lo: 10, hi: 15},
+      {lo: 20, hi: 30}
+    ]);
+
+    assert.equal(array.findLowIntervalIndex(0), -1);
+    assert.equal(array.findLowIntervalIndex(9.9), -1);
+
+    assert.equal(array.findLowIntervalIndex(10), 0);
+    assert.equal(array.findLowIntervalIndex(12), 0);
+    assert.equal(array.findLowIntervalIndex(14.9), 0);
+
+    // These two are a little odd... the return is correct in that
+    // it was not found, but its neither below, nor above. Whatever.
+    assert.equal(array.findLowIntervalIndex(15), 2);
+    assert.equal(array.findLowIntervalIndex(19.9), 2);
+
+    assert.equal(array.findLowIntervalIndex(20), 1);
+    assert.equal(array.findLowIntervalIndex(21), 1);
+    assert.equal(array.findLowIntervalIndex(29.99), 1);
+
+    assert.equal(array.findLowIntervalIndex(30), 2);
+    assert.equal(array.findLowIntervalIndex(40), 2);
+  });
+
+  test('findIntersectingIntervals', function() {
+    var array = new ArrayOfIntervals([
+      {lo: 10, hi: 15},
+      {lo: 20, hi: 30}
+    ]);
+
+    assert.deepEqual(array.findIntersectingIntervals(0, 0), []);
+    assert.deepEqual(array.findIntersectingIntervals(100, 0), []);
+    assert.deepEqual(array.findIntersectingIntervals(0, 10), []);
+
+    assert.deepEqual(array.findIntersectingIntervals(0, 10.1), [0]);
+    assert.deepEqual(array.findIntersectingIntervals(5, 15), [0]);
+    assert.deepEqual(array.findIntersectingIntervals(15, 20), [0]);
+
+    assert.deepEqual(array.findIntersectingIntervals(15.1, 20), []);
+
+    assert.deepEqual(array.findIntersectingIntervals(15.1, 20.1), [1]);
+    assert.deepEqual(array.findIntersectingIntervals(20, 30), [1]);
+    assert.deepEqual(array.findIntersectingIntervals(30, 100), [1]);
+
+    assert.deepEqual(array.findIntersectingIntervals(0, 100), [0, 1]);
+    assert.deepEqual(array.findIntersectingIntervals(15, 20.1), [0, 1]);
+  });
+
+  test('findClosestElement', function() {
+    var array = new ArrayOfIntervals([
+      {lo: 10, hi: 15},
+      {lo: 20, hi: 30}
+    ]);
+
+    // Test the helper method first.
+    assert.isUndefined(array.get(-1));
+    assert.equal(array.get(0), array.array[0]);
+    assert.equal(array.get(1), array.array[1]);
+    assert.isUndefined(array.get(2));
+
+    assert.isNull(array.findClosestElement(0, 0));
+    assert.isNull(array.findClosestElement(0, 9.9));
+    assert.isNull(array.findClosestElement(10, -10));
+
+    assert.equal(array.get(0), array.findClosestElement(0, 10));
+    assert.equal(array.get(0), array.findClosestElement(8, 5));
+    assert.equal(array.get(0), array.findClosestElement(10, 0));
+    assert.equal(array.get(0), array.findClosestElement(12, 2));
+
+    assert.isNull(array.findClosestElement(15, 3));
+    assert.isNotNull(array.findClosestElement(15, 5));
+
+    assert.equal(array.get(1), array.findClosestElement(19, 1));
+    assert.equal(array.get(1), array.findClosestElement(20, 0));
+    assert.equal(array.get(1), array.findClosestElement(30, 15));
+
+    assert.isNull(array.findClosestElement(30, 9.9));
+    assert.isNull(array.findClosestElement(100, 50));
+  });
+
+  test('findClosestInterval', function() {
+    var array = new ArrayOfIntervals([
+      {lo: 10, hi: 15},
+      {lo: 20, hi: 30}
+    ]);
+
+    assert.isNull(array.findClosestInterval(0, 0));
+    assert.isNull(array.findClosestInterval(0, 9.9));
+    assert.isNull(array.findClosestInterval(0, -100));
+
+    assert.equal(array.get(0), array.findClosestInterval(0, 10));
+    assert.equal(array.get(0), array.findClosestInterval(10, 0));
+    assert.equal(array.get(0), array.findClosestInterval(12, 3));
+    assert.equal(array.get(0), array.findClosestInterval(12, 100));
+
+    assert.equal(array.get(0), array.findClosestInterval(13, 3));
+    assert.equal(array.get(0), array.findClosestInterval(13, 20));
+    assert.equal(array.get(0), array.findClosestInterval(15, 0));
+
+    assert.isNull(array.findClosestInterval(17.5, 0));
+    assert.isNull(array.findClosestInterval(17.5, 2.4));
+    assert.isNotNull(array.findClosestInterval(17.5, 2.5));
+    assert.isNotNull(array.findClosestInterval(17.5, 10));
+
+    assert.equal(array.get(1), array.findClosestInterval(19, 2));
+    assert.equal(array.get(1), array.findClosestInterval(20, 0));
+    assert.equal(array.get(1), array.findClosestInterval(24, 100));
+    assert.equal(array.get(1), array.findClosestInterval(26, 100));
+
+    assert.equal(array.get(1), array.findClosestInterval(30, 0));
+    assert.equal(array.get(1), array.findClosestInterval(35, 10));
+    assert.equal(array.get(1), array.findClosestInterval(50, 100));
+
+    assert.isNull(array.findClosestInterval(50, 19));
+    assert.isNull(array.findClosestInterval(100, 50));
+    assert.isNull(array.findClosestInterval(50, -100));
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/statistics.html b/trace-viewer/trace_viewer/base/statistics.html
new file mode 100644
index 0000000..a9c6380
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/statistics.html
@@ -0,0 +1,91 @@
+<!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.b', function() {
+
+  function identity(d) {
+    return d;
+  }
+
+  function Statistics() {
+  }
+
+  Statistics.sum = function(ary, opt_func, opt_this) {
+    var func = opt_func || identity;
+    var ret = 0;
+    for (var i = 0; i < ary.length; i++)
+      ret += func.call(opt_this, ary[i], i);
+    return ret;
+  };
+
+  Statistics.mean = function(ary, opt_func, opt_this) {
+    return Statistics.sum(ary, opt_func, opt_this) / ary.length;
+  };
+
+  Statistics.variance = function(ary, opt_func, opt_this) {
+    var func = opt_func || identity;
+    var mean = Statistics.mean(ary, func, opt_this);
+    var sumOfSquaredDistances = Statistics.sum(
+        ary,
+        function(d, i) {
+          var v = func.call(this, d, i) - mean;
+          return v * v;
+        },
+        opt_this);
+    return sumOfSquaredDistances / (ary.length - 1);
+  };
+
+  Statistics.stddev = function(ary, opt_func, opt_this) {
+    return Math.sqrt(
+        Statistics.variance(ary, opt_func, opt_this));
+  };
+
+  Statistics.max = function(ary, opt_func, opt_this) {
+    var func = opt_func || identity;
+    var ret = -Infinity;
+    for (var i = 0; i < ary.length; i++)
+      ret = Math.max(ret, func.call(opt_this, ary[i], i));
+    return ret;
+  };
+
+  Statistics.min = function(ary, opt_func, opt_this) {
+    var func = opt_func || identity;
+    var ret = Infinity;
+    for (var i = 0; i < ary.length; i++)
+      ret = Math.min(ret, func.call(opt_this, ary[i], i));
+    return ret;
+  };
+
+  Statistics.range = function(ary, opt_func, opt_this) {
+    var func = opt_func || identity;
+    var ret = new tv.b.Range();
+    for (var i = 0; i < ary.length; i++)
+      ret.addValue(func.call(opt_this, ary[i], i));
+    return ret;
+  }
+
+  Statistics.percentile = function(ary, percent, opt_func, opt_this) {
+    if (!(percent >= 0 && percent <= 1))
+      throw new Error('percent must be [0,1]');
+
+    var func = opt_func || identity;
+    var tmp = new Array(ary.length);
+    for (var i = 0; i < ary.length; i++)
+      tmp[i] = func.call(opt_this, ary[i], i);
+    tmp.sort();
+    var idx = Math.floor((ary.length - 1) * percent);
+    return tmp[idx];
+  };
+
+  return {
+    Statistics: Statistics
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/statistics_test.html b/trace-viewer/trace_viewer/base/statistics_test.html
new file mode 100644
index 0000000..1fd0d2f
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/statistics_test.html
@@ -0,0 +1,115 @@
+<!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/statistics.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Statistics = tv.b.Statistics;
+
+  test('sumBasic', function() {
+    assert.equal(Statistics.sum([1, 2, 3]), 6);
+  });
+
+  test('sumWithFunctor', function() {
+    var ctx = {};
+    var ary = [1, 2, 3];
+    assert.equal(12, Statistics.sum(ary, function(x, i) {
+      assert.equal(this, ctx);
+      assert.equal(ary[i], x);
+      return x * 2;
+    }, ctx));
+  });
+
+  test('minMaxWithFunctor', function() {
+    var ctx = {};
+    var ary = [1, 2, 3];
+    function func(x, i) {
+      assert.equal(this, ctx);
+      assert.equal(ary[i], x);
+      return x;
+    }
+    assert.equal(Statistics.max(ary, func, ctx), 3);
+    assert.equal(Statistics.min(ary, func, ctx), 1);
+
+    var range = Statistics.range(ary, func, ctx);
+    assert.isFalse(range.isEmpty);
+    assert.equal(range.min, 1);
+    assert.equal(range.max, 3);
+  });
+
+  test('maxExtrema', function() {
+    assert.equal(Statistics.max([]), -Infinity);
+    assert.equal(Statistics.min([]), Infinity);
+  });
+
+  test('meanBasic', function() {
+    assert.equal(Statistics.mean([1, 2, 3]), 2);
+  });
+
+  test('varianceBasic', function() {
+    // In [2, 4, 4, 2], all items have a deviation of 1.0 from the mean so the
+    // population variance is 4.0 / 4 = 1.0, but the sample variance is 4.0 / 3.
+    assert.equal(Statistics.variance([2, 4, 4, 2]), 4.0 / 3);
+
+    // In [1, 2, 3], the squared deviations are 1.0, 0.0 and 1.0 respectively;
+    // population variance 2.0 / 3 but sample variance is 2.0 / 2 = 1.0.
+    assert.equal(Statistics.variance([1, 2, 3]), 1.0);
+  });
+
+  test('varianceWithFunctor', function() {
+    var ctx = {};
+    var ary = [{x: 2},
+               {x: 4},
+               {x: 4},
+               {x: 2}];
+    assert.equal(4.0 / 3, Statistics.variance(ary, function(d) {
+      assert.equal(ctx, this);
+      return d.x;
+    }, ctx));
+  });
+
+  test('stddevBasic', function() {
+    assert.equal(Statistics.stddev([2, 4, 4, 2]), Math.sqrt(4.0 / 3));
+  });
+
+  test('stddevWithFunctor', function() {
+    var ctx = {};
+    var ary = [{x: 2},
+               {x: 4},
+               {x: 4},
+               {x: 2}];
+    assert.equal(Math.sqrt(4.0 / 3), Statistics.stddev(ary, function(d) {
+      assert.equal(ctx, this);
+      return d.x;
+    }, ctx));
+  });
+
+  test('percentile', function() {
+    var ctx = {};
+    var ary = [{x: 0},
+               {x: 1},
+               {x: 2},
+               {x: 3},
+               {x: 4},
+               {x: 5},
+               {x: 6},
+               {x: 7},
+               {x: 8},
+               {x: 9}];
+    function func(d, i) {
+      assert.equal(ctx, this);
+      return d.x;
+    }
+    assert.equal(Statistics.percentile(ary, 0, func, ctx), 0);
+    assert.equal(Statistics.percentile(ary, .5, func, ctx), 4);
+    assert.equal(Statistics.percentile(ary, .75, func, ctx), 6);
+    assert.equal(Statistics.percentile(ary, 1, func, ctx), 9);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/task.html b/trace-viewer/trace_viewer/base/task.html
new file mode 100644
index 0000000..d5a7cbc
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/task.html
@@ -0,0 +1,134 @@
+<!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/raf.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  /**
+   * A task is a combination of a run callback, a set of subtasks, and an after
+   * task.
+   *
+   * When executed, a task does the following things:
+   * 1. Runs its callback
+   * 2. Runs its subtasks
+   * 3. Runs its after callback.
+   *
+   * The list of subtasks and after task can be mutated inside step #1 but as
+   * soon as the task's callback returns, the subtask list and after task is
+   * fixed and cannot be changed again.
+   *
+   * Use task.after().after().after() to describe the toplevel passes that make
+   * up your computation. Then, use subTasks to add detail to each subtask as it
+   * runs. For example:
+   *    var pieces = [];
+   *    taskA = new Task(function() { pieces = getPieces(); });
+   *    taskA.after(function(taskA) {
+   *      pieces.forEach(function(piece) {
+   *        taskA.subTask(function(taskB) { piece.process(); }, this);
+   *      });
+   *    });
+   *
+   * @constructor
+   */
+  function Task(runCb, thisArg) {
+    if (runCb !== undefined && thisArg === undefined)
+      throw new Error('Almost certainly, you meant to pass a thisArg.');
+    this.runCb_ = runCb;
+    this.thisArg_ = thisArg;
+    this.afterTask_ = undefined;
+    this.subTasks_ = [];
+  }
+
+  Task.prototype = {
+    /*
+     * See constructor documentation on semantics of subtasks.
+     */
+    subTask: function(cb, thisArg) {
+      if (cb instanceof Task)
+        this.subTasks_.push(cb);
+      else
+        this.subTasks_.push(new Task(cb, thisArg));
+      return this.subTasks_[this.subTasks_.length - 1];
+    },
+
+    /**
+     * Runs the current task and returns the task that should be executed next.
+     */
+    run: function() {
+      if (this.runCb_ !== undefined)
+        this.runCb_.call(this.thisArg_, this);
+      var subTasks = this.subTasks_;
+      this.subTasks_ = undefined; // Prevent more subTasks from being posted.
+
+      if (!subTasks.length)
+        return this.afterTask_;
+
+      // If there are subtasks, then we want to execute all the subtasks and
+      // then this task's afterTask. To make this happen, we update the
+      // afterTask of all the subtasks so the point upward to each other, e.g.
+      // subTask[0].afterTask to subTask[1] and so on. Then, the last subTask's
+      // afterTask points at this task's afterTask.
+      for (var i = 1; i < subTasks.length; i++)
+        subTasks[i - 1].afterTask_ = subTasks[i];
+      subTasks[subTasks.length - 1].afterTask_ = this.afterTask_;
+      return subTasks[0];
+    },
+
+    /*
+     * See constructor documentation on semantics of after tasks.
+     */
+    after: function(cb, thisArg) {
+      if (this.afterTask_)
+        throw new Error('Has an after task already');
+      if (cb instanceof Task)
+        this.afterTask_ = cb;
+      else
+        this.afterTask_ = new Task(cb, thisArg);
+      return this.afterTask_;
+    }
+  };
+
+  Task.RunSynchronously = function(task) {
+    var curTask = task;
+    while (curTask)
+      curTask = curTask.run();
+  }
+
+  /**
+   * Runs a task using raf.requestIdleCallback, returning
+   * a promise for its completion.
+   */
+  Task.RunWhenIdle = function(task) {
+    return new Promise(function(resolve, reject) {
+      var curTask = task;
+      function runAnother() {
+        try {
+          curTask = curTask.run();
+        } catch (e) {
+          reject(e);
+          console.error(e.stack);
+          return;
+        }
+
+        if (curTask) {
+          tv.b.requestIdleCallback(runAnother);
+          return;
+        }
+
+        resolve();
+      }
+      tv.b.requestIdleCallback(runAnother);
+    });
+  }
+
+  return {
+    Task: Task
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/task_test.html b/trace-viewer/trace_viewer/base/task_test.html
new file mode 100644
index 0000000..9eac8dd
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/task_test.html
@@ -0,0 +1,80 @@
+<!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">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var Task = tv.b.Task;
+
+  test('basicAllStepsPass', function() {
+    var results = [];
+
+    var startingTask = new Task(function(task) {
+      results.push('a');
+      task.subTask(function() {
+        results.push('a/1');
+      }, this);
+      task.subTask(function() {
+        results.push('a/2');
+      }, this);
+    }, this);
+    startingTask.after(function() {
+      results.push('b');
+    }, this).after(function() {
+      results.push('c');
+    }, this);
+
+    Task.RunSynchronously(startingTask);
+    assert.deepEqual(results, ['a', 'a/1', 'a/2', 'b', 'c']);
+  });
+
+  test('basicAllStepsPassAsync', function() {
+    var results = [];
+
+    var startingTask = new Task(function(task) {
+      results.push('a');
+      task.subTask(function() {
+        results.push('a/1');
+      }, this);
+      task.subTask(function() {
+        results.push('a/2');
+      }, this);
+    }, this);
+    startingTask.after(function() {
+      results.push('b');
+    }, this).after(function() {
+      results.push('c');
+    }, this);
+
+    var promise = Task.RunWhenIdle(startingTask);
+    promise.then(function() {
+      assert.deepEqual(results, ['a', 'a/1', 'a/2', 'b', 'c']);
+    });
+    return promise;
+  });
+
+  test('taskThatThrowsShouldRejectItsPromise', function() {
+    var startingTask = new Task(function(task) {
+      throw new Error(
+          'IGNORE. This is an expected error to test error handling.');
+    }, this);
+
+    var taskPromise = Task.RunWhenIdle(startingTask);
+
+    return new Promise(function(resolve, reject) {
+      taskPromise.then(function() {
+        reject(new Error('Should have thrown'));
+      }, function(err) {
+        resolve();
+      });
+    });
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/tests.html b/trace-viewer/trace_viewer/base/tests.html
new file mode 100644
index 0000000..54fa2f5
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/tests.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<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.
+-->
+<head>
+  <title>Trace-Viewer Tests: loading...</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+  <link rel="shortcut icon" href="data:image/x-icon;base64," type="image/x-icon">
+
+  <script src="/components/webcomponentsjs/webcomponents.js"></script>
+
+  <link rel="import" href="/components/polymer/polymer.html">
+  <link rel="import" href="/base/base.html">
+  <link rel="import" href="/base/unittest.html">
+  <link rel="import" href="/base/unittest/interactive_test_runner.html">
+  <style>
+    html, body {
+      box-sizing: border-box;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+      margin: 0px;
+    }
+    body > x-base-interactive-test-runner {
+      height: 100%;
+      width: 100%;
+    }
+
+  </style>
+</head>
+<body>
+  <script>
+    'use strict';
+
+    // The test runner no-ops pushState so keep it around.
+    var realWindowHistoryPushState = window.history.pushState.bind(
+        window.history);
+
+    function stateToSearchString(defaultState, state) {
+      var parts = [];
+      for (var k in state) {
+        if (state[k] === defaultState[k])
+          continue;
+        var v = state[k];
+        var kv;
+        if (v === true) {
+          kv = k;
+        } else if (v === false) {
+          kv = k + '=false';
+        } else if (v === '') {
+          continue;
+        } else {
+          kv = k + '=' + v;
+        }
+        parts.push(kv);
+      }
+      return parts.join('&');
+    }
+
+    function stateFromSearchString(string) {
+      var state = {};
+      string.split('&').forEach(function(part) {
+        if (part == '')
+          return;
+        var kv = part.split('=');
+        var k, v;
+        if (kv.length == 1) {
+          k = kv[0];
+          v = true;
+        } else {
+          k = kv[0];
+          if (kv[1] == 'false')
+            v = false;
+          else
+            v = kv[1];
+        }
+        state[k] = v;
+      });
+      return state;
+    }
+
+    function loadAndRunTests() {
+      var state = stateFromSearchString(
+          window.location.search.substring(1));
+      updateTitle(state);
+
+      var suiteNamesToLoad;
+      if (state.testSuiteName) {
+        suiteNamesToLoad = [];
+        suiteNamesToLoad.push(state.testSuiteName);
+      }
+
+      showLoadingOverlay();
+      var loader = new tv.b.unittest.SuiteLoader(suiteNamesToLoad);
+      return loader.allSuitesLoadedPromise.then(
+        function() {
+          hideLoadingOverlay();
+          Polymer.whenReady(function() {
+            runTests(loader, state);
+          });
+        },
+        function(err) {
+          hideLoadingOverlay();
+          tv.showPanic('Module loading failure', err);
+          throw err;
+        });
+    }
+
+    function showLoadingOverlay() {
+      var overlay = document.createElement('div');
+      overlay.id = 'tests-loading-overlay';
+      overlay.style.backgroundColor = 'white';
+      overlay.style.boxSizing = 'border-box';
+      overlay.style.color = 'black';
+      overlay.style.display = '-webkit-flex';
+      overlay.style.height = '100%';
+      overlay.style.left = 0;
+      overlay.style.padding = '8px';
+      overlay.style.position = 'fixed';
+      overlay.style.top = 0;
+      overlay.style.webkitFlexDirection = 'column';
+      overlay.style.width = '100%';
+
+      var element = document.createElement('div');
+      element.style.webkitFlex = '1 1 auto';
+      element.style.overflow = 'auto';
+      overlay.appendChild(element);
+
+      element.textContent = 'Loading tests...';
+      document.body.appendChild(overlay);
+    }
+    function hideLoadingOverlay() {
+      var overlay = document.body.querySelector('#tests-loading-overlay');
+      document.body.removeChild(overlay);
+    }
+
+    function updateTitle(state) {
+      var testFilterString = state.testFilterString || '';
+      var testSuiteName = state.testSuiteName || '';
+
+      var title;
+      if (testSuiteName && testFilterString.length) {
+        title = testFilterString + ' in ' + testSuiteName;
+      } else if (testSuiteName) {
+        title = testSuiteName;
+      } else if (testFilterString) {
+        title = testFilterString + ' in all tests';
+      } else {
+        title = 'All Trace-Viewer Tests';
+      }
+
+      if (state.shortFormat)
+        title += '(s)';
+      document.title = title;
+      var runner = document.querySelector('x-base-interactive-test-runner');
+      if (runner)
+        runner.title = title;
+    }
+
+    function runTests(loader, state) {
+      var runner = new tv.b.unittest.InteractiveTestRunner();
+      runner.testLinks = loader.testLinks;
+      runner.allTests = loader.getAllTests();
+      document.body.appendChild(runner);
+
+      runner.setState(state);
+      updateTitle(state);
+
+      runner.addEventListener('statechange', function() {
+        var state = runner.getState();
+        var stateString = stateToSearchString(runner.getDefaultState(),
+                                              state);
+        if (window.location.search.substring(1) == stateString)
+          return;
+
+        updateTitle(state);
+        var stateURL;
+        if (stateString.length > 0)
+          stateURL = window.location.pathname + '?' + stateString;
+        else
+          stateURL = window.location.pathname;
+        realWindowHistoryPushState(state, document.title, stateURL);
+      });
+
+      window.addEventListener('popstate', function(state) {
+        runner.setState(state, true);
+      });
+
+      runner.getHRefForTestCase = function(testCase) {
+        var state = runner.getState();
+        if (state.testFilterString === '' &&
+            state.testSuiteName === '') {
+          state.testSuiteName = testCase.suite.name;
+          state.testFilterString = '';
+          state.shortFormat = false;
+        } else {
+          state.testSuiteName = testCase.suite.name;
+          state.testFilterString = testCase.name;
+          state.shortFormat = false;
+        }
+        var stateString = stateToSearchString(runner.getDefaultState(),
+                                              state);
+        if (stateString.length > 0)
+          return window.location.pathname + '?' + stateString;
+        else
+          return window.location.pathname;
+      }
+    }
+
+    window.addEventListener('load', loadAndRunTests);
+
+  </script>
+</body>
+</html>
diff --git a/trace-viewer/trace_viewer/base/ui.html b/trace-viewer/trace_viewer/base/ui.html
new file mode 100644
index 0000000..2b710e0
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui.html
@@ -0,0 +1,169 @@
+<!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.b.ui', function() {
+
+  /**
+   * Decorates elements as an instance of a class.
+   * @param {string|!Element} source The way to find the element(s) to decorate.
+   *     If this is a string then {@code querySeletorAll} is used to find the
+   *     elements to decorate.
+   * @param {!Function} constr The constructor to decorate with. The constr
+   *     needs to have a {@code decorate} function.
+   */
+  function decorate(source, constr) {
+    var elements;
+    if (typeof source == 'string')
+      elements = tv.doc.querySelectorAll(source);
+    else
+      elements = [source];
+
+    for (var i = 0, el; el = elements[i]; i++) {
+      if (!(el instanceof constr))
+        constr.decorate(el);
+    }
+  }
+
+  /**
+   * Defines a tracing UI component, a function that can be called to construct
+   * the component.
+   *
+   * tv class:
+   * var List = tv.b.ui.define('list');
+   * List.prototype = {
+   *   __proto__: HTMLUListElement.prototype,
+   *   decorate: function() {
+   *     ...
+   *   },
+   *   ...
+   * };
+   *
+   * Derived class:
+   * var CustomList = tv.b.ui.define('custom-list', List);
+   * CustomList.prototype = {
+   *   __proto__: List.prototype,
+   *   decorate: function() {
+   *     ...
+   *   },
+   *   ...
+   * };
+   *
+   * @param {string} className The className of the newly created subtype. If
+   *     subclassing by passing in opt_parentConstructor, this is used for
+   *     debugging. If not subclassing, then it is the tag name that will be
+   *     created by the component.
+
+   * @param {function=} opt_parentConstructor The parent class for this new
+   *     element, if subclassing is desired. If provided, the parent class must
+   *     be also a function created by tv.b.ui.define.
+   *
+   * @param {string=} opt_tagNS The namespace in which to create the base
+   *     element. Has no meaning when opt_parentConstructor is passed and must
+   *     either be undefined or the same namespace as the parent class.
+   *
+   * @return {function(Object=):Element} The newly created component
+   *     constructor.
+   */
+  function define(className, opt_parentConstructor, opt_tagNS) {
+    if (typeof className == 'function') {
+      throw new Error('Passing functions as className is deprecated. Please ' +
+                      'use (className, opt_parentConstructor) to subclass');
+    }
+
+    var className = className.toLowerCase();
+    if (opt_parentConstructor && !opt_parentConstructor.tagName)
+      throw new Error('opt_parentConstructor was not ' +
+                      'created by tv.b.ui.define');
+
+    // Walk up the parent constructors until we can find the type of tag
+    // to create.
+    var tagName = className;
+    var tagNS = undefined;
+    if (opt_parentConstructor) {
+      if (opt_tagNS)
+        throw new Error('Must not specify tagNS if parentConstructor is given');
+      var parent = opt_parentConstructor;
+      while (parent && parent.tagName) {
+        tagName = parent.tagName;
+        tagNS = parent.tagNS;
+        parent = parent.parentConstructor;
+      }
+    } else {
+      tagNS = opt_tagNS;
+    }
+
+    /**
+     * Creates a new UI element constructor.
+     * Arguments passed to the constuctor are provided to the decorate method.
+     * You will need to call the parent elements decorate method from within
+     * your decorate method and pass any required parameters.
+     * @constructor
+     */
+    function f() {
+      if (opt_parentConstructor &&
+          f.prototype.__proto__ != opt_parentConstructor.prototype) {
+        throw new Error(
+            className + ' prototye\'s __proto__ field is messed up. ' +
+            'It MUST be the prototype of ' + opt_parentConstructor.tagName);
+      }
+
+      var el;
+      if (tagNS === undefined)
+        el = tv.doc.createElement(tagName);
+      else
+        el = tv.doc.createElementNS(tagNS, tagName);
+      f.decorate.call(this, el, arguments);
+      return el;
+    }
+
+    /**
+     * Decorates an element as a UI element class.
+     * @param {!Element} el The element to decorate.
+     */
+    f.decorate = function(el) {
+      el.__proto__ = f.prototype;
+      el.decorate.apply(el, arguments[1]);
+      el.constructor = f;
+    };
+
+    f.className = className;
+    f.tagName = tagName;
+    f.tagNS = tagNS;
+    f.parentConstructor = (opt_parentConstructor ? opt_parentConstructor :
+                                                   undefined);
+    f.toString = function() {
+      if (!f.parentConstructor)
+        return f.tagName;
+      return f.parentConstructor.toString() + '::' + f.className;
+    };
+
+    return f;
+  }
+
+  function elementIsChildOf(el, potentialParent) {
+    if (el == potentialParent)
+      return false;
+
+    var cur = el;
+    while (cur.parentNode) {
+      if (cur == potentialParent)
+        return true;
+      cur = cur.parentNode;
+    }
+    return false;
+  };
+
+  return {
+    decorate: decorate,
+    define: define,
+    elementIsChildOf: elementIsChildOf
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/animation.html b/trace-viewer/trace_viewer/base/ui/animation.html
new file mode 100644
index 0000000..4e86daa
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/animation.html
@@ -0,0 +1,79 @@
+<!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.b.ui', function() {
+  /**
+   * Represents a procedural animation that can be run by an
+   * tv.b.ui.AnimationController.
+   *
+   * @constructor
+   */
+  function Animation() {
+  }
+
+  Animation.prototype = {
+
+    /**
+     * Called when an animation has been queued after a running animation.
+     *
+     * @return {boolean} True if the animation can take on the responsibilities
+     * of the running animation. If true, takeOverFor will be called on the
+     * animation.
+     *
+     * This can be used to build animations that accelerate as pairs of them are
+     * queued.
+     */
+    canTakeOverFor: function(existingAnimation) {
+      throw new Error('Not implemented');
+    },
+
+    /**
+     * Called to take over responsiblities of an existingAnimation.
+     *
+     * At this point, the existingAnimation has been ticked one last time, then
+     * stopped. This animation will be started after this returns and has the
+     * job of finishing(or transitioning away from) the effect the existing
+     * animation was trying to accomplish.
+     */
+    takeOverFor: function(existingAnimation, newStartTimestamp, target) {
+      throw new Error('Not implemented');
+    },
+
+    start: function(timestamp, target) {
+      throw new Error('Not implemented');
+    },
+
+    /**
+     * Called when an animation is stopped before it finishes. The animation can
+     * do what it wants here, usually nothing.
+     *
+     * @param {Number} timestamp When the animation was stopped.
+     * @param {Object} target The object being animated. May be undefined, take
+     * care.
+     * @param {boolean} willBeTakenOverByAnotherAnimation Whether this animation
+     * is going to be handed to another animation's takeOverFor function.
+     */
+    didStopEarly: function(timestamp, target,
+                           willBeTakenOverByAnotherAnimation) {
+    },
+
+    /**
+     * @return {boolean} true if the animation is finished.
+     */
+    tick: function(timestamp, target) {
+      throw new Error('Not implemented');
+    }
+  };
+
+  return {
+    Animation: Animation
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/animation_controller.html b/trace-viewer/trace_viewer/base/ui/animation_controller.html
new file mode 100644
index 0000000..3080b8e
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/animation_controller.html
@@ -0,0 +1,140 @@
+<!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/event_target.html">
+<link rel="import" href="/base/raf.html">
+<link rel="import" href="/base/ui/animation.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+  /**
+   * Manages execution, queueing and blending of tv.b.ui.Animations against
+   * a single target.
+   *
+   * Targets must have a cloneAnimationState() method that returns all the
+   * animatable states of that target.
+   *
+   * @constructor
+   * @extends {tv.b.EventTarget}
+   */
+  function AnimationController() {
+    tv.b.EventTarget.call(this);
+
+    this.target_ = undefined;
+
+    this.activeAnimation_ = undefined;
+
+    this.tickScheduled_ = false;
+  }
+
+  AnimationController.prototype = {
+    __proto__: tv.b.EventTarget.prototype,
+
+    get target() {
+      return this.target_;
+    },
+
+    set target(target) {
+      if (this.activeAnimation_)
+        throw new Error('Cannot change target while animation is running.');
+      if (target.cloneAnimationState === undefined ||
+          typeof target.cloneAnimationState !== 'function')
+        throw new Error('target must have a cloneAnimationState function');
+
+      this.target_ = target;
+    },
+
+    get activeAnimation() {
+      return this.activeAnimation_;
+    },
+
+    get hasActiveAnimation() {
+      return !!this.activeAnimation_;
+    },
+
+    queueAnimation: function(animation, opt_now) {
+      if (this.target_ === undefined)
+        throw new Error('Cannot queue animations without a target');
+
+      var now;
+      if (opt_now !== undefined)
+        now = opt_now;
+      else
+        now = window.performance.now();
+
+      if (this.activeAnimation_) {
+        // Must tick the animation before stopping it case its about to stop,
+        // and to update the target with its final sets of edits up to this
+        // point.
+        var done = this.activeAnimation_.tick(now, this.target_);
+        if (done)
+          this.activeAnimation_ = undefined;
+      }
+
+      if (this.activeAnimation_) {
+        if (animation.canTakeOverFor(this.activeAnimation_)) {
+          this.activeAnimation_.didStopEarly(now, this.target_, true);
+          animation.takeOverFor(this.activeAnimation_, now, this.target_);
+        } else {
+          this.activeAnimation_.didStopEarly(now, this.target_, false);
+        }
+      }
+      this.activeAnimation_ = animation;
+      this.activeAnimation_.start(now, this.target_);
+
+      if (this.tickScheduled_)
+        return;
+      this.tickScheduled_ = true;
+      tv.b.requestAnimationFrame(this.tickActiveAnimation_, this);
+    },
+
+    cancelActiveAnimation: function(opt_now) {
+      if (!this.activeAnimation_)
+        return;
+      var now;
+      if (opt_now !== undefined)
+        now = opt_now;
+      else
+        now = window.performance.now();
+      this.activeAnimation_.didStopEarly(now, this.target_, false);
+      this.activeAnimation_ = undefined;
+    },
+
+    tickActiveAnimation_: function(frameBeginTime) {
+      this.tickScheduled_ = false;
+      if (!this.activeAnimation_)
+        return;
+
+      if (this.target_ === undefined) {
+        this.activeAnimation_.didStopEarly(frameBeginTime, this.target_, false);
+        return;
+      }
+
+      var oldTargetState = this.target_.cloneAnimationState();
+
+      var done = this.activeAnimation_.tick(frameBeginTime, this.target_);
+      if (done)
+        this.activeAnimation_ = undefined;
+
+      if (this.activeAnimation_) {
+        this.tickScheduled_ = true;
+        tv.b.requestAnimationFrame(this.tickActiveAnimation_, this);
+      }
+
+      if (oldTargetState) {
+        var e = new Event('didtick');
+        e.oldTargetState = oldTargetState;
+        this.dispatchEvent(e, false, false);
+      }
+    }
+  };
+
+  return {
+    AnimationController: AnimationController
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/animation_controller_test.html b/trace-viewer/trace_viewer/base/ui/animation_controller_test.html
new file mode 100644
index 0000000..13662fb
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/animation_controller_test.html
@@ -0,0 +1,166 @@
+<!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/utils.html">
+<link rel="import" href="/base/ui/animation_controller.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function SimpleAnimation(options) {
+    this.stopTime = options.stopTime;
+
+    this.startCalled = false;
+    this.didStopEarlyCalled = false;
+    this.wasTakenOver = false;
+    this.tickCount = 0;
+  }
+
+  SimpleAnimation.prototype = {
+    __proto__: tv.b.ui.Animation.prototype,
+
+    canTakeOverFor: function(existingAnimation) {
+      return false;
+    },
+
+    takeOverFor: function(existingAnimation, newStartTimestamp, target) {
+      throw new Error('Not implemented');
+    },
+
+    start: function(timestamp, target) {
+      this.startCalled = true;
+    },
+
+    didStopEarly: function(timestamp, target, willBeTakenOver) {
+      this.didStopEarlyCalled = true;
+      this.wasTakenOver = willBeTakenOver;
+    },
+
+    /**
+     * @return {boolean} true if the animation is finished.
+     */
+    tick: function(timestamp, target) {
+      this.tickCount++;
+      return timestamp >= this.stopTime;
+    }
+  };
+
+  test('cancel', function() {
+    var target = {
+      x: 0,
+      cloneAnimationState: function() { return {x: this.x}; }
+    };
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+
+    var animation = new SimpleAnimation({stopTime: 100});
+    controller.queueAnimation(animation);
+
+    tv.b.forcePendingRAFTasksToRun(0);
+    assert.equal(animation.tickCount, 1);
+    controller.cancelActiveAnimation();
+    assert.isFalse(controller.hasActiveAnimation);
+    assert.isTrue(animation.didStopEarlyCalled);
+  });
+
+  test('simple', function() {
+    var target = {
+      x: 0,
+      cloneAnimationState: function() { return {x: this.x}; }
+    };
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+
+    var animation = new SimpleAnimation({stopTime: 100});
+    controller.queueAnimation(animation);
+
+    tv.b.forcePendingRAFTasksToRun(0);
+    assert.equal(animation.tickCount, 1);
+    assert.isTrue(controller.hasActiveAnimation);
+
+    tv.b.forcePendingRAFTasksToRun(100);
+    assert.equal(animation.tickCount, 2);
+    assert.isFalse(controller.hasActiveAnimation);
+  });
+
+  test('queueTwo', function() {
+    // Clear all pending rafs so if something is lingering it will blow up here.
+    tv.b.forcePendingRAFTasksToRun(0);
+
+    var target = {
+      x: 0,
+      cloneAnimationState: function() { return {x: this.x}; }
+    };
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+
+    var a1 = new SimpleAnimation({stopTime: 100});
+    var a2 = new SimpleAnimation({stopTime: 100});
+    controller.queueAnimation(a1, 0);
+    assert.isTrue(a1.startCalled);
+    controller.queueAnimation(a2, 50);
+    assert.isTrue(a1.didStopEarlyCalled);
+    assert.isTrue(a2.startCalled);
+
+    tv.b.forcePendingRAFTasksToRun(150);
+    assert.isFalse(controller.hasActiveAnimation);
+    assert.isAbove(a2.tickCount, 0);
+  });
+
+  /**
+   * @constructor
+   */
+  function AnimationThatCanTakeOverForSimpleAnimation() {
+    this.takeOverForAnimation = undefined;
+  }
+
+  AnimationThatCanTakeOverForSimpleAnimation.prototype = {
+    __proto__: tv.b.ui.Animation.prototype,
+
+
+    canTakeOverFor: function(existingAnimation) {
+      return existingAnimation instanceof SimpleAnimation;
+    },
+
+    takeOverFor: function(existingAnimation, newStartTimestamp, target) {
+      this.takeOverForAnimation = existingAnimation;
+    },
+
+    start: function(timestamp, target) {
+      this.startCalled = true;
+    }
+  };
+
+  test('takeOver', function() {
+    var target = {
+      x: 0,
+      cloneAnimationState: function() { return {x: this.x}; }
+    };
+
+    var controller = new tv.b.ui.AnimationController();
+    controller.target = target;
+
+    var a1 = new SimpleAnimation({stopTime: 100});
+    var a2 = new AnimationThatCanTakeOverForSimpleAnimation();
+    controller.queueAnimation(a1, 0);
+    assert.isTrue(a1.startCalled);
+    assert.equal(a1.tickCount, 0);
+    controller.queueAnimation(a2, 10);
+    assert.isTrue(a1.didStopEarlyCalled);
+    assert.isTrue(a1.wasTakenOver);
+    assert.equal(a1.tickCount, 1);
+
+    assert.equal(a1, a2.takeOverForAnimation);
+    assert.isTrue(a2.startCalled);
+
+    controller.cancelActiveAnimation();
+    assert.isFalse(controller.hasActiveAnimation);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/camera.html b/trace-viewer/trace_viewer/base/ui/camera.html
new file mode 100644
index 0000000..ad17b27
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/camera.html
@@ -0,0 +1,344 @@
+<!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/ui.html">
+<link rel="import" href="/base/settings.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+
+  var constants = {
+    DEFAULT_SCALE: 0.5,
+    DEFAULT_EYE_DISTANCE: 10000,
+    MINIMUM_DISTANCE: 1000,
+    MAXIMUM_DISTANCE: 100000,
+    FOV: 15,
+    RESCALE_TIMEOUT_MS: 200,
+    MAXIMUM_TILT: 80,
+    SETTINGS_NAMESPACE: 'tv.ui_camera'
+  };
+
+
+  var Camera = tv.b.ui.define('camera');
+
+  Camera.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function(eventSource) {
+      this.eventSource_ = eventSource;
+
+      this.eventSource_.addEventListener('beginpan',
+          this.onPanBegin_.bind(this));
+      this.eventSource_.addEventListener('updatepan',
+          this.onPanUpdate_.bind(this));
+      this.eventSource_.addEventListener('endpan',
+          this.onPanEnd_.bind(this));
+
+      this.eventSource_.addEventListener('beginzoom',
+          this.onZoomBegin_.bind(this));
+      this.eventSource_.addEventListener('updatezoom',
+          this.onZoomUpdate_.bind(this));
+      this.eventSource_.addEventListener('endzoom',
+          this.onZoomEnd_.bind(this));
+
+      this.eventSource_.addEventListener('beginrotate',
+          this.onRotateBegin_.bind(this));
+      this.eventSource_.addEventListener('updaterotate',
+          this.onRotateUpdate_.bind(this));
+      this.eventSource_.addEventListener('endrotate',
+          this.onRotateEnd_.bind(this));
+
+      this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE];
+      this.gazeTarget_ = [0, 0, 0];
+      this.rotation_ = [0, 0];
+
+      this.pixelRatio_ = window.devicePixelRatio || 1;
+    },
+
+
+    get modelViewMatrix() {
+      var mvMatrix = mat4.create();
+
+      mat4.lookAt(mvMatrix, this.eye_, this.gazeTarget_, [0, 1, 0]);
+      return mvMatrix;
+    },
+
+    get projectionMatrix() {
+      var rect =
+          tv.b.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_);
+
+      var aspectRatio = rect.width / rect.height;
+      var matrix = mat4.create();
+      mat4.perspective(
+          matrix, tv.b.deg2rad(constants.FOV), aspectRatio, 1, 100000);
+
+      return matrix;
+    },
+
+    set canvas(c) {
+      this.canvas_ = c;
+    },
+
+    set deviceRect(rect) {
+      this.deviceRect_ = rect;
+    },
+
+    get stackingDistanceDampening() {
+      var gazeVector = [
+        this.gazeTarget_[0] - this.eye_[0],
+        this.gazeTarget_[1] - this.eye_[1],
+        this.gazeTarget_[2] - this.eye_[2]];
+      vec3.normalize(gazeVector, gazeVector);
+      return 1 + gazeVector[2];
+    },
+
+    loadCameraFromSettings: function(settings) {
+      this.eye_ = settings.get(
+          'eye', this.eye_, constants.SETTINGS_NAMESPACE);
+      this.gazeTarget_ = settings.get(
+          'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE);
+      this.rotation_ = settings.get(
+          'rotation', this.rotation_, constants.SETTINGS_NAMESPACE);
+
+      this.dispatchRenderEvent_();
+    },
+
+    saveCameraToSettings: function(settings) {
+      settings.set(
+          'eye', this.eye_, constants.SETTINGS_NAMESPACE);
+      settings.set(
+          'gaze_target', this.gazeTarget_, constants.SETTINGS_NAMESPACE);
+      settings.set(
+          'rotation', this.rotation_, constants.SETTINGS_NAMESPACE);
+    },
+
+    resetCamera: function() {
+      this.eye_ = [0, 0, constants.DEFAULT_EYE_DISTANCE];
+      this.gazeTarget_ = [0, 0, 0];
+      this.rotation_ = [0, 0];
+
+      var settings = tv.b.SessionSettings();
+      var keys = settings.keys(constants.SETTINGS_NAMESPACE);
+      if (keys.length !== 0) {
+        this.loadCameraFromSettings(settings);
+        return;
+      }
+
+      if (this.deviceRect_) {
+        var rect =
+            tv.b.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_);
+
+        this.eye_[0] = this.deviceRect_.width / 2;
+        this.eye_[1] = this.deviceRect_.height / 2;
+
+        this.gazeTarget_[0] = this.deviceRect_.width / 2;
+        this.gazeTarget_[1] = this.deviceRect_.height / 2;
+      }
+
+      this.saveCameraToSettings(settings);
+      this.dispatchRenderEvent_();
+    },
+
+    updatePanByDelta: function(delta) {
+      var rect =
+          tv.b.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_);
+
+      // Get the eye vector, since we'll be adjusting gazeTarget.
+      var eyeVector = [
+        this.eye_[0] - this.gazeTarget_[0],
+        this.eye_[1] - this.gazeTarget_[1],
+        this.eye_[2] - this.gazeTarget_[2]];
+      var length = vec3.length(eyeVector);
+      vec3.normalize(eyeVector, eyeVector);
+
+      var halfFov = constants.FOV / 2;
+      var multiplier =
+          2.0 * length * Math.tan(tv.b.deg2rad(halfFov)) / rect.height;
+
+      // Get the up and right vectors.
+      var up = [0, 1, 0];
+      var rotMatrix = mat4.create();
+      mat4.rotate(
+          rotMatrix, rotMatrix, tv.b.deg2rad(this.rotation_[1]), [0, 1, 0]);
+      mat4.rotate(
+          rotMatrix, rotMatrix, tv.b.deg2rad(this.rotation_[0]), [1, 0, 0]);
+      vec3.transformMat4(up, up, rotMatrix);
+
+      var right = [0, 0, 0];
+      vec3.cross(right, eyeVector, up);
+      vec3.normalize(right, right);
+
+      // Update the gaze target.
+      for (var i = 0; i < 3; ++i) {
+        this.gazeTarget_[i] +=
+            delta[0] * multiplier * right[i] - delta[1] * multiplier * up[i];
+
+        this.eye_[i] = this.gazeTarget_[i] + length * eyeVector[i];
+      }
+
+      // If we have some z offset, we need to reposition gazeTarget
+      // to be on the plane z = 0 with normal [0, 0, 1].
+      if (Math.abs(this.gazeTarget_[2]) > 1e-6) {
+        var gazeVector = [-eyeVector[0], -eyeVector[1], -eyeVector[2]];
+        var newLength = tv.b.clamp(
+            -this.eye_[2] / gazeVector[2],
+            constants.MINIMUM_DISTANCE,
+            constants.MAXIMUM_DISTANCE);
+
+        for (var i = 0; i < 3; ++i)
+          this.gazeTarget_[i] = this.eye_[i] + newLength * gazeVector[i];
+      }
+
+      this.saveCameraToSettings(tv.b.SessionSettings());
+      this.dispatchRenderEvent_();
+    },
+
+    updateZoomByDelta: function(delta) {
+      var deltaY = delta[1];
+      deltaY = tv.b.clamp(deltaY, -50, 50);
+      var scale = 1.0 - deltaY / 100.0;
+
+      var eyeVector = [0, 0, 0];
+      vec3.subtract(eyeVector, this.eye_, this.gazeTarget_);
+
+      var length = vec3.length(eyeVector);
+
+      // Clamp the length to allowed values by changing the scale.
+      if (length * scale < constants.MINIMUM_DISTANCE)
+        scale = constants.MINIMUM_DISTANCE / length;
+      else if (length * scale > constants.MAXIMUM_DISTANCE)
+        scale = constants.MAXIMUM_DISTANCE / length;
+
+      vec3.scale(eyeVector, eyeVector, scale);
+      vec3.add(this.eye_, this.gazeTarget_, eyeVector);
+
+      this.saveCameraToSettings(tv.b.SessionSettings());
+      this.dispatchRenderEvent_();
+    },
+
+    updateRotateByDelta: function(delta) {
+      delta[0] *= 0.5;
+      delta[1] *= 0.5;
+
+      if (Math.abs(this.rotation_[0] + delta[1]) > constants.MAXIMUM_TILT)
+        return;
+      if (Math.abs(this.rotation_[1] - delta[0]) > constants.MAXIMUM_TILT)
+        return;
+
+      var eyeVector = [0, 0, 0, 0];
+      vec3.subtract(eyeVector, this.eye_, this.gazeTarget_);
+
+      // Undo the current rotation.
+      var rotMatrix = mat4.create();
+      mat4.rotate(
+          rotMatrix, rotMatrix, -tv.b.deg2rad(this.rotation_[0]), [1, 0, 0]);
+      mat4.rotate(
+          rotMatrix, rotMatrix, -tv.b.deg2rad(this.rotation_[1]), [0, 1, 0]);
+      vec4.transformMat4(eyeVector, eyeVector, rotMatrix);
+
+      // Update rotation values.
+      this.rotation_[0] += delta[1];
+      this.rotation_[1] -= delta[0];
+
+      // Redo the new rotation.
+      mat4.identity(rotMatrix);
+      mat4.rotate(
+          rotMatrix, rotMatrix, tv.b.deg2rad(this.rotation_[1]), [0, 1, 0]);
+      mat4.rotate(
+          rotMatrix, rotMatrix, tv.b.deg2rad(this.rotation_[0]), [1, 0, 0]);
+      vec4.transformMat4(eyeVector, eyeVector, rotMatrix);
+
+      vec3.add(this.eye_, this.gazeTarget_, eyeVector);
+
+      this.saveCameraToSettings(tv.b.SessionSettings());
+      this.dispatchRenderEvent_();
+    },
+
+
+    // Event callbacks.
+    onPanBegin_: function(e) {
+      this.panning_ = true;
+      this.lastMousePosition_ = this.getMousePosition_(e);
+    },
+
+    onPanUpdate_: function(e) {
+      if (!this.panning_)
+        return;
+
+      var delta = this.getMouseDelta_(e, this.lastMousePosition_);
+      this.lastMousePosition_ = this.getMousePosition_(e);
+      this.updatePanByDelta(delta);
+    },
+
+    onPanEnd_: function(e) {
+      this.panning_ = false;
+    },
+
+    onZoomBegin_: function(e) {
+      this.zooming_ = true;
+
+      var p = this.getMousePosition_(e);
+
+      this.lastMousePosition_ = p;
+      this.zoomPoint_ = p;
+    },
+
+    onZoomUpdate_: function(e) {
+      if (!this.zooming_)
+        return;
+
+      var delta = this.getMouseDelta_(e, this.lastMousePosition_);
+      this.lastMousePosition_ = this.getMousePosition_(e);
+      this.updateZoomByDelta(delta);
+    },
+
+    onZoomEnd_: function(e) {
+      this.zooming_ = false;
+      this.zoomPoint_ = undefined;
+    },
+
+    onRotateBegin_: function(e) {
+      this.rotating_ = true;
+      this.lastMousePosition_ = this.getMousePosition_(e);
+    },
+
+    onRotateUpdate_: function(e) {
+      if (!this.rotating_)
+        return;
+
+      var delta = this.getMouseDelta_(e, this.lastMousePosition_);
+      this.lastMousePosition_ = this.getMousePosition_(e);
+      this.updateRotateByDelta(delta);
+    },
+
+    onRotateEnd_: function(e) {
+      this.rotating_ = false;
+    },
+
+
+    // Misc helper functions.
+    getMousePosition_: function(e) {
+      var rect = tv.b.windowRectForElement(this.canvas_);
+      return [(e.clientX - rect.x) * this.pixelRatio_,
+              (e.clientY - rect.y) * this.pixelRatio_];
+    },
+
+    getMouseDelta_: function(e, p) {
+      var newP = this.getMousePosition_(e);
+      return [newP[0] - p[0], newP[1] - p[1]];
+    },
+
+    dispatchRenderEvent_: function() {
+      tv.b.dispatchSimpleEvent(this, 'renderrequired', false, false);
+    }
+  };
+
+  return {
+    Camera: Camera
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/camera_test.html b/trace-viewer/trace_viewer/base/ui/camera_test.html
new file mode 100644
index 0000000..751a89d
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/camera_test.html
@@ -0,0 +1,60 @@
+<!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/rect.html">
+<link rel="import" href="/base/quad.html">
+<link rel="import" href="/base/bbox2.html">
+<link rel="import" href="/base/ui/quad_stack_view.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+  function createQuads() {
+    var quads = [
+      tv.b.Quad.fromXYWH(-500, -500, 30, 30), // 4 corners
+      tv.b.Quad.fromXYWH(-500, 470, 30, 30),
+      tv.b.Quad.fromXYWH(470, -500, 30, 30),
+      tv.b.Quad.fromXYWH(470, 470, 30, 30),
+      tv.b.Quad.fromXYWH(-250, -250, 250, 250), // crosshairs
+      tv.b.Quad.fromXYWH(0, -250, 250, 250), // crosshairs
+      tv.b.Quad.fromXYWH(-250, 0, 250, 250), // crosshairs
+      tv.b.Quad.fromXYWH(0, 0, 250, 250) // crosshairs
+    ];
+    quads[0].stackingGroupId = 0;
+    quads[1].stackingGroupId = 0;
+    quads[2].stackingGroupId = 0;
+    quads[3].stackingGroupId = 0;
+    quads[4].stackingGroupId = 1;
+    quads[5].stackingGroupId = 1;
+    quads[6].stackingGroupId = 1;
+    quads[7].stackingGroupId = 1;
+    return quads;
+  }
+
+  function createQuadStackView(testFramework) {
+    var quads = createQuads();
+    var view = new tv.b.ui.QuadStackView();
+    // simulate the constraints of the layer-tree-view
+    view.style.height = '400px';
+    view.style.width = '800px';
+    view.deviceRect = tv.b.Rect.fromXYWH(-250, -250, 500, 500);
+    view.quads = quads;
+
+    testFramework.addHTMLOutput(view);
+    return view;
+  }
+
+  test('initialState', function() {
+    var view = createQuadStackView(this);
+
+    var viewRect =
+        view.getBoundingClientRect();
+    assert.equal(viewRect.height, 400);
+    assert.equal(viewRect.width, 800);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/chart_base.html b/trace-viewer/trace_viewer/base/ui/chart_base.html
new file mode 100644
index 0000000..cf2ee65
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/chart_base.html
@@ -0,0 +1,293 @@
+<!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/ui.html">
+<link rel="import" href="/base/ui/color_scheme.html">
+<link rel="import" href="/base/ui/d3.html">
+
+<style>
+  * /deep/ .chart-base #title {
+    font-size: 16pt;
+  }
+
+  * /deep/ .chart-base {
+    font-size: 12pt;
+    -webkit-user-select: none;
+    cursor: default;
+  }
+
+  * /deep/ .chart-base .axis path,
+  * /deep/ .chart-base .axis line {
+    fill: none;
+    shape-rendering: crispEdges;
+    stroke: #000;
+  }
+</style>
+
+<template id="chart-base-template">
+  <svg> <!-- svg tag is dropped by ChartBase.decorate. -->
+    <g xmlns="http://www.w3.org/2000/svg" id="chart-area">
+      <g class="x axis"></g>
+      <g class="y axis"></g>
+      <text id="title"></text>
+    </g>
+  </svg>
+</template>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  var svgNS = 'http://www.w3.org/2000/svg';
+  var highlightIdBoost = tv.b.ui.getColorPaletteHighlightIdBoost();
+
+  function getColorOfKey(key, selected) {
+    var id = tv.b.ui.getColorIdForGeneralPurposeString(key);
+    if (selected)
+      id += highlightIdBoost;
+    return tv.b.ui.getColorPalette()[id];
+  }
+
+  /**
+   * A virtual base class for basic charts that provides X and Y axes, if
+   * needed, a title, and legend.
+   *
+   * @constructor
+   */
+  var ChartBase = tv.b.ui.define('svg', undefined, svgNS);
+
+  ChartBase.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.classList.add('chart-base');
+      this.chartTitle_ = undefined;
+      this.data_ = undefined;
+      this.seriesKeys_ = undefined;
+      this.width_ = 400;
+      this.height_ = 300;
+
+      // This should use tv.b.instantiateTemplate. However, creating
+      // svg-namespaced elements inside a template isn't possible. Thus, this
+      // hack.
+      var template = THIS_DOC.querySelector('#chart-base-template');
+      var svgEl = template.content.querySelector('svg');
+      for (var i = 0; i < svgEl.children.length; i++)
+        this.appendChild(svgEl.children[i].cloneNode(true));
+
+      // svg likes to take over width & height properties for some reason. This
+      // works around it.
+      Object.defineProperty(
+          this, 'width', {
+            get: function() {
+              return this.width_;
+            },
+            set: function(width) {
+              this.width_ = width;
+              this.updateContents_();
+            }
+          });
+      Object.defineProperty(
+          this, 'height', {
+            get: function() {
+              return this.height_;
+            },
+            set: function(height) {
+              this.height_ = height;
+              this.updateContents_();
+            }
+          });
+    },
+
+    get chartTitle() {
+      return chartTitle_;
+    },
+
+    set chartTitle(chartTitle) {
+      this.chartTitle_ = chartTitle;
+      this.updateContents_();
+    },
+
+    get chartAreaElement() {
+      return this.querySelector('#chart-area');
+    },
+
+    get data() {
+      return this.data_;
+    },
+
+    setSize: function(size) {
+      this.width_ = size.width;
+      this.height_ = size.height;
+      this.updateContents_();
+    },
+
+    get margin() {
+      var margin = {top: 20, right: 20, bottom: 30, left: 50};
+      if (this.chartTitle_)
+        margin.top += 20;
+      return margin;
+    },
+
+    get chartAreaSize() {
+      var margin = this.margin;
+      return {
+        width: this.width_ - margin.left - margin.right,
+        height: this.height_ - margin.top - margin.bottom
+      };
+    },
+
+    getLegendKeys_: function() {
+      throw new Error('Not implemented');
+    },
+
+    updateScales_: function(width, height) {
+      throw new Error('Not implemented');
+    },
+
+    updateContents_: function() {
+      var margin = this.margin;
+      var width = this.chartAreaSize.width;
+      var height = this.chartAreaSize.height;
+
+      var thisSel = d3.select(this);
+      thisSel.attr('width', this.width_);
+      thisSel.attr('height', this.height_);
+
+      var chartAreaSel = d3.select(this.chartAreaElement);
+      chartAreaSel.attr(
+          'transform',
+          'translate(' + margin.left + ',' + margin.top + ')');
+
+      this.updateScales_(width, height);
+
+      // Axes.
+      if (this.xScale_ && this.yScale_) {
+        var xAxisRenderer = d3.svg.axis()
+            .scale(this.xScale_)
+            .orient('bottom');
+
+        var yAxisRenderer = d3.svg.axis()
+            .scale(this.yScale_)
+            .orient('left');
+
+        chartAreaSel.select('.x.axis')
+            .attr('transform', 'translate(0,' + height + ')')
+            .call(xAxisRenderer);
+
+        chartAreaSel.select('.y.axis')
+            .call(yAxisRenderer);
+      }
+
+      // Title.
+      var titleSel = chartAreaSel.select('#title');
+      if (this.chartTitle_) {
+        titleSel.attr('transform', 'translate(' + width * 0.5 + ',-5)')
+            .style('display', undefined)
+            .style('text-anchor', 'middle')
+            .attr('class', 'title')
+            .attr('width', width)
+            .text(this.chartTitle_);
+      } else {
+        titleSel.style('display', 'none');
+      }
+
+      // Basics
+      this.updateLegend_();
+    },
+
+    updateLegend_: function() {
+      var keys = this.getLegendKeys_();
+      if (keys === undefined)
+        return;
+
+      var chartAreaSel = d3.select(this.chartAreaElement);
+      var chartAreaSize = this.chartAreaSize;
+
+      var legendEntriesSel = chartAreaSel.selectAll('.legend')
+          .data(keys.slice().reverse());
+
+      legendEntriesSel.enter()
+          .append('g')
+          .attr('class', 'legend')
+          .attr('transform', function(d, i) {
+            return 'translate(0,' + i * 20 + ')';
+          }).append('text').text(function(key) {
+            return key;
+          });
+      legendEntriesSel.exit().remove();
+
+      legendEntriesSel.attr('x', chartAreaSize.width - 18)
+          .attr('width', 18)
+          .attr('height', 18)
+          .style('fill', function(key) {
+            var selected = this.currentHighlightedLegendKey === key;
+            return getColorOfKey(key, selected);
+          }.bind(this));
+
+      legendEntriesSel.selectAll('text')
+        .attr('x', chartAreaSize.width - 24)
+        .attr('y', 9)
+        .attr('dy', '.35em')
+        .style('text-anchor', 'end')
+        .text(function(d) { return d; });
+    },
+
+    get highlightedLegendKey() {
+      return this.highlightedLegendKey_;
+    },
+
+    set highlightedLegendKey(highlightedLegendKey) {
+      this.highlightedLegendKey_ = highlightedLegendKey;
+      this.updateHighlight_();
+    },
+
+    get currentHighlightedLegendKey() {
+      if (this.tempHighlightedLegendKey_)
+        return this.tempHighlightedLegendKey_;
+      return this.highlightedLegendKey_;
+    },
+
+    pushTempHighlightedLegendKey: function(key) {
+      if (this.tempHighlightedLegendKey_)
+        throw new Error('push cannot nest');
+      this.tempHighlightedLegendKey_ = key;
+      this.updateHighlight_();
+    },
+
+    popTempHighlightedLegendKey: function(key) {
+      if (this.tempHighlightedLegendKey_ != key)
+        throw new Error('pop cannot happen');
+      this.tempHighlightedLegendKey_ = undefined;
+      this.updateHighlight_();
+    },
+
+    updateHighlight_: function() {
+      // Update label colors.
+      var chartAreaSel = d3.select(this.chartAreaElement);
+      var legendEntriesSel = chartAreaSel.selectAll('.legend');
+
+      var that = this;
+      legendEntriesSel.each(function(key) {
+        var highlighted = key == that.currentHighlightedLegendKey;
+        var color = getColorOfKey(key, highlighted);
+        this.style.fill = color;
+        if (highlighted)
+          this.style.fontWeight = 'bold';
+        else
+          this.style.fontWeight = '';
+      });
+    }
+  };
+
+  return {
+    getColorOfKey: getColorOfKey,
+    ChartBase: ChartBase
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/color_scheme.html b/trace-viewer/trace_viewer/base/ui/color_scheme.html
new file mode 100644
index 0000000..4886982
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/color_scheme.html
@@ -0,0 +1,181 @@
+<!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/iteration_helpers.html">
+<link rel="import" href="/base/ui/color_utils.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides color scheme related functions.
+ */
+tv.exportTo('tv.b.ui', function() {
+  var colorToRGBString = tv.b.ui.colorToRGBString;
+  var colorToRGBAString = tv.b.ui.colorToRGBAString;
+
+  // Basic constants...
+
+  var generalPurposeColors = [
+    {r: 138, g: 113, b: 152},
+    {r: 175, g: 112, b: 133},
+    {r: 127, g: 135, b: 225},
+    {r: 93, g: 81, b: 137},
+    {r: 116, g: 143, b: 119},
+    {r: 178, g: 214, b: 122},
+    {r: 87, g: 109, b: 147},
+    {r: 119, g: 155, b: 95},
+    {r: 114, g: 180, b: 160},
+    {r: 132, g: 85, b: 103},
+    {r: 157, g: 210, b: 150},
+    {r: 148, g: 94, b: 86},
+    {r: 164, g: 108, b: 138},
+    {r: 139, g: 191, b: 150},
+    {r: 110, g: 99, b: 145},
+    {r: 80, g: 129, b: 109},
+    {r: 125, g: 140, b: 149},
+    {r: 93, g: 124, b: 132},
+    {r: 140, g: 85, b: 140},
+    {r: 104, g: 163, b: 162},
+    {r: 132, g: 141, b: 178},
+    {r: 131, g: 105, b: 147},
+    {r: 135, g: 183, b: 98},
+    {r: 152, g: 134, b: 177},
+    {r: 141, g: 188, b: 141},
+    {r: 133, g: 160, b: 210},
+    {r: 126, g: 186, b: 148},
+    {r: 112, g: 198, b: 205},
+    {r: 180, g: 122, b: 195},
+    {r: 203, g: 144, b: 152}];
+
+  var reservedColorsByName = {
+    thread_state_iowait: {r: 182, g: 125, b: 143},
+    thread_state_running: {r: 126, g: 200, b: 148},
+    thread_state_runnable: {r: 133, g: 160, b: 210},
+    thread_state_sleeping: {r: 240, g: 240, b: 240},
+    thread_state_unknown: {r: 199, g: 155, b: 125},
+
+    memory_dump: {r: 0, g: 0, b: 180},
+
+    generic_work: {r: 125, g: 125, b: 125}
+  };
+
+  // Some constants we'll need for later lookups.
+  var numGeneralPurposeColorIds = generalPurposeColors.length;
+  var numReservedColorIds = tv.b.dictionaryLength(reservedColorsByName);
+
+  // The color palette is split in half, with the upper
+  // half of the palette being the "highlighted" verison
+  // of the base color. So, color 7's highlighted form is
+  // 7 + (palette.length / 2).
+  //
+  // These bright versions of colors are automatically generated
+  // from the base colors.
+  //
+  // Within the color palette, there are "general purpose" colors,
+  // which can be used for random color selection, and
+  // reserved colors, which are used when specific colors
+  // need to be used, e.g. where red is desired.
+  var paletteRaw = (function() {
+    var paletteBase = [];
+    paletteBase.push.apply(paletteBase, generalPurposeColors);
+    paletteBase.push.apply(paletteBase,
+                           tv.b.dictionaryValues(reservedColorsByName));
+    return paletteBase.concat(paletteBase.map(tv.b.ui.brightenColor));
+  })();
+  var palette = paletteRaw.map(colorToRGBString);
+
+  var highlightIdBoost = palette.length / 2;
+
+  // Build reservedColorNameToIdMap.
+  var reservedColorNameToIdMap = (function() {
+    var m = {};
+    var i = generalPurposeColors.length;
+    tv.b.iterItems(reservedColorsByName, function(key, value) {
+      m[key] = i++;
+    });
+    return m;
+  })();
+
+  /**
+   * Computes a simplistic hashcode of the provide name. Used to chose colors
+   * for slices.
+   * @param {string} name The string to hash.
+   */
+  function getStringHash(name) {
+    var hash = 0;
+    for (var i = 0; i < name.length; ++i)
+      hash = (hash + 37 * hash + 11 * name.charCodeAt(i)) % 0xFFFFFFFF;
+    return hash;
+  }
+
+  /**
+   * Gets the color palette.
+   */
+  function getColorPalette() {
+    return palette;
+  }
+
+  /**
+   * Gets the raw color palette, where entries are still objects.
+   */
+  function getRawColorPalette() {
+    return paletteRaw;
+  }
+
+  /**
+   * @return {Number} The value to add to a color ID to get its highlighted
+   * colro ID. E.g. 7 + getPaletteHighlightIdBoost() yields a brightened from
+   * of 7's base color.
+   */
+  function getColorPaletteHighlightIdBoost() {
+    return highlightIdBoost;
+  }
+
+  /**
+   * @param {String} name The color name.
+   * @return {Number} The color ID for the given color name.
+   */
+  function getColorIdForReservedName(name) {
+    var id = reservedColorNameToIdMap[name];
+    if (id === undefined)
+      throw new Error('Unrecognized color ') + name;
+    return id;
+  }
+
+  // Previously computed string color IDs. They are based on a stable hash, so
+  // it is safe to save them throughout the program time.
+  var stringColorIdCache = {};
+
+  /**
+   * @return {Number} A color ID that is stably associated to the provided via
+   * the getStringHash method. The color ID will be chosen from the general
+   * purpose ID space only, e.g. no reserved ID will be used.
+   */
+  function getColorIdForGeneralPurposeString(string) {
+    if (stringColorIdCache[string] === undefined) {
+      var hash = getStringHash(string);
+      stringColorIdCache[string] = hash % numGeneralPurposeColorIds;
+    }
+    return stringColorIdCache[string];
+  }
+
+  var paletteProperties = {
+    numGeneralPurposeColorIds: numGeneralPurposeColorIds,
+    highlightIdBoost: highlightIdBoost
+  };
+
+  return {
+    getRawColorPalette: getRawColorPalette,
+    getColorPalette: getColorPalette,
+    paletteProperties: paletteProperties,
+    getColorPaletteHighlightIdBoost: getColorPaletteHighlightIdBoost,
+    getColorIdForReservedName: getColorIdForReservedName,
+    getStringHash: getStringHash,
+    getColorIdForGeneralPurposeString: getColorIdForGeneralPurposeString
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/color_scheme_test.html b/trace-viewer/trace_viewer/base/ui/color_scheme_test.html
new file mode 100644
index 0000000..251d87b
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/color_scheme_test.html
@@ -0,0 +1,30 @@
+<!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.b.unittest.testSuite(function() {
+  var brighten = tv.b.ui.brightenColor;
+
+  test('buildColorPalette', function() {
+    var palette = tv.b.ui.getColorPalette();
+    var rawPalette = tv.b.ui.getRawColorPalette();
+    var highlightOffset = tv.b.ui.getColorPaletteHighlightIdBoost();
+
+    assert.strictEqual(palette.length, rawPalette.length);
+    assert.equal(palette.length % 2, 0);
+    assert.equal(palette.length / 2, highlightOffset);
+
+    for (var i = 0; i < rawPalette.length / 2; i++)
+      assert.deepEqual(rawPalette[i + highlightOffset],
+                       brighten(rawPalette[i]));
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/color_utils.html b/trace-viewer/trace_viewer/base/ui/color_utils.html
new file mode 100644
index 0000000..3e6df82
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/color_utils.html
@@ -0,0 +1,40 @@
+<!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 color scheme related functions.
+ */
+tv.exportTo('tv.b.ui', function() {
+
+  function brightenColor(c) {
+    var k;
+    if (c.r >= 240 && c.g >= 240 && c.b >= 240)
+      k = -0.20;
+    else
+      k = 0.45;
+
+    return {r: Math.min(255, c.r + Math.floor(c.r * k)),
+      g: Math.min(255, c.g + Math.floor(c.g * k)),
+      b: Math.min(255, c.b + Math.floor(c.b * k))};
+  }
+  function colorToRGBString(c) {
+    return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')';
+  }
+  function colorToRGBAString(c, a) {
+    return 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + a + ')';
+  }
+
+  return {
+    brightenColor: brightenColor,
+    colorToRGBString: colorToRGBString,
+    colorToRGBAString: colorToRGBAString
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/common.css b/trace-viewer/trace_viewer/base/ui/common.css
new file mode 100644
index 0000000..ee06a62
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/common.css
@@ -0,0 +1,11 @@
+/* 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.
+ */
+
+/* Global TVCM CSS values, applied application-wide */
+
+body * {
+  -webkit-user-select: none;
+  box-sizing: border-box;
+}
diff --git a/trace-viewer/trace_viewer/base/ui/container_that_decorates_its_children.html b/trace-viewer/trace_viewer/base/ui/container_that_decorates_its_children.html
new file mode 100644
index 0000000..86ee86f
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/container_that_decorates_its_children.html
@@ -0,0 +1,105 @@
+<!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/events.html">
+<link rel="import" href="/base/ui.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Container that decorates its children.
+ */
+tv.exportTo('tv.b.ui', function() {
+  /**
+   * @constructor
+   */
+  var ContainerThatDecoratesItsChildren = tv.b.ui.define('div');
+
+  ContainerThatDecoratesItsChildren.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.observer_ = new WebKitMutationObserver(this.didMutate_.bind(this));
+      this.observer_.observe(this, { childList: true });
+
+      // textContent is a variable on regular HTMLElements. However, we want to
+      // hook and prevent writes to it.
+      Object.defineProperty(
+          this, 'textContent',
+          { get: undefined, set: this.onSetTextContent_});
+    },
+
+    appendChild: function(x) {
+      HTMLUnknownElement.prototype.appendChild.call(this, x);
+      this.didMutate_(this.observer_.takeRecords());
+    },
+
+    insertBefore: function(x, y) {
+      HTMLUnknownElement.prototype.insertBefore.call(this, x, y);
+      this.didMutate_(this.observer_.takeRecords());
+    },
+
+    removeChild: function(x) {
+      HTMLUnknownElement.prototype.removeChild.call(this, x);
+      this.didMutate_(this.observer_.takeRecords());
+    },
+
+    replaceChild: function(x, y) {
+      HTMLUnknownElement.prototype.replaceChild.call(this, x, y);
+      this.didMutate_(this.observer_.takeRecords());
+    },
+
+    onSetTextContent_: function(textContent) {
+      if (textContent != '')
+        throw new Error('textContent can only be set to \'\'.');
+      this.clear();
+    },
+
+    clear: function() {
+      while (this.lastChild)
+        HTMLUnknownElement.prototype.removeChild.call(this, this.lastChild);
+      this.didMutate_(this.observer_.takeRecords());
+    },
+
+    didMutate_: function(records) {
+      this.beginDecorating_();
+      for (var i = 0; i < records.length; i++) {
+        var addedNodes = records[i].addedNodes;
+        if (addedNodes) {
+          for (var j = 0; j < addedNodes.length; j++)
+            this.decorateChild_(addedNodes[j]);
+        }
+        var removedNodes = records[i].removedNodes;
+        if (removedNodes) {
+          for (var j = 0; j < removedNodes.length; j++) {
+            this.undecorateChild_(removedNodes[j]);
+          }
+        }
+      }
+      this.doneDecoratingForNow_();
+    },
+
+    decorateChild_: function(child) {
+      throw new Error('Not implemented');
+    },
+
+    undecorateChild_: function(child) {
+      throw new Error('Not implemented');
+    },
+
+    beginDecorating_: function() {
+    },
+
+    doneDecoratingForNow_: function() {
+    }
+  };
+
+  return {
+    ContainerThatDecoratesItsChildren: ContainerThatDecoratesItsChildren
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/container_that_decorates_its_children_test.html b/trace-viewer/trace_viewer/base/ui/container_that_decorates_its_children_test.html
new file mode 100644
index 0000000..bc96feb
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/container_that_decorates_its_children_test.html
@@ -0,0 +1,97 @@
+<!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/ui/container_that_decorates_its_children.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+
+  function createChild() {
+    var span = document.createElement('span');
+    span.decorated = false;
+    return span;
+  }
+
+  /**
+   * @constructor
+   */
+  var SimpleContainer = tv.b.ui.define(
+      'simple-container', tv.b.ui.ContainerThatDecoratesItsChildren);
+
+  SimpleContainer.prototype = {
+    __proto__: tv.b.ui.ContainerThatDecoratesItsChildren.prototype,
+
+    decorateChild_: function(child) {
+      assert.isFalse(child.decorated);
+      child.decorated = true;
+    },
+
+    undecorateChild_: function(child) {
+      assert.isTrue(child.decorated);
+      child.decorated = false;
+    }
+  };
+
+  test('add', function() {
+    var container = new SimpleContainer();
+    container.appendChild(createChild());
+    container.appendChild(createChild());
+    container.appendChild(createChild());
+    assert.isTrue(container.children[0].decorated);
+    assert.isTrue(container.children[1].decorated);
+    assert.isTrue(container.children[2].decorated);
+  });
+
+  test('clearUsingTextContent', function() {
+    var c0 = createChild();
+    var container = new SimpleContainer();
+    container.appendChild(c0);
+    container.textContent = '';
+    assert.isFalse(c0.decorated);
+  });
+
+  test('clear', function() {
+    var c0 = createChild();
+    var container = new SimpleContainer();
+    container.appendChild(c0);
+    container.clear();
+    assert.isFalse(c0.decorated);
+  });
+
+  test('insertNewBefore', function() {
+    var c0 = createChild();
+    var c1 = createChild();
+    var container = new SimpleContainer();
+    container.appendChild(c1);
+    container.insertBefore(c0, c1);
+    assert.isTrue(c0.decorated);
+    assert.isTrue(c1.decorated);
+  });
+
+  test('insertExistingBefore', function() {
+    var c0 = createChild();
+    var c1 = createChild();
+    var container = new SimpleContainer();
+    container.appendChild(c1);
+    container.appendChild(c0);
+    container.insertBefore(c0, c1);
+    assert.isTrue(c0.decorated);
+    assert.isTrue(c1.decorated);
+  });
+
+  test('testReplace', function() {
+    var c0 = createChild();
+    var c1 = createChild();
+    var container = new SimpleContainer();
+    container.appendChild(c0);
+    container.replaceChild(c1, c0);
+    assert.isFalse(c0.decorated);
+    assert.isTrue(c1.decorated);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/d3.html b/trace-viewer/trace_viewer/base/ui/d3.html
new file mode 100644
index 0000000..ed4c962
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/d3.html
@@ -0,0 +1,7 @@
+<!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.
+-->
+<script src="/d3.min.js"></script>
diff --git a/trace-viewer/trace_viewer/base/ui/dom_helpers.html b/trace-viewer/trace_viewer/base/ui/dom_helpers.html
new file mode 100644
index 0000000..8858269
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/dom_helpers.html
@@ -0,0 +1,263 @@
+<!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/ui.html">
+<link rel="import" href="/base/settings.html">
+<style>
+.labeled-checkbox {
+  display: flex;
+  white-space: nowrap;
+}
+</style>
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+
+  function createSpan(opt_dictionary) {
+    var spanEl = document.createElement('span');
+    if (opt_dictionary) {
+      if (opt_dictionary.className)
+        spanEl.className = opt_dictionary.className;
+      if (opt_dictionary.textContent)
+        spanEl.textContent = opt_dictionary.textContent;
+      if (opt_dictionary.parent)
+        opt_dictionary.parent.appendChild(spanEl);
+      if (opt_dictionary.bold)
+        spanEl.style.fontWeight = 'bold';
+    }
+    return spanEl;
+  };
+
+  function createDiv(opt_dictionary) {
+    var divEl = document.createElement('div');
+    if (opt_dictionary) {
+      if (opt_dictionary.className)
+        divEl.className = opt_dictionary.className;
+      if (opt_dictionary.parent)
+        opt_dictionary.parent.appendChild(divEl);
+    }
+    return divEl;
+  };
+
+  function createScopedStyle(styleContent) {
+    var styleEl = document.createElement('style');
+    styleEl.scoped = true;
+    styleEl.innerHTML = styleContent;
+    return styleEl;
+  }
+
+  function valuesEqual(a, b) {
+    if (a instanceof Array && b instanceof Array)
+      return a.length === b.length && JSON.stringify(a) === JSON.stringify(b);
+    return a === b;
+  }
+
+  function createSelector(
+      targetEl, targetElProperty,
+      settingsKey, defaultValue,
+      items, opt_namespace) {
+    var defaultValueIndex;
+    for (var i = 0; i < items.length; i++) {
+      var item = items[i];
+      if (valuesEqual(item.value, defaultValue)) {
+        defaultValueIndex = i;
+        break;
+      }
+    }
+    if (defaultValueIndex === undefined)
+      throw new Error('defaultValue must be in the items list');
+
+    var selectorEl = document.createElement('select');
+    selectorEl.addEventListener('change', onChange);
+    for (var i = 0; i < items.length; i++) {
+      var item = items[i];
+      var optionEl = document.createElement('option');
+      optionEl.textContent = item.label;
+      optionEl.targetPropertyValue = item.value;
+      selectorEl.appendChild(optionEl);
+    }
+    function onChange(e) {
+      var value = selectorEl.selectedOptions[0].targetPropertyValue;
+      tv.b.Settings.set(settingsKey, value, opt_namespace);
+      targetEl[targetElProperty] = value;
+    }
+    var oldSetter = targetEl.__lookupSetter__('selectedIndex');
+    selectorEl.__defineGetter__('selectedValue', function(v) {
+      return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue;
+    });
+    selectorEl.__defineSetter__('selectedValue', function(v) {
+      for (var i = 0; i < selectorEl.children.length; i++) {
+        var value = selectorEl.children[i].targetPropertyValue;
+        if (valuesEqual(value, v)) {
+          var changed = selectorEl.selectedIndex != i;
+          if (changed) {
+            selectorEl.selectedIndex = i;
+            onChange();
+          }
+          return;
+        }
+      }
+      throw new Error('Not a valid value');
+    });
+
+    var initialValue = tv.b.Settings.get(
+        settingsKey, defaultValue, opt_namespace);
+    var didSet = false;
+    for (var i = 0; i < selectorEl.children.length; i++) {
+      if (valuesEqual(selectorEl.children[i].targetPropertyValue,
+          initialValue)) {
+        didSet = true;
+        targetEl[targetElProperty] = initialValue;
+        selectorEl.selectedIndex = i;
+        break;
+      }
+    }
+    if (!didSet) {
+      selectorEl.selectedIndex = defaultValueIndex;
+      targetEl[targetElProperty] = defaultValue;
+    }
+
+    return selectorEl;
+  }
+
+  function createEditCategorySpan(optionGroupEl, targetEl) {
+    var spanEl = createSpan({className: 'edit-categories'});
+    spanEl.textContent = 'Edit categories';
+    spanEl.classList.add('labeled-option');
+
+    spanEl.addEventListener('click', function() {
+      targetEl.onClickEditCategories();
+    });
+    return spanEl;
+  }
+
+  function createOptionGroup(targetEl, targetElProperty,
+                             settingsKey, defaultValue,
+                             items) {
+    function onChange() {
+      var value = [];
+      if (this.value.length)
+        value = this.value.split(',');
+      tv.b.Settings.set(settingsKey, value);
+      targetEl[targetElProperty] = value;
+    }
+
+    var optionGroupEl = createSpan({className: 'labeled-option-group'});
+    var initialValue = tv.b.Settings.get(settingsKey, defaultValue);
+    for (var i = 0; i < items.length; ++i) {
+      var item = items[i];
+      var id = 'category-preset-' + item.label.replace(/ /g, '-');
+
+      var radioEl = document.createElement('input');
+      radioEl.type = 'radio';
+      radioEl.setAttribute('id', id);
+      radioEl.setAttribute('name', 'category-presets-group');
+      radioEl.setAttribute('value', item.value);
+      radioEl.addEventListener('change', onChange.bind(radioEl, targetEl,
+                                                       targetElProperty,
+                                                       settingsKey));
+      if (valuesEqual(initialValue, item.value))
+        radioEl.checked = true;
+
+      var labelEl = document.createElement('label');
+      labelEl.textContent = item.label;
+      labelEl.setAttribute('for', id);
+
+      var spanEl = createSpan({className: 'labeled-option'});
+      spanEl.appendChild(radioEl);
+      spanEl.appendChild(labelEl);
+
+      spanEl.__defineSetter__('checked', function(opt_bool) {
+        var changed = radioEl.checked !== (!!opt_bool);
+        if (!changed)
+          return;
+
+        radioEl.checked = !!opt_bool;
+        onChange();
+      });
+      spanEl.__defineGetter__('checked', function() {
+        return radioEl.checked;
+      });
+
+      optionGroupEl.appendChild(spanEl);
+    }
+    optionGroupEl.appendChild(createEditCategorySpan(optionGroupEl, targetEl));
+    // Since this option group element is not yet added to the tree,
+    // querySelector will fail during updateEditCategoriesStatus_ call.
+    // Hence, creating the element with the 'expanded' classlist category
+    // added, if last selected value was 'Manual' selection.
+    if (!initialValue.length)
+      optionGroupEl.classList.add('categories-expanded');
+    targetEl[targetElProperty] = initialValue;
+
+    return optionGroupEl;
+  }
+
+  var nextCheckboxId = 1;
+  function createCheckBox(targetEl, targetElProperty,
+                          settingsKey, defaultValue,
+                          label) {
+    var buttonEl = document.createElement('input');
+    buttonEl.type = 'checkbox';
+
+    var initialValue = tv.b.Settings.get(settingsKey, defaultValue);
+    buttonEl.checked = !!initialValue;
+    if (targetEl)
+      targetEl[targetElProperty] = initialValue;
+
+    function onChange() {
+      tv.b.Settings.set(settingsKey, buttonEl.checked);
+      if (targetEl)
+        targetEl[targetElProperty] = buttonEl.checked;
+    }
+
+    buttonEl.addEventListener('change', onChange);
+
+    var id = '#checkbox-' + nextCheckboxId++;
+
+    var spanEl = createSpan({className: 'labeled-checkbox'});
+    buttonEl.setAttribute('id', id);
+
+    var labelEl = document.createElement('label');
+    labelEl.textContent = label;
+    labelEl.setAttribute('for', id);
+    spanEl.appendChild(buttonEl);
+    spanEl.appendChild(labelEl);
+
+    spanEl.__defineSetter__('checked', function(opt_bool) {
+      var changed = buttonEl.checked !== (!!opt_bool);
+      if (!changed)
+        return;
+
+      buttonEl.checked = !!opt_bool;
+      onChange();
+    });
+    spanEl.__defineGetter__('checked', function() {
+      return buttonEl.checked;
+    });
+
+    return spanEl;
+  }
+
+  function isElementAttachedToDocument(el) {
+    var cur = el;
+    while (cur.parentNode)
+      cur = cur.parentNode;
+    return (cur === el.ownerDocument || cur.nodeName === '#document-fragment');
+  }
+
+  return {
+    createSpan: createSpan,
+    createDiv: createDiv,
+    createScopedStyle: createScopedStyle,
+    createSelector: createSelector,
+    createOptionGroup: createOptionGroup,
+    createCheckBox: createCheckBox,
+    isElementAttachedToDocument: isElementAttachedToDocument
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/dom_helpers_test.html b/trace-viewer/trace_viewer/base/ui/dom_helpers_test.html
new file mode 100644
index 0000000..4ed278d
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/dom_helpers_test.html
@@ -0,0 +1,115 @@
+<!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/ui/dom_helpers.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+  test('simpleSpanAndDiv', function() {
+    var divEl = tv.b.ui.createDiv({
+      className: 'a-div-class', parent: document.body
+    });
+    var testText = 'some span text';
+    var spanEl = tv.b.ui.createSpan({
+      className: 'a-span-class',
+      textContent: testText,
+      parent: divEl
+    });
+    var eltInDocument = document.querySelector('.a-div-class>.a-span-class');
+    assert.equal(eltInDocument.textContent, testText);
+    eltInDocument.parentElement.removeChild(eltInDocument);
+  });
+
+  test('checkboxFromDefaults', function() {
+    var target = {foo: undefined};
+    var cb = tv.b.ui.createCheckBox(target, 'foo', 'myCheckBox', false, 'Foo');
+    assert.isFalse(target.foo);
+  });
+
+  test('checkboxFromSettings', function() {
+    tv.b.Settings.set('myCheckBox', true);
+    var target = {foo: undefined};
+    var cb = tv.b.ui.createCheckBox(target, 'foo', 'myCheckBox', false, 'Foo');
+    assert.isTrue(target.foo);
+  });
+
+  test('checkboxChanged', function() {
+    var target = {foo: undefined};
+    var cb = tv.b.ui.createCheckBox(target, 'foo', 'myCheckBox', false, 'Foo');
+    cb.checked = true;
+
+    assert.isTrue(tv.b.Settings.get('myCheckBox', undefined));
+    assert.isTrue(target.foo);
+  });
+
+  test('selectorSettingsAlreaySet', function() {
+    tv.b.Settings.set('myScale', 0.25);
+
+    var target = {
+      scale: 314
+    };
+    var sel = tv.b.ui.createSelector(
+        target, 'scale',
+        'myScale', 0.375,
+        [{label: '6.25%', value: 0.0625},
+         {label: '12.5%', value: 0.125},
+         {label: '25%', value: 0.25},
+         {label: '37.5%', value: 0.375},
+         {label: '50%', value: 0.5},
+         {label: '75%', value: 0.75},
+         {label: '100%', value: 1},
+         {label: '200%', value: 2}
+        ]);
+    assert.equal(target.scale, 0.25);
+    assert.equal(sel.selectedIndex, 2);
+  });
+
+  test('selectorSettingsDefault', function() {
+    var target = {
+      scale: 314
+    };
+    var sel = tv.b.ui.createSelector(
+        target, 'scale',
+        'myScale', 0.375,
+        [{label: '6.25%', value: 0.0625},
+         {label: '12.5%', value: 0.125},
+         {label: '25%', value: 0.25},
+         {label: '37.5%', value: 0.375},
+         {label: '50%', value: 0.5},
+         {label: '75%', value: 0.75},
+         {label: '100%', value: 1},
+         {label: '200%', value: 2}
+        ]);
+    assert.equal(target.scale, 0.375);
+    assert.equal(sel.selectedIndex, 3);
+  });
+
+  test('selectorSettingsChanged', function() {
+    var target = {
+      scale: 314
+    };
+    var sel = tv.b.ui.createSelector(
+        target, 'scale',
+        'myScale', 0.375,
+        [{label: '6.25%', value: 0.0625},
+         {label: '12.5%', value: 0.125},
+         {label: '25%', value: 0.25},
+         {label: '37.5%', value: 0.375},
+         {label: '50%', value: 0.5},
+         {label: '75%', value: 0.75},
+         {label: '100%', value: 1},
+         {label: '200%', value: 2}
+        ]);
+    assert.equal(sel.selectedValue, 0.375);
+    sel.selectedValue = 0.75;
+    assert.equal(target.scale, 0.75);
+    assert.equal(sel.selectedValue, 0.75);
+    assert.equal(undefined), 0.75, tv.b.Settings.get('myScale');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/drag_handle.css b/trace-viewer/trace_viewer/base/ui/drag_handle.css
new file mode 100644
index 0000000..2cd2b1c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/drag_handle.css
@@ -0,0 +1,36 @@
+/* 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-drag-handle {
+  -webkit-user-select: none;
+  box-sizing: border-box;
+  display: block;
+}
+
+x-drag-handle.horizontal-drag-handle {
+  background-image: -webkit-gradient(linear,
+                                     0 0, 0 100%,
+                                     from(#E5E5E5),
+                                     to(#D1D1D1));
+  border-bottom: 1px solid #8e8e8e;
+  border-top: 1px solid white;
+  cursor: ns-resize;
+  height: 7px;
+  position: relative;
+  z-index: 10;
+}
+
+x-drag-handle.vertical-drag-handle {
+  background-image: -webkit-gradient(linear,
+                                     0 0, 100% 0,
+                                     from(#E5E5E5),
+                                     to(#D1D1D1));
+  border-left: 1px solid white;
+  border-right: 1px solid #8e8e8e;
+  cursor: ew-resize;
+  position: relative;
+  width: 7px;
+  z-index: 10;
+}
diff --git a/trace-viewer/trace_viewer/base/ui/drag_handle.html b/trace-viewer/trace_viewer/base/ui/drag_handle.html
new file mode 100644
index 0000000..4c2a466
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/drag_handle.html
@@ -0,0 +1,163 @@
+<!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/ui.html">
+<link rel="stylesheet" href="/base/ui/drag_handle.css">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+
+  /**
+   * Detects when user clicks handle determines new height of container based
+   * on user's vertical mouse move and resizes the target.
+   * @constructor
+   * @extends {HTMLDivElement}
+   * You will need to set target to be the draggable element
+   */
+  var DragHandle = tv.b.ui.define('x-drag-handle');
+
+  DragHandle.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function() {
+      this.lastMousePos_ = 0;
+      this.onMouseMove_ = this.onMouseMove_.bind(this);
+      this.onMouseUp_ = this.onMouseUp_.bind(this);
+      this.addEventListener('mousedown', this.onMouseDown_);
+      this.target_ = undefined;
+      this.horizontal = true;
+      this.observer_ = new WebKitMutationObserver(
+          this.didTargetMutate_.bind(this));
+      this.targetSizesByModeKey_ = {};
+    },
+
+    get modeKey_() {
+      return this.target_.className == '' ? '.' : this.target_.className;
+    },
+
+    get target() {
+      return this.target_;
+    },
+
+    set target(target) {
+      this.observer_.disconnect();
+      this.target_ = target;
+      if (!this.target_)
+        return;
+      this.observer_.observe(this.target_, {
+        attributes: true,
+        attributeFilter: ['class']
+      });
+    },
+
+    get horizontal() {
+      return this.horizontal_;
+    },
+
+    set horizontal(h) {
+      this.horizontal_ = h;
+      if (this.horizontal_)
+        this.className = 'horizontal-drag-handle';
+      else
+        this.className = 'vertical-drag-handle';
+    },
+
+    get vertical() {
+      return !this.horizontal_;
+    },
+
+    set vertical(v) {
+      this.horizontal = !v;
+    },
+
+    forceMutationObserverFlush_: function() {
+      var records = this.observer_.takeRecords();
+      if (records.length)
+        this.didTargetMutate_(records);
+    },
+
+    didTargetMutate_: function(e) {
+      var modeSize = this.targetSizesByModeKey_[this.modeKey_];
+      if (modeSize !== undefined) {
+        this.setTargetSize_(modeSize);
+        return;
+      }
+
+      // If we hadn't previously sized the target, then just remove any manual
+      // sizing that we applied.
+      this.target_.style[this.targetStyleKey_] = '';
+    },
+
+    get targetStyleKey_() {
+      return this.horizontal_ ? 'height' : 'width';
+    },
+
+    getTargetSize_: function() {
+      // If style is not set, start off with computed height.
+      var targetStyleKey = this.targetStyleKey_;
+      if (!this.target_.style[targetStyleKey]) {
+        this.target_.style[targetStyleKey] =
+            window.getComputedStyle(this.target_)[targetStyleKey];
+      }
+      var size = parseInt(this.target_.style[targetStyleKey]);
+      this.targetSizesByModeKey_[this.modeKey_] = size;
+      return size;
+    },
+
+    setTargetSize_: function(s) {
+      this.target_.style[this.targetStyleKey_] = s + 'px';
+      this.targetSizesByModeKey_[this.modeKey_] = s;
+    },
+
+    applyDelta_: function(delta) {
+      // Apply new size to the container.
+      var curSize = this.getTargetSize_();
+      var newSize;
+      if (this.target_ === this.nextElementSibling) {
+        newSize = curSize + delta;
+      } else {
+        newSize = curSize - delta;
+      }
+      this.setTargetSize_(newSize);
+    },
+
+    onMouseMove_: function(e) {
+      // Compute the difference in height position.
+      var curMousePos = this.horizontal_ ? e.clientY : e.clientX;
+      var delta = this.lastMousePos_ - curMousePos;
+
+      this.applyDelta_(delta);
+
+      this.lastMousePos_ = curMousePos;
+      e.preventDefault();
+      return true;
+    },
+
+    onMouseDown_: function(e) {
+      if (!this.target_)
+        return;
+      this.forceMutationObserverFlush_();
+      this.lastMousePos_ = this.horizontal_ ? e.clientY : e.clientX;
+      document.addEventListener('mousemove', this.onMouseMove_);
+      document.addEventListener('mouseup', this.onMouseUp_);
+      e.preventDefault();
+      return true;
+    },
+
+    onMouseUp_: function(e) {
+      document.removeEventListener('mousemove', this.onMouseMove_);
+      document.removeEventListener('mouseup', this.onMouseUp_);
+      e.preventDefault();
+    }
+  };
+
+  return {
+    DragHandle: DragHandle
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/drag_handle_test.html b/trace-viewer/trace_viewer/base/ui/drag_handle_test.html
new file mode 100644
index 0000000..4280773
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/drag_handle_test.html
@@ -0,0 +1,94 @@
+<!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/ui/drag_handle.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var createDragHandle = function() {
+    var el = document.createElement('div');
+    el.style.border = '1px solid black';
+    el.style.width = '200px';
+    el.style.height = '200px';
+    el.style.display = '-webkit-flex';
+    el.style.webkitFlexDirection = 'column';
+
+    var upperEl = document.createElement('div');
+    upperEl.style.webkitFlex = '1 1 auto';
+
+    var lowerEl = document.createElement('div');
+    lowerEl.style.height = '100px';
+
+    var dragHandle = new tv.b.ui.DragHandle();
+    dragHandle.target = lowerEl;
+
+    el.appendChild(upperEl);
+    el.appendChild(dragHandle);
+    el.appendChild(lowerEl);
+    el.upperEl = upperEl;
+    el.dragHandle = dragHandle;
+    el.lowerEl = lowerEl;
+
+    el.getLowerElHeight = function() {
+      return parseInt(getComputedStyle(this.lowerEl).height);
+    };
+    return el;
+  };
+
+  test('instantiate', function() {
+    this.addHTMLOutput(createDragHandle());
+  });
+
+  test('applyDelta', function() {
+    var el = createDragHandle();
+    document.body.appendChild(el);
+
+    var dragHandle = el.dragHandle;
+    var oldHeight = el.getLowerElHeight();
+    dragHandle.applyDelta_(10);
+    assert.equal(el.getLowerElHeight(), oldHeight + 10);
+
+    document.body.removeChild(el);
+  });
+
+  test('classNameMutation', function() {
+    var el = createDragHandle();
+
+    var styleEl = document.createElement('style');
+    styleEl.textContent =
+        '.mode-a { height: 100px; } .mode-b { height: 50px; }';
+    document.head.appendChild(styleEl);
+
+    document.body.appendChild(el);
+
+    var dragHandle = el.dragHandle;
+    el.lowerEl.className = 'mode-a';
+    assert.equal(el.getLowerElHeight(), 100);
+
+    dragHandle.applyDelta_(10);
+    assert.equal(el.getLowerElHeight(), 110);
+
+    // Change the class, which should restore the layout
+    // to the default sizing for mode-b
+    el.lowerEl.className = 'mode-b';
+    dragHandle.forceMutationObserverFlush_();
+    assert.equal(el.getLowerElHeight(), 50);
+
+    dragHandle.applyDelta_(10);
+    assert.equal(el.getLowerElHeight(), 60);
+
+    // Restore the class-a, which should restore the layout
+    // to sizing when we were changed.
+    el.lowerEl.className = 'mode-a';
+    dragHandle.forceMutationObserverFlush_();
+    assert.equal(el.getLowerElHeight(), 110);
+
+    document.head.removeChild(styleEl);
+    document.body.removeChild(el);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/info_bar.html b/trace-viewer/trace_viewer/base/ui/info_bar.html
new file mode 100644
index 0000000..1270fb9
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/info_bar.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="/base/ui.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<polymer-element name='tv-b-ui-info-bar' is='HTMLDivElement'>
+  <template>
+    <style>
+    :host {
+      align-items: center;
+      flex: 0 0 auto;
+      background-color: rgb(252, 235, 162);
+      border-bottom: 1px solid #A3A3A3;
+      border-left: 1px solid white;
+      border-right: 1px solid #A3A3A3;
+      border-top: 1px solid white;
+      display: flex;
+      height: 26px;
+      padding: 0 3px 0 3px;
+    }
+
+    .info-bar-hidden { display: none; }
+    #message { flex: 1 1 auto; }
+    </style>
+
+    <span id='message'></span>
+    <span id='buttons'></span>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.messageEl_ = this.$.message;
+      this.buttonsEl_ = this.$.buttons;
+
+      this.message = '';
+      this.visible = false;
+    },
+
+    get message() {
+      return this.messageEl_.textContent;
+    },
+
+    set message(message) {
+      this.messageEl_.textContent = message;
+    },
+
+    get visible() {
+      return !this.classList.contains('info-bar-hidden');
+    },
+
+    set visible(visible) {
+      if (visible)
+        this.classList.remove('info-bar-hidden');
+      else
+        this.classList.add('info-bar-hidden');
+    },
+
+    removeAllButtons: function() {
+      this.buttonsEl_.textContent = '';
+    },
+
+    addButton: function(text, clickCallback) {
+      var button = document.createElement('button');
+      button.textContent = text;
+      button.addEventListener('click', clickCallback);
+      this.buttonsEl_.appendChild(button);
+      return button;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/base/ui/info_bar_group.html b/trace-viewer/trace_viewer/base/ui/info_bar_group.html
new file mode 100644
index 0000000..fb34cfb
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/info_bar_group.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/ui/info_bar.html'>
+
+<polymer-element name='tv-b-ui-info-bar-group' is='HTMLUnknownElement'>
+  <template>
+    <style>
+    :host {
+      flex: 0 0 auto;
+      flex-direction: column;
+      display: flex;
+    }
+    </style>
+    <div id='messages'></div>
+  </template>
+
+  <script>
+  'use strict';
+  Polymer({
+    ready: function() {
+      this.messages_ = [];
+    },
+
+    clearMessages: function() {
+      this.messages_ = [];
+      this.updateContents_();
+    },
+
+    addMessage: function(text, opt_buttons) {
+      opt_buttons = opt_buttons || [];
+      for (var i = 0; i < opt_buttons.length; i++) {
+        if (opt_buttons[i].buttonText === undefined)
+          throw new Error('buttonText must be provided');
+        if (opt_buttons[i].onClick === undefined)
+          throw new Error('onClick must be provided');
+      }
+
+      this.messages_.push({
+        text: text,
+        buttons: opt_buttons || []
+      });
+      this.updateContents_();
+    },
+
+    updateContents_: function() {
+      this.$.messages.textContent = '';
+      this.messages_.forEach(function(message) {
+        var bar = document.createElement('tv-b-ui-info-bar');
+        bar.message = message.text;
+        bar.visible = true;
+
+        message.buttons.forEach(function(button) {
+          bar.addButton(button.buttonText, button.onClick);
+        }, this);
+
+        this.$.messages.appendChild(bar);
+      }, this);
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/base/ui/info_bar_group_test.html b/trace-viewer/trace_viewer/base/ui/info_bar_group_test.html
new file mode 100644
index 0000000..4b1719e
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/info_bar_group_test.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="/base/ui/info_bar_group.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('group-instantiate', function() {
+    var infoBarGroup = document.createElement('tv-b-ui-info-bar-group');
+    infoBarGroup.addMessage(
+        'Message 1',
+        [{buttonText: 'ok', onClick: function() {}}]);
+    infoBarGroup.addMessage(
+        'Message 2',
+        [{buttonText: 'button 2', onClick: function() {}}]);
+    this.addHTMLOutput(infoBarGroup);
+  });
+
+  test('group-populate-then-clear', function() {
+    var infoBarGroup = document.createElement('tv-b-ui-info-bar-group');
+    infoBarGroup.addMessage(
+        'Message 1',
+        [{buttonText: 'ok', onClick: function() {}}]);
+    infoBarGroup.addMessage(
+        'Message 2',
+        [{buttonText: 'button 2', onClick: function() {}}]);
+    infoBarGroup.clearMessages();
+    assert.equal(infoBarGroup.children.length, 0);
+  });
+
+  test('group-populate-clear-repopulate', function() {
+    var infoBarGroup = document.createElement('tv-b-ui-info-bar-group');
+    infoBarGroup.addMessage(
+        'Message 1',
+        [{buttonText: 'ok', onClick: function() {}}]);
+    infoBarGroup.addMessage(
+        'Message 2',
+        [{buttonText: 'button 2', onClick: function() {}}]);
+    infoBarGroup.clearMessages();
+    infoBarGroup.addMessage(
+        'Message 1',
+        [{buttonText: 'ok', onClick: function() {}}]);
+    this.addHTMLOutput(infoBarGroup);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/info_bar_test.html b/trace-viewer/trace_viewer/base/ui/info_bar_test.html
new file mode 100644
index 0000000..b63b9b4
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/info_bar_test.html
@@ -0,0 +1,32 @@
+<!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/ui/info_bar.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var infoBar = document.createElement('tv-b-ui-info-bar');
+    infoBar.message = 'This is an info';
+    infoBar.visible = true;
+    this.addHTMLOutput(infoBar);
+  });
+
+  test('buttons', function() {
+    var infoBar = document.createElement('tv-b-ui-info-bar');
+    infoBar.visible = true;
+    infoBar.message = 'This is an info bar with buttons';
+    var didClick = false;
+    var button = infoBar.addButton('More info...', function() {
+      didClick = true;
+    });
+    button.click();
+    assert.isTrue(didClick);
+    this.addHTMLOutput(infoBar);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/line_chart.css b/trace-viewer/trace_viewer/base/ui/line_chart.css
new file mode 100644
index 0000000..e2c945b
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/line_chart.css
@@ -0,0 +1,13 @@
+/* Copyright 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.
+ */
+
+* /deep/ .line-chart .line {
+  fill: none;
+  stroke-width: 1.5px;
+}
+
+* /deep/ .line-chart #brushes > rect {
+  fill: rgb(192, 192, 192);
+}
diff --git a/trace-viewer/trace_viewer/base/ui/line_chart.html b/trace-viewer/trace_viewer/base/ui/line_chart.html
new file mode 100644
index 0000000..4297ab2
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/line_chart.html
@@ -0,0 +1,254 @@
+<!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">
+<link rel="import" href="/base/ui/d3.html">
+<link rel="import" href="/base/ui/chart_base.html">
+<link rel="import" href="/base/ui/mouse_tracker.html">
+<link rel="stylesheet" href="/base/ui/line_chart.css">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+  var ChartBase = tv.b.ui.ChartBase;
+  var getColorOfKey = tv.b.ui.getColorOfKey;
+
+  function getSampleWidth(data, index, leftSide) {
+    var leftIndex, rightIndex;
+    if (leftSide) {
+      leftIndex = Math.max(index - 1, 0);
+      rightIndex = index;
+    } else {
+      leftIndex = index;
+      rightIndex = Math.min(index + 1, data.length - 1);
+    }
+    var leftWidth = data[index].x - data[leftIndex].x;
+    var rightWidth = data[rightIndex].x - data[index].x;
+    return leftWidth * 0.5 + rightWidth * 0.5;
+  }
+
+  /**
+   * @constructor
+   */
+  var LineChart = tv.b.ui.define('line-chart', ChartBase);
+
+  LineChart.prototype = {
+    __proto__: ChartBase.prototype,
+
+    decorate: function() {
+      ChartBase.prototype.decorate.call(this);
+      this.classList.add('line-chart');
+
+      this.brushedRange_ = new tv.b.Range();
+
+      this.xScale_ = d3.scale.linear();
+      this.yScale_ = d3.scale.linear();
+      d3.select(this.chartAreaElement)
+          .append('g')
+          .attr('id', 'brushes');
+      d3.select(this.chartAreaElement)
+          .append('g')
+          .attr('id', 'series');
+
+      this.addEventListener('mousedown', this.onMouseDown_.bind(this));
+    },
+
+    /**
+     * Sets the data array for the object
+     *
+     * @param {Array} data The data. Each element must be an object, with at
+     * least an x property. All other properties become series names in the
+     * chart.
+     */
+    set data(data) {
+      if (data.length == 0)
+        throw new Error('Data must be nonzero. Pass undefined.');
+
+      var keys;
+      if (data !== undefined) {
+        var d = data[0];
+        if (d.x === undefined)
+          throw new Error('Elements must have "x" fields');
+        keys = d3.keys(data[0]);
+        keys.splice(keys.indexOf('x'), 1);
+        if (keys.length == 0)
+          throw new Error('Elements must have at least one other field than X');
+      } else {
+        keys = undefined;
+      }
+      this.data_ = data;
+      this.seriesKeys_ = keys;
+
+      this.updateContents_();
+    },
+
+    // Note: range can only be set, not retrieved. It needs to be immutable
+    // or else odd data binding effects will result.
+    set brushedRange(range) {
+      this.brushedRange_.reset();
+      this.brushedRange_.addRange(range);
+      this.updateContents_();
+    },
+
+    computeBrushRangeFromIndices: function(indexA, indexB) {
+      var r = new tv.b.Range();
+      var leftIndex = Math.min(indexA, indexB);
+      var rightIndex = Math.max(indexA, indexB);
+      leftIndex = Math.max(0, leftIndex);
+      rightIndex = Math.min(this.data_.length - 1, rightIndex);
+      r.addValue(this.data_[leftIndex].x -
+          getSampleWidth(this.data_, leftIndex, true));
+      r.addValue(this.data_[rightIndex].x +
+          getSampleWidth(this.data_, rightIndex, false));
+      return r;
+    },
+
+    getLegendKeys_: function() {
+      if (this.seriesKeys_ &&
+          this.seriesKeys_.length > 1)
+        return this.seriesKeys_.slice();
+      return [];
+    },
+
+    updateScales_: function(width, height) {
+      if (this.data_ === undefined)
+        return;
+
+      // X.
+      this.xScale_.range([0, width]);
+      this.xScale_.domain(d3.extent(this.data_, function(d) { return d.x; }));
+
+      // Y.
+      var yRange = new tv.b.Range();
+      this.data_.forEach(function(d) {
+        this.seriesKeys_.forEach(function(k) {
+          yRange.addValue(d[k]);
+        });
+      }, this);
+
+      this.yScale_.range([height, 0]);
+      this.yScale_.domain([yRange.min, yRange.max]);
+    },
+
+    updateContents_: function() {
+      ChartBase.prototype.updateContents_.call(this);
+      if (!this.data_)
+        return;
+
+      var chartAreaSel = d3.select(this.chartAreaElement);
+
+      var brushes = this.brushedRange_.isEmpty ? [] : [this.brushedRange_];
+
+      var brushRectsSel = chartAreaSel.select('#brushes')
+          .selectAll('rect').data(brushes);
+      brushRectsSel.enter()
+          .append('rect');
+      brushRectsSel.exit().remove();
+      brushRectsSel
+        .attr('x', function(d) {
+            return this.xScale_(d.min);
+          }.bind(this))
+        .attr('y', 0)
+        .attr('width', function(d) {
+            return this.xScale_(d.max) - this.xScale_(d.min);
+          }.bind(this))
+        .attr('height', this.chartAreaSize.height);
+
+
+      var seriesSel = chartAreaSel.select('#series');
+      var pathsSel = seriesSel.selectAll('path').data(this.seriesKeys_);
+      pathsSel.enter()
+          .append('path')
+          .attr('class', 'line')
+          .style('stroke', function(key) {
+            return getColorOfKey(key);
+          })
+          .attr('d', function(key) {
+            var line = d3.svg.line()
+              .x(function(d) { return this.xScale_(d.x); }.bind(this))
+              .y(function(d) { return this.yScale_(d[key]); }.bind(this));
+            return line(this.data_);
+          }.bind(this));
+      pathsSel.exit().remove();
+    },
+
+    getDataIndexAtClientPoint_: function(clientX, clientY, clipToY) {
+      var rect = this.getBoundingClientRect();
+      var margin = this.margin;
+      var chartAreaSize = this.chartAreaSize;
+
+      var x = clientX - rect.left - margin.left;
+      var y = clientY - rect.top - margin.top;
+
+      // Don't check width: let people select the left- and right-most data
+      // points.
+      if (clipToY) {
+        if (y < 0 ||
+            y >= chartAreaSize.height)
+          return undefined;
+      }
+
+      var dataX = this.xScale_.invert(x);
+
+      var index;
+      if (this.data_) {
+        var bisect = d3.bisector(function(d) { return d.x; }).right;
+        index = bisect(this.data_, dataX) - 1;
+      }
+
+      return index;
+    },
+
+    onMouseDown_: function(e) {
+      var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, true);
+
+      if (index !== undefined) {
+        tv.b.ui.trackMouseMovesUntilMouseUp(
+            this.onMouseMove_.bind(this, e.button),
+            this.onMouseUp_.bind(this, e.button));
+      }
+      e.preventDefault();
+      e.stopPropagation();
+
+      var event = new Event('item-mousedown');
+      event.data = this.data_[index];
+      event.index = index;
+      event.buttons = e.buttons;
+      this.dispatchEvent(event);
+    },
+
+    onMouseMove_: function(button, e) {
+      var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, false);
+      if (e.buttons !== undefined) {
+        e.preventDefault();
+        e.stopPropagation();
+      }
+
+      var event = new Event('item-mousemove');
+      event.data = this.data_[index];
+      event.index = index;
+      event.button = button;
+      this.dispatchEvent(event);
+    },
+
+    onMouseUp_: function(button, e) {
+      var index = this.getDataIndexAtClientPoint_(e.clientX, e.clientY, false);
+      e.preventDefault();
+      e.stopPropagation();
+
+      var event = new Event('item-mouseup');
+      event.data = this.data_[index];
+      event.index = index;
+      event.button = button;
+      this.dispatchEvent(event);
+    }
+  };
+
+  return {
+    LineChart: LineChart
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/line_chart_test.html b/trace-viewer/trace_viewer/base/ui/line_chart_test.html
new file mode 100644
index 0000000..cae753b
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/line_chart_test.html
@@ -0,0 +1,127 @@
+<!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/ui/line_chart.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('singleSeries', function() {
+    var chart = new tv.b.ui.LineChart();
+    chart.width = 400;
+    chart.height = 200;
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {x: 10, y: 100},
+      {x: 20, y: 110},
+      {x: 30, y: 100},
+      {x: 40, y: 50}
+    ];
+    chart.data = data;
+    this.addHTMLOutput(chart);
+  });
+
+  test('twoSeries', function() {
+    var chart = new tv.b.ui.LineChart();
+
+    chart.width = 400;
+    chart.height = 200;
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {x: 10, value1: 100, value2: 50},
+      {x: 20, value1: 110, value2: 75},
+      {x: 30, value1: 100, value2: 125},
+      {x: 40, value1: 50, value2: 125}
+    ];
+    chart.data = data;
+
+    var r = new tv.b.Range();
+    r.addValue(20);
+    r.addValue(40);
+    chart.brushedRange = r;
+
+    this.addHTMLOutput(chart);
+  });
+
+  test('brushRangeFromIndices', function() {
+    var chart = new tv.b.ui.LineChart();
+    var data = [
+      {x: 10, value: 50},
+      {x: 30, value: 60},
+      {x: 70, value: 70},
+      {x: 80, value: 80},
+      {x: 120, value: 90}
+    ];
+    chart.data = data;
+    var r = new tv.b.Range();
+
+    // Range min should be 10.
+    r = chart.computeBrushRangeFromIndices(-2, 1);
+    assert.equal(r.min, 10);
+
+    // Range max should be 120.
+    r = chart.computeBrushRangeFromIndices(3, 10);
+    assert.equal(r.max, 120);
+
+    // Range should be [10, 120]
+    r = chart.computeBrushRangeFromIndices(-2, 10);
+    assert.equal(r.min, 10);
+    assert.equal(r.max, 120);
+
+    // Range should be [20, 100]
+    r = chart.computeBrushRangeFromIndices(1, 3);
+    assert.equal(r.min, 20);
+    assert.equal(r.max, 100);
+  });
+
+  test('interactiveBrushing', function() {
+    var chart = new tv.b.ui.LineChart();
+    chart.width = 400;
+    chart.height = 200;
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {x: 10, value: 50},
+      {x: 20, value: 60},
+      {x: 30, value: 80},
+      {x: 40, value: 20},
+      {x: 50, value: 30},
+      {x: 60, value: 20},
+      {x: 70, value: 15},
+      {x: 80, value: 20}
+    ];
+    chart.data = data;
+
+    var mouseDownIndex = undefined;
+    var curMouseIndex = undefined;
+
+    function updateBrushedRange() {
+      if (mouseDownIndex === undefined) {
+        chart.brushedRange = new tv.b.Range();
+        return;
+      }
+      chart.brushedRange = chart.computeBrushRangeFromIndices(
+          mouseDownIndex, curMouseIndex);
+    }
+
+    chart.addEventListener('item-mousedown', function(e) {
+      mouseDownIndex = e.index;
+      curMouseIndex = e.index;
+      updateBrushedRange();
+    });
+    chart.addEventListener('item-mousemove', function(e) {
+      if (e.button == undefined)
+        return;
+      curMouseIndex = e.index;
+      updateBrushedRange();
+    });
+    chart.addEventListener('item-mouseup', function(e) {
+      curMouseIndex = e.index;
+      updateBrushedRange();
+    });
+    this.addHTMLOutput(chart);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/list_and_associated_view.css b/trace-viewer/trace_viewer/base/ui/list_and_associated_view.css
new file mode 100644
index 0000000..2ef74a4
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/list_and_associated_view.css
@@ -0,0 +1,17 @@
+/* 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-list-and-associated-view {
+  -webkit-flex-direction: row;
+  display: -webkit-flex;
+}
+
+x-list-and-associated-view > .x-list-view {
+  min-width: 100px;
+}
+
+x-list-and-associated-view > :nth-child(2) {
+  -webkit-flex: 1;
+}
diff --git a/trace-viewer/trace_viewer/base/ui/list_and_associated_view.html b/trace-viewer/trace_viewer/base/ui/list_and_associated_view.html
new file mode 100644
index 0000000..bae7d0d
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/list_and_associated_view.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="/base/ui.html">
+<link rel="import" href="/base/ui/list_view.html">
+<link rel="stylesheet" href="/base/ui/list_and_associated_view.css">
+<script>
+'use strict';
+
+/**
+ * @fileoverview A list of things, and a viewer for the currently selected
+ * thing.
+ */
+tv.exportTo('tv.b.ui', function() {
+
+  /**
+   * @constructor
+   */
+  var ListAndAssociatedView = tv.b.ui.define('x-list-and-associated-view');
+  ListAndAssociatedView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.list_ = undefined;
+      this.listProperty_ = undefined;
+      this.view_ = undefined;
+      this.viewProperty_ = undefined;
+      this.listView_ = new tv.b.ui.ListView();
+      this.listView_.addEventListener('selection-changed',
+                                      this.onSelectionChanged_.bind(this));
+      this.placeholder_ = document.createElement('div');
+      this.appendChild(this.listView_);
+      this.appendChild(this.placeholder_);
+    },
+
+    get listView() {
+      return this.listView_;
+    },
+
+    get list() {
+      return this.list_;
+    },
+
+    set list(list) {
+      this.list_ = list;
+      this.updateChildren_();
+    },
+
+    get listProperty() {
+      return this.listProperty_;
+    },
+
+    set listProperty(listProperty) {
+      this.listProperty_ = listProperty;
+      this.updateChildren_();
+    },
+
+    get view() {
+      return this.view_;
+    },
+
+    set view(view) {
+      this.view_ = view;
+      this.updateChildren_();
+    },
+
+    get viewProperty() {
+      return this.viewProperty_;
+    },
+
+    set viewProperty(viewProperty) {
+      this.viewProperty_ = viewProperty;
+      this.updateChildren_();
+    },
+
+    updateChildren_: function() {
+      var complete = this.list_ &&
+          this.listProperty_ &&
+          this.view_ &&
+          this.viewProperty_;
+      if (!complete) {
+        this.replaceChild(this.placeholder_,
+                          this.children[1]);
+        return;
+      }
+
+      for (var i = 0; i < this.list_.length; i++) {
+        var itemEl;
+        if (i >= this.listView_.children.length) {
+          itemEl = document.createElement('div');
+          this.listView_.appendChild(itemEl);
+        } else {
+          itemEl = this.listView_.children[i];
+        }
+        itemEl.item = this.list_[i];
+        var getter = this.list_[i].__lookupGetter__(this.listProperty_);
+        if (getter)
+          itemEl.textContent = getter.call(this.list_[i]);
+        else
+          itemEl.textContent = this.list_[i][this.listProperty_];
+      }
+
+      if (this.children[1] == this.placeholder_) {
+        this.replaceChild(this.view_,
+                          this.children[1]);
+      }
+      if (this.listView_.children.length &&
+          !this.listView_.selectedElement)
+        this.listView_.selectedElement = this.listView_.children[0];
+    },
+
+    onSelectionChanged_: function(e) {
+      var setter = this.view_.__lookupSetter__(this.viewProperty_);
+      if (!setter) {
+        var prop = this.viewProperty_;
+        setter = function(value) { this[prop] = value; }
+      }
+      if (this.listView_.selectedElement) {
+        setter.call(this.view_,
+                    this.listView_.selectedElement.item);
+      } else {
+        setter.call(this.view_,
+                    undefined);
+      }
+    }
+  };
+
+  return {
+    ListAndAssociatedView: ListAndAssociatedView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/list_and_associated_view_test.html b/trace-viewer/trace_viewer/base/ui/list_and_associated_view_test.html
new file mode 100644
index 0000000..be96f02
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/list_and_associated_view_test.html
@@ -0,0 +1,98 @@
+<!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/ui/list_and_associated_view.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ListAndAssociatedView = tv.b.ui.ListAndAssociatedView;
+
+  var SimpleView = tv.b.ui.define('div');
+  SimpleView.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function() {
+      this.item_ = undefined;
+    },
+
+    set item(item) {
+      this.item_ = item;
+    },
+    get item() {
+      return this.item_;
+    }
+  };
+
+  test('listViewNamingWithField', function() {
+    var lav = new ListAndAssociatedView();
+    var list = [
+      {x: '1'},
+      {x: '2'},
+      {x: '3'}
+    ];
+    var view = new SimpleView();
+
+    lav.list = list;
+    lav.listProperty = 'x';
+    lav.view = view;
+    lav.viewProperty = 'item';
+
+    var lavListView = lav.listView;
+    assert.equal(lavListView.children.length, 3);
+    assert.equal(lavListView.children[0].textContent, '1');
+  });
+
+  test('listViewNamingWithProperty', function() {
+    var lav = new ListAndAssociatedView();
+
+    function X(x) {
+      this.x = x;
+    }
+    X.prototype = {
+      get title() {
+        return this.x;
+      }
+    };
+
+    var list = [
+      new X('1'),
+      new X('2'),
+      new X('3')
+    ];
+    var view = new SimpleView();
+
+    lav.list = list;
+    lav.listProperty = 'title';
+    lav.view = view;
+    lav.viewProperty = 'item';
+
+    var lavListView = lav.listView;
+    assert.equal(lavListView.children.length, 3);
+    assert.equal(lavListView.children[0].textContent, '1');
+  });
+
+  test('selectionChangesView', function() {
+    var lav = new ListAndAssociatedView();
+    var list = [
+      {x: '1'},
+      {x: '2'},
+      {x: '3'}
+    ];
+    var view = new SimpleView();
+
+    lav.list = list;
+    lav.listProperty = 'x';
+    lav.view = view;
+    lav.viewProperty = 'item';
+    var lavListView = lav.listView;
+
+    assert.equal(list[0], view.item);
+    lavListView.children[1].selected = true;
+    assert.equal(list[1], view.item);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/list_view.css b/trace-viewer/trace_viewer/base/ui/list_view.css
new file mode 100644
index 0000000..7dc5f02
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/list_view.css
@@ -0,0 +1,30 @@
+/* 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-list-view {
+  -webkit-user-select: none;
+  display: block;
+}
+.x-list-view:focus {
+  outline: none;
+}
+
+.x-list-view * {
+  -webkit-user-select: none;
+}
+
+.x-list-view > .list-item {
+  padding: 2px 4px 2px 4px;
+}
+
+.x-list-view:focus > .list-item[selected] {
+  background-color: rgb(171, 217, 202);
+  outline: 1px dotted rgba(0,0,0,0.1);
+  outline-offset: 0;
+}
+
+.x-list-view > .list-item[selected] {
+  background-color: rgb(103, 199, 165);
+}
diff --git a/trace-viewer/trace_viewer/base/ui/list_view.html b/trace-viewer/trace_viewer/base/ui/list_view.html
new file mode 100644
index 0000000..738d11e
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/list_view.html
@@ -0,0 +1,164 @@
+<!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/events.html">
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/container_that_decorates_its_children.html">
+<link rel="stylesheet" href="/base/ui/list_view.css">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Simple list view.
+ */
+tv.exportTo('tv.b.ui', function() {
+  /**
+   * @constructor
+   */
+  var ListView = tv.b.ui.define(
+      'x-list-view', tv.b.ui.ContainerThatDecoratesItsChildren);
+
+  ListView.prototype = {
+    __proto__: tv.b.ui.ContainerThatDecoratesItsChildren.prototype,
+
+    decorate: function() {
+      tv.b.ui.ContainerThatDecoratesItsChildren.prototype.decorate.call(this);
+
+      this.classList.add('x-list-view');
+      this.onItemClicked_ = this.onItemClicked_.bind(this);
+      this.onKeyDown_ = this.onKeyDown_.bind(this);
+      this.tabIndex = 0;
+      this.addEventListener('keydown', this.onKeyDown_);
+
+      this.selectionChanged_ = false;
+    },
+
+    decorateChild_: function(item) {
+      item.classList.add('list-item');
+      item.addEventListener('click', this.onItemClicked_, true);
+
+      var listView = this;
+      Object.defineProperty(
+          item,
+          'selected', {
+            configurable: true,
+            set: function(value) {
+              var oldSelection = listView.selectedElement;
+              if (oldSelection && oldSelection != this && value)
+                listView.selectedElement.removeAttribute('selected');
+              if (value)
+                this.setAttribute('selected', 'selected');
+              else
+                this.removeAttribute('selected');
+              var newSelection = listView.selectedElement;
+              if (newSelection != oldSelection)
+                tv.b.dispatchSimpleEvent(listView, 'selection-changed', false);
+            },
+            get: function() {
+              return this.hasAttribute('selected');
+            }
+          });
+    },
+
+    undecorateChild_: function(item) {
+      this.selectionChanged_ |= item.selected;
+
+      item.classList.remove('list-item');
+      item.removeEventListener('click', this.onItemClicked_);
+      delete item.selected;
+    },
+
+    beginDecorating_: function() {
+      this.selectionChanged_ = false;
+    },
+
+    doneDecoratingForNow_: function() {
+      if (this.selectionChanged_)
+        tv.b.dispatchSimpleEvent(this, 'selection-changed', false);
+    },
+
+    get selectedElement() {
+      var el = this.querySelector('.list-item[selected]');
+      if (!el)
+        return undefined;
+      return el;
+    },
+
+    set selectedElement(el) {
+      if (!el) {
+        if (this.selectedElement)
+          this.selectedElement.selected = false;
+        return;
+      }
+
+      if (el.parentElement != this)
+        throw new Error(
+            'Can only select elements that are children of this list view');
+      el.selected = true;
+    },
+
+    getElementByIndex: function(index) {
+      return this.querySelector('.list-item:nth-child(' + index + ')');
+    },
+
+    clear: function() {
+      var changed = this.selectedElement !== undefined;
+      tv.b.ui.ContainerThatDecoratesItsChildren.prototype.clear.call(this);
+      if (changed)
+        tv.b.dispatchSimpleEvent(this, 'selection-changed', false);
+    },
+
+    onItemClicked_: function(e) {
+      var currentSelectedElement = this.selectedElement;
+      if (currentSelectedElement)
+        currentSelectedElement.removeAttribute('selected');
+      var element = e.target;
+      while (element.parentElement != this)
+        element = element.parentElement;
+      if (element !== currentSelectedElement)
+        element.setAttribute('selected', 'selected');
+      tv.b.dispatchSimpleEvent(this, 'selection-changed', false);
+    },
+
+    onKeyDown_: function(e) {
+      if (this.selectedElement === undefined)
+        return;
+
+      if (e.keyCode == 38) { // Up arrow.
+        var prev = this.selectedElement.previousSibling;
+        if (prev) {
+          prev.selected = true;
+          tv.b.scrollIntoViewIfNeeded(prev);
+          e.preventDefault();
+          return true;
+        }
+      } else if (e.keyCode == 40) { // Down arrow.
+        var next = this.selectedElement.nextSibling;
+        if (next) {
+          next.selected = true;
+          tv.b.scrollIntoViewIfNeeded(next);
+          e.preventDefault();
+          return true;
+        }
+      }
+    },
+
+    addItem: function(textContent) {
+      var item = document.createElement('div');
+      item.textContent = textContent;
+      this.appendChild(item);
+      return item;
+    }
+
+  };
+
+  return {
+    ListView: ListView
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/list_view_test.html b/trace-viewer/trace_viewer/base/ui/list_view_test.html
new file mode 100644
index 0000000..3978ad7
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/list_view_test.html
@@ -0,0 +1,67 @@
+<!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/ui/list_view.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var ListView = tv.b.ui.ListView;
+
+  test('instantiate', function() {
+    var view = new ListView();
+    var i1 = view.addItem('item 1');
+    var i2 = view.addItem('item 2');
+    var i3 = view.addItem('item 3');
+    this.addHTMLOutput(view);
+  });
+
+  test('programmaticSelection', function() {
+    var view = new ListView();
+    var i1 = view.addItem('item 1');
+    var i2 = view.addItem('item 2');
+    var i3 = view.addItem('item 3');
+
+    i2.selected = true;
+    assert.isTrue(i2.hasAttribute('selected'));
+    i3.selected = true;
+    assert.isFalse(i2.hasAttribute('selected'));
+    assert.isTrue(i3.hasAttribute('selected'));
+  });
+
+  test('clickSelection', function() {
+    var view = new ListView();
+    var didFireSelectionChange = false;
+    view.addEventListener('selection-changed', function() {
+      didFireSelectionChange = true;
+    });
+    var i1 = view.addItem('item 1');
+    var i2 = view.addItem('item 2');
+    var i3 = view.addItem('item 3');
+
+    didFireSelectionChange = false;
+    i2.click();
+    assert.isTrue(didFireSelectionChange);
+    assert.equal(view.selectedElement, i2);
+
+    didFireSelectionChange = false;
+    i3.click();
+    assert.isTrue(didFireSelectionChange);
+    assert.equal(view.selectedElement, i3);
+
+    // Click the same target again.
+    didFireSelectionChange = false;
+    i3.click();
+    assert.isTrue(didFireSelectionChange);
+    assert.isUndefined(view.selectedElement);
+
+    didFireSelectionChange = false;
+    i1.click();
+    assert.isTrue(didFireSelectionChange);
+    assert.equal(view.selectedElement, i1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/mouse_mode_selector.css b/trace-viewer/trace_viewer/base/ui/mouse_mode_selector.css
new file mode 100644
index 0000000..793b525
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/mouse_mode_selector.css
@@ -0,0 +1,82 @@
+/* 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.
+ */
+
+.mouse-mode-selector {
+
+  -webkit-user-drag: element;
+  -webkit-user-select: none;
+
+  background: #DDD;
+  border: 1px solid #BBB;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
+  left: calc(100% - 120px);
+  position: absolute;
+  top: 100px;
+  user-select: none;
+  width: 29px;
+  z-index: 20;
+}
+
+.mouse-mode-icon {
+  background-image: url(../images/ui-states.png);
+}
+
+.mouse-mode-selector .drag-handle {
+  background: url(../images/ui-states.png) 2px 3px no-repeat;
+  background-repeat: no-repeat;
+  border-bottom: 1px solid #BCBCBC;
+  cursor: move;
+  display: block;
+  height: 13px;
+  width: 27px;
+}
+
+.mouse-mode-selector .pan-scan-mode-button {
+  background-image: url(../images/ui-states.png);
+  background-position: 0 -10px;
+}
+
+.mouse-mode-selector .pan-scan-mode-button.active {
+  background-position: -30px -10px;
+}
+
+.mouse-mode-selector .selection-mode-button {
+  background-image: url(../images/ui-states.png);
+  background-position: 0 -40px;
+}
+
+.mouse-mode-selector .selection-mode-button.active {
+  background-position: -30px -40px;
+}
+
+.mouse-mode-selector .zoom-mode-button {
+  background-image: url(../images/ui-states.png);
+  background-position: 0 -70px;
+}
+
+.mouse-mode-selector .zoom-mode-button.active {
+  background-position: -30px -70px;
+}
+
+.mouse-mode-selector .timing-mode-button {
+  background-image: url(../images/ui-states.png);
+  background-position: 0 -100px;
+  border-bottom: none;
+}
+
+.mouse-mode-selector .timing-mode-button.active {
+  background-position: -30px -100px;
+}
+
+.mouse-mode-selector .rotate-mode-button {
+  background-image: url(../images/ui-states.png);
+  background-position: 0 -130px;
+  border-bottom: none;
+}
+
+.mouse-mode-selector .rotate-mode-button.active {
+  background-position: -30px -130px;
+}
diff --git a/trace-viewer/trace_viewer/base/ui/mouse_mode_selector.html b/trace-viewer/trace_viewer/base/ui/mouse_mode_selector.html
new file mode 100644
index 0000000..03b3b5f
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/mouse_mode_selector.html
@@ -0,0 +1,586 @@
+<!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/events.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/key_event_manager.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/mouse_tracker.html">
+
+<link rel="stylesheet" href="/base/ui/mouse_mode_selector.css">
+<link rel="stylesheet" href="/base/ui/tool_button.css">
+
+<template id="mouse-mode-selector-template">
+  <div class="drag-handle"></div>
+  <div class="buttons">
+  </div>
+</template>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  var MIN_MOUSE_SELECTION_DISTANCE = 4;
+
+  var MOUSE_SELECTOR_MODE = {};
+  MOUSE_SELECTOR_MODE.SELECTION = 0x1;
+  MOUSE_SELECTOR_MODE.PANSCAN = 0x2;
+  MOUSE_SELECTOR_MODE.ZOOM = 0x4;
+  MOUSE_SELECTOR_MODE.TIMING = 0x8;
+  MOUSE_SELECTOR_MODE.ROTATE = 0x10;
+  MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F;
+
+  var allModeInfo = {};
+  allModeInfo[MOUSE_SELECTOR_MODE.PANSCAN] = {
+    title: 'pan',
+    className: 'pan-scan-mode-button',
+    eventNames: {
+      enter: 'enterpan',
+      begin: 'beginpan',
+      update: 'updatepan',
+      end: 'endpan',
+      exit: 'exitpan'
+    }
+  };
+  allModeInfo[MOUSE_SELECTOR_MODE.SELECTION] = {
+    title: 'selection',
+    className: 'selection-mode-button',
+    eventNames: {
+      enter: 'enterselection',
+      begin: 'beginselection',
+      update: 'updateselection',
+      end: 'endselection',
+      exit: 'exitselection'
+    }
+  };
+
+  allModeInfo[MOUSE_SELECTOR_MODE.ZOOM] = {
+    title: 'zoom',
+    className: 'zoom-mode-button',
+    eventNames: {
+      enter: 'enterzoom',
+      begin: 'beginzoom',
+      update: 'updatezoom',
+      end: 'endzoom',
+      exit: 'exitzoom'
+    }
+  };
+  allModeInfo[MOUSE_SELECTOR_MODE.TIMING] = {
+    title: 'timing',
+    className: 'timing-mode-button',
+    eventNames: {
+      enter: 'entertiming',
+      begin: 'begintiming',
+      update: 'updatetiming',
+      end: 'endtiming',
+      exit: 'exittiming'
+    }
+  };
+  allModeInfo[MOUSE_SELECTOR_MODE.ROTATE] = {
+    title: 'rotate',
+    className: 'rotate-mode-button',
+    eventNames: {
+      enter: 'enterrotate',
+      begin: 'beginrotate',
+      update: 'updaterotate',
+      end: 'endrotate',
+      exit: 'exitrotate'
+    }
+  };
+
+  var MODIFIER = {
+    SHIFT: 0x1,
+    SPACE: 0x2,
+    CMD_OR_CTRL: 0x4
+  };
+
+  /**
+   * Provides a panel for switching the interaction mode of the mouse.
+   * It handles the user interaction and dispatches events for the various
+   * modes.
+   *
+   * @constructor
+   * @extends {HTMLDivElement}
+   */
+  var MouseModeSelector = tv.b.ui.define('div');
+
+  MouseModeSelector.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function(opt_targetElement) {
+      this.classList.add('mouse-mode-selector');
+
+      var node = tv.b.instantiateTemplate('#mouse-mode-selector-template',
+                                          THIS_DOC);
+      this.appendChild(node);
+
+      this.buttonsEl_ = this.querySelector('.buttons');
+      this.dragHandleEl_ = this.querySelector('.drag-handle');
+
+      this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES;
+
+      this.initialRelativeMouseDownPos_ = {x: 0, y: 0};
+
+      this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN;
+      this.settingsKey_ = undefined;
+      this.mousePos_ = {x: 0, y: 0};
+      this.mouseDownPos_ = {x: 0, y: 0};
+
+      this.dragHandleEl_.addEventListener('mousedown',
+          this.onDragHandleMouseDown_.bind(this));
+
+      this.onMouseDown_ = this.onMouseDown_.bind(this);
+      this.onMouseMove_ = this.onMouseMove_.bind(this);
+      this.onMouseUp_ = this.onMouseUp_.bind(this);
+
+      this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_);
+      this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_);
+      this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this));
+
+      tv.b.KeyEventManager.instance.addListener(
+          'keydown', this.onKeyDown_, this);
+      tv.b.KeyEventManager.instance.addListener(
+          'keyup', this.onKeyUp_, this);
+      this.keyCodeCondition = undefined;
+
+      this.mode_ = undefined;
+      this.modeToKeyCodeMap_ = {};
+      this.modifierToModeMap_ = {};
+
+      this.targetElement = opt_targetElement;
+      this.spacePressed_ = false;
+      this.modeBeforeAlternativeModeActivated_ = null;
+
+      this.isInteracting_ = false;
+      this.isClick_ = false;
+    },
+
+    get targetElement() {
+      return this.targetElement_;
+    },
+
+    set targetElement(target) {
+      if (this.targetElement_)
+        this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
+      this.targetElement_ = target;
+      if (this.targetElement_)
+        this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
+    },
+
+    get defaultMode() {
+      return this.defaultMode_;
+    },
+
+    set defaultMode(defaultMode) {
+      this.defaultMode_ = defaultMode;
+    },
+
+    get settingsKey() {
+      return this.settingsKey_;
+    },
+
+    set settingsKey(settingsKey) {
+      this.settingsKey_ = settingsKey;
+      if (!this.settingsKey_)
+        return;
+
+      var mode = tv.b.Settings.get(this.settingsKey_ + '.mode', undefined);
+      // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray
+      // settings to the best of our abilities.
+      if (allModeInfo[mode] === undefined)
+        mode = undefined;
+
+      // Restoring settings against unsupported modes should just go back to the
+      // default mode.
+      if ((mode & this.supportedModeMask_) === 0)
+        mode = undefined;
+
+      if (!mode)
+        mode = this.defaultMode_;
+      this.mode = mode;
+
+      var pos = tv.b.Settings.get(this.settingsKey_ + '.pos', undefined);
+      if (pos)
+        this.pos = pos;
+    },
+
+    get supportedModeMask() {
+      return this.supportedModeMask_;
+    },
+
+    /**
+     * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE
+     * values.
+     */
+    set supportedModeMask(supportedModeMask) {
+      if (this.mode && (supportedModeMask & this.mode) === 0)
+        throw new Error('supportedModeMask must include current mode.');
+
+      function createButtonForMode(mode) {
+        var button = document.createElement('div');
+        button.mode = mode;
+        button.title = allModeInfo[mode].title;
+        button.classList.add('tool-button');
+        button.classList.add(allModeInfo[mode].className);
+        return button;
+      }
+
+      this.supportedModeMask_ = supportedModeMask;
+      this.buttonsEl_.textContent = '';
+      for (var modeName in MOUSE_SELECTOR_MODE) {
+        if (modeName == 'ALL_MODES')
+          continue;
+        var mode = MOUSE_SELECTOR_MODE[modeName];
+        if ((this.supportedModeMask_ & mode) === 0)
+          continue;
+        this.buttonsEl_.appendChild(createButtonForMode(mode));
+      }
+    },
+
+    get mode() {
+      return this.currentMode_;
+    },
+
+    set mode(newMode) {
+      if (newMode !== undefined) {
+        if (typeof newMode !== 'number')
+          throw new Error('Mode must be a number');
+        if ((newMode & this.supportedModeMask_) === 0)
+          throw new Error('Cannot switch to this mode, it is not supported');
+        if (allModeInfo[newMode] === undefined)
+          throw new Error('Unrecognized mode');
+      }
+
+      var modeInfo;
+
+      if (this.currentMode_ === newMode)
+        return;
+
+      if (this.currentMode_) {
+        modeInfo = allModeInfo[this.currentMode_];
+        var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className);
+        if (buttonEl)
+          buttonEl.classList.remove('active');
+
+        // End event.
+        if (this.isInteracting_) {
+
+          var mouseEvent = this.createEvent_(
+              allModeInfo[this.mode].eventNames.end);
+          this.dispatchEvent(mouseEvent);
+        }
+
+        // Exit event.
+        tv.b.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true);
+      }
+
+      this.currentMode_ = newMode;
+
+      if (this.currentMode_) {
+        modeInfo = allModeInfo[this.currentMode_];
+        var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className);
+        if (buttonEl)
+          buttonEl.classList.add('active');
+
+        // Entering a new mode resets mouse down pos.
+        this.mouseDownPos_.x = this.mousePos_.x;
+        this.mouseDownPos_.y = this.mousePos_.y;
+
+        // Enter event.
+        if (!this.isInAlternativeMode_)
+          tv.b.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true);
+
+        // Begin event.
+        if (this.isInteracting_) {
+          var mouseEvent = this.createEvent_(
+              allModeInfo[this.mode].eventNames.begin);
+          this.dispatchEvent(mouseEvent);
+        }
+
+
+      }
+
+      if (this.settingsKey_ && !this.isInAlternativeMode_)
+        tv.b.Settings.set(this.settingsKey_ + '.mode', this.mode);
+    },
+
+    setKeyCodeForMode: function(mode, keyCode) {
+      if ((mode & this.supportedModeMask_) === 0)
+        throw new Error('Mode not supported');
+      this.modeToKeyCodeMap_[mode] = keyCode;
+
+      if (!this.buttonsEl_)
+        return;
+
+      var modeInfo = allModeInfo[mode];
+      var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className);
+      if (buttonEl) {
+        buttonEl.title =
+            modeInfo.title + ' (' + String.fromCharCode(keyCode) + ')';
+      }
+    },
+
+    setKeyCodeCondition: function(callback) {
+      this.keyCodeCondition = callback;
+    },
+
+    setCurrentMousePosFromEvent_: function(e) {
+      this.mousePos_.x = e.clientX;
+      this.mousePos_.y = e.clientY;
+    },
+
+    createEvent_: function(eventName, sourceEvent) {
+      var event = new tv.b.Event(eventName, true);
+      event.clientX = this.mousePos_.x;
+      event.clientY = this.mousePos_.y;
+      event.deltaX = this.mousePos_.x - this.mouseDownPos_.x;
+      event.deltaY = this.mousePos_.y - this.mouseDownPos_.y;
+      event.mouseDownX = this.mouseDownPos_.x;
+      event.mouseDownY = this.mouseDownPos_.y;
+      event.didPreventDefault = false;
+      event.preventDefault = function() {
+        event.didPreventDefault = true;
+        if (sourceEvent)
+          sourceEvent.preventDefault();
+      };
+      event.stopPropagation = function() {
+        sourceEvent.stopPropagation();
+      };
+      event.stopImmediatePropagation = function() {
+        throw new Error('Not implemented');
+      };
+      return event;
+    },
+
+    onMouseDown_: function(e) {
+      if (e.button !== 0)
+        return;
+      this.setCurrentMousePosFromEvent_(e);
+      var mouseEvent = this.createEvent_(
+          allModeInfo[this.mode].eventNames.begin, e);
+      this.dispatchEvent(mouseEvent);
+      this.isInteracting_ = true;
+      this.isClick_ = true;
+      tv.b.ui.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_);
+    },
+
+    onMouseMove_: function(e) {
+      this.setCurrentMousePosFromEvent_(e);
+
+      var mouseEvent = this.createEvent_(
+          allModeInfo[this.mode].eventNames.update, e);
+      this.dispatchEvent(mouseEvent);
+
+      if (this.isInteracting_)
+        this.checkIsClick_(e);
+    },
+
+    onMouseUp_: function(e) {
+      if (e.button !== 0)
+        return;
+
+      var mouseEvent = this.createEvent_(
+          allModeInfo[this.mode].eventNames.end, e);
+      mouseEvent.isClick = this.isClick_;
+      this.dispatchEvent(mouseEvent);
+
+      if (this.isClick_ && !mouseEvent.didPreventDefault)
+        this.dispatchClickEvents_(e);
+
+      this.isInteracting_ = false;
+      this.updateAlternativeModeState_(e);
+    },
+
+    onButtonMouseDown_: function(e) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+    },
+
+    onButtonMouseUp_: function(e) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+    },
+
+    onButtonPress_: function(e) {
+      this.modeBeforeAlternativeModeActivated_ = undefined;
+      this.mode = e.target.mode;
+      e.preventDefault();
+    },
+
+    onKeyDown_: function(e) {
+      if (e.keyCode === ' '.charCodeAt(0))
+        this.spacePressed_ = true;
+      this.updateAlternativeModeState_(e);
+    },
+
+    onKeyUp_: function(e) {
+      if (e.keyCode === ' '.charCodeAt(0))
+        this.spacePressed_ = false;
+
+      if (this.keyCodeCondition != undefined && !this.keyCodeCondition()) {
+        // If keyCodeCondition is false when the FindControl is active,
+        // ignore the keyUp event.
+        return;
+      }
+
+      var didHandleKey = false;
+      tv.b.iterItems(this.modeToKeyCodeMap_, function(modeStr, keyCode) {
+        if (e.keyCode === keyCode) {
+          this.modeBeforeAlternativeModeActivated_ = undefined;
+          var mode = parseInt(modeStr);
+          this.mode = mode;
+          didHandleKey = true;
+        }
+      }, this);
+
+      if (didHandleKey) {
+        e.preventDefault();
+        e.stopPropagation();
+        return;
+      }
+      this.updateAlternativeModeState_(e);
+    },
+
+    updateAlternativeModeState_: function(e) {
+      var shiftPressed = e.shiftKey;
+      var spacePressed = this.spacePressed_;
+      var cmdOrCtrlPressed =
+          (tv.isMac && e.metaKey) || (!tv.isMac && e.ctrlKey);
+
+      // Figure out the new mode
+      var smm = this.supportedModeMask_;
+      var newMode;
+      var isNewModeAnAlternativeMode = false;
+      if (shiftPressed &&
+          (this.modifierToModeMap_[MODIFIER.SHIFT] & smm) !== 0) {
+        newMode = this.modifierToModeMap_[MODIFIER.SHIFT];
+        isNewModeAnAlternativeMode = true;
+      } else if (spacePressed &&
+                 (this.modifierToModeMap_[MODIFIER.SPACE] & smm) !== 0) {
+        newMode = this.modifierToModeMap_[MODIFIER.SPACE];
+        isNewModeAnAlternativeMode = true;
+      } else if (cmdOrCtrlPressed &&
+                 (this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL] & smm) !== 0) {
+        newMode = this.modifierToModeMap_[MODIFIER.CMD_OR_CTRL];
+        isNewModeAnAlternativeMode = true;
+      } else {
+        // Go to the old mode, if there is one.
+        if (this.isInAlternativeMode_) {
+          newMode = this.modeBeforeAlternativeModeActivated_;
+          isNewModeAnAlternativeMode = false;
+        } else {
+          newMode = undefined;
+        }
+      }
+
+      // Maybe a mode change isn't needed.
+      if (this.mode === newMode || newMode === undefined)
+        return;
+
+      // Okay, we're changing.
+      if (isNewModeAnAlternativeMode)
+        this.modeBeforeAlternativeModeActivated_ = this.mode;
+      this.mode = newMode;
+    },
+
+    get isInAlternativeMode_() {
+      return !!this.modeBeforeAlternativeModeActivated_;
+    },
+
+    setModifierForAlternateMode: function(mode, modifier) {
+      this.modifierToModeMap_[modifier] = mode;
+    },
+
+    get pos() {
+      return {
+        x: parseInt(this.style.left),
+        y: parseInt(this.style.top)
+      };
+    },
+
+    set pos(pos) {
+      pos = this.constrainPositionToBounds_(pos);
+
+      this.style.left = pos.x + 'px';
+      this.style.top = pos.y + 'px';
+
+      if (this.settingsKey_)
+        tv.b.Settings.set(this.settingsKey_ + '.pos', this.pos);
+    },
+
+    constrainPositionToBounds_: function(pos) {
+      var parent = this.offsetParent || document.body;
+      var parentRect = tv.b.windowRectForElement(parent);
+
+      var top = 0;
+      var bottom = parentRect.height - this.offsetHeight;
+      var left = 0;
+      var right = parentRect.width - this.offsetWidth;
+
+      var res = {};
+      res.x = Math.max(pos.x, left);
+      res.x = Math.min(res.x, right);
+
+      res.y = Math.max(pos.y, top);
+      res.y = Math.min(res.y, bottom);
+      return res;
+    },
+
+    onDragHandleMouseDown_: function(e) {
+      e.preventDefault();
+      e.stopImmediatePropagation();
+
+      var mouseDownPos = {
+        x: e.clientX - this.offsetLeft,
+        y: e.clientY - this.offsetTop
+      };
+      tv.b.ui.trackMouseMovesUntilMouseUp(function(e) {
+        var pos = {};
+        pos.x = e.clientX - mouseDownPos.x;
+        pos.y = e.clientY - mouseDownPos.y;
+        this.pos = pos;
+      }.bind(this));
+    },
+
+    checkIsClick_: function(e) {
+      if (!this.isInteracting_ || !this.isClick_)
+        return;
+
+      var deltaX = this.mousePos_.x - this.mouseDownPos_.x;
+      var deltaY = this.mousePos_.y - this.mouseDownPos_.y;
+      var minDist = MIN_MOUSE_SELECTION_DISTANCE;
+
+      if (deltaX * deltaX + deltaY * deltaY > minDist * minDist)
+        this.isClick_ = false;
+    },
+
+    dispatchClickEvents_: function(e) {
+      if (!this.isClick_)
+        return;
+
+      var eventNames = allModeInfo[MOUSE_SELECTOR_MODE.SELECTION].eventNames;
+
+      var mouseEvent = this.createEvent_(eventNames.begin);
+      this.dispatchEvent(mouseEvent);
+
+      mouseEvent = this.createEvent_(eventNames.end);
+      this.dispatchEvent(mouseEvent);
+    }
+  };
+
+  return {
+    MIN_MOUSE_SELECTION_DISTANCE: MIN_MOUSE_SELECTION_DISTANCE,
+    MouseModeSelector: MouseModeSelector,
+    MOUSE_SELECTOR_MODE: MOUSE_SELECTOR_MODE,
+    MODIFIER: MODIFIER
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/mouse_mode_selector_test.html b/trace-viewer/trace_viewer/base/ui/mouse_mode_selector_test.html
new file mode 100644
index 0000000..f54332d
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/mouse_mode_selector_test.html
@@ -0,0 +1,43 @@
+<!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/ui/mouse_mode_selector.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var MOUSE_SELECTOR_MODE = tv.b.ui.MOUSE_SELECTOR_MODE;
+  test('instantiate', function() {
+    var sel = new tv.b.ui.MouseModeSelector();
+    sel.supportedModeMask =
+        MOUSE_SELECTOR_MODE.SELECTION |
+        MOUSE_SELECTOR_MODE.PANSCAN;
+    this.addHTMLOutput(sel);
+  });
+
+  test('changeMaskWithUnsupportedMode', function() {
+    var sel = new tv.b.ui.MouseModeSelector();
+    sel.mode = MOUSE_SELECTOR_MODE.SELECTION;
+    assert.throw(function() {
+      sel.supportedModeMask = MOUSE_SELECTOR_MODE.ZOOM;
+    });
+  });
+
+  test('modePersists', function() {
+    var sel1 = new tv.b.ui.MouseModeSelector();
+    sel1.defaultMode_ = MOUSE_SELECTOR_MODE.ZOOM;
+    sel1.settingsKey = 'foo';
+    assert.equal(sel1.mode, MOUSE_SELECTOR_MODE.ZOOM);
+
+    sel1.mode = MOUSE_SELECTOR_MODE.PANSCAN;
+
+    var sel2 = new tv.b.ui.MouseModeSelector();
+    sel2.settingsKey = 'foo';
+    assert.equal(sel2.mode, MOUSE_SELECTOR_MODE.PANSCAN);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/mouse_tracker.html b/trace-viewer/trace_viewer/base/ui/mouse_tracker.html
new file mode 100644
index 0000000..8d84e38
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/mouse_tracker.html
@@ -0,0 +1,109 @@
+<!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';
+
+/**
+ * @fileoverview A Mouse-event abtraction that waits for
+ *   mousedown, then watches for subsequent mousemove events
+ *   until the next mouseup event, then waits again.
+ *   State changes are signaled with
+ *      'mouse-tracker-start' : mousedown and tracking
+ *      'mouse-tracker-move' : mouse move
+ *      'mouse-tracker-end' : mouseup and not tracking.
+ */
+
+tv.exportTo('tv.b.ui', function() {
+
+  /**
+   * @constructor
+   * @param {HTMLElement} targetElement will recv events 'mouse-tracker-start',
+   *     'mouse-tracker-move', 'mouse-tracker-end'.
+   */
+  function MouseTracker(opt_targetElement) {
+    this.onMouseDown_ = this.onMouseDown_.bind(this);
+    this.onMouseMove_ = this.onMouseMove_.bind(this);
+    this.onMouseUp_ = this.onMouseUp_.bind(this);
+
+    this.targetElement = opt_targetElement;
+  }
+
+  MouseTracker.prototype = {
+
+    get targetElement() {
+      return this.targetElement_;
+    },
+
+    set targetElement(targetElement) {
+      if (this.targetElement_)
+        this.targetElement_.removeEventListener('mousedown', this.onMouseDown_);
+      this.targetElement_ = targetElement;
+      if (this.targetElement_)
+        this.targetElement_.addEventListener('mousedown', this.onMouseDown_);
+    },
+
+    onMouseDown_: function(e) {
+      if (e.button !== 0)
+        return true;
+
+      e = this.remakeEvent_(e, 'mouse-tracker-start');
+      this.targetElement_.dispatchEvent(e);
+      document.addEventListener('mousemove', this.onMouseMove_);
+      document.addEventListener('mouseup', this.onMouseUp_);
+      this.targetElement_.addEventListener('blur', this.onMouseUp_);
+      this.savePreviousUserSelect_ = document.body.style['-webkit-user-select'];
+      document.body.style['-webkit-user-select'] = 'none';
+      e.preventDefault();
+      return true;
+    },
+
+    onMouseMove_: function(e) {
+      e = this.remakeEvent_(e, 'mouse-tracker-move');
+      this.targetElement_.dispatchEvent(e);
+    },
+
+    onMouseUp_: function(e) {
+      document.removeEventListener('mousemove', this.onMouseMove_);
+      document.removeEventListener('mouseup', this.onMouseUp_);
+      this.targetElement_.removeEventListener('blur', this.onMouseUp_);
+      document.body.style['-webkit-user-select'] =
+          this.savePreviousUserSelect_;
+      e = this.remakeEvent_(e, 'mouse-tracker-end');
+      this.targetElement_.dispatchEvent(e);
+    },
+
+    remakeEvent_: function(e, newType) {
+      var remade = new tv.b.Event(newType, true, true);
+      remade.x = e.x;
+      remade.y = e.y;
+      remade.offsetX = e.offsetX;
+      remade.offsetY = e.offsetY;
+      remade.clientX = e.clientX;
+      remade.clientY = e.clientY;
+      return remade;
+    }
+
+  };
+
+  function trackMouseMovesUntilMouseUp(mouseMoveHandler, opt_mouseUpHandler) {
+    function cleanupAndDispatchToMouseUp(e) {
+      document.removeEventListener('mousemove', mouseMoveHandler);
+      document.removeEventListener('mouseup', cleanupAndDispatchToMouseUp);
+      if (opt_mouseUpHandler)
+        opt_mouseUpHandler(e);
+    }
+    document.addEventListener('mousemove', mouseMoveHandler);
+    document.addEventListener('mouseup', cleanupAndDispatchToMouseUp);
+  }
+
+  return {
+    MouseTracker: MouseTracker,
+    trackMouseMovesUntilMouseUp: trackMouseMovesUntilMouseUp
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/overlay.html b/trace-viewer/trace_viewer/base/ui/overlay.html
new file mode 100644
index 0000000..5179783
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/overlay.html
@@ -0,0 +1,333 @@
+<!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/utils.html">
+<link rel="import" href="/base/properties.html">
+<link rel="import" href="/base/events.html">
+<link rel="import" href="/base/ui.html">
+
+<template id="overlay-template">
+  <style>
+    overlay-mask {
+      left: 0;
+      padding: 8px;
+      position: absolute;
+      top: 0;
+      z-index: 1000;
+      font-family: sans-serif;
+      -webkit-justify-content: center;
+      background: rgba(0, 0, 0, 0.8);
+      display: -webkit-flex;
+      height: 100%;
+      left: 0;
+      position: fixed;
+      top: 0;
+      width: 100%;
+    }
+    overlay-mask:focus {
+      outline: none;
+    }
+    overlay-vertical-centering-container {
+      -webkit-justify-content: center;
+      -webkit-flex-direction: column;
+      display: -webkit-flex;
+    }
+    overlay-frame {
+      z-index: 1100;
+      background: rgb(255, 255, 255);
+      border: 1px solid #ccc;
+      margin: 75px;
+      display: -webkit-flex;
+      -webkit-flex-direction: column;
+    }
+    title-bar {
+      -webkit-align-items: center;
+      -webkit-flex-direction: row;
+      border-bottom: 1px solid #ccc;
+      background-color: #ddd;
+      display: -webkit-flex;
+      padding: 5px;
+      -webkit-flex: 0 0 auto;
+    }
+    title {
+      display: inline;
+      font-weight: bold;
+      -webkit-box-flex: 1;
+      -webkit-flex: 1 1 auto;
+    }
+    close-button {
+      -webkit-align-self: flex-end;
+      border: 1px solid #eee;
+      background-color: #999;
+      font-size: 10pt;
+      font-weight: bold;
+      padding: 2px;
+      text-align: center;
+      width: 16px;
+    }
+    close-button:hover {
+      background-color: #ddd;
+      border-color: black;
+      cursor: pointer;
+    }
+    overlay-content {
+      display: -webkit-flex;
+      -webkit-flex: 1 1 auto;
+      -webkit-flex-direction: column;
+      overflow-y: auto;
+      padding: 10px;
+      min-width: 300px;
+    }
+    button-bar {
+      -webkit-align-items: baseline;
+      border-top: 1px solid #ccc;
+      display: -webkit-flex;
+      -webkit-flex: 0 0 auto;
+      -webkit-flex-direction: row-reverse;
+      padding: 4px;
+    }
+  </style>
+
+  <overlay-mask>
+    <overlay-vertical-centering-container>
+      <overlay-frame>
+        <title-bar>
+          <title></title>
+          <close-button>&#x2715</close-button>
+        </title-bar>
+        <overlay-content>
+          <content></content>
+        </overlay-content>
+        <button-bar></button-bar>
+      </overlay-frame>
+    </overlay-vertical-centering-container>
+  </overlay-mask>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Implements an element that is hidden by default, but
+ * when shown, dims and (attempts to) disable the main document.
+ *
+ * You can turn any div into an overlay. Note that while an
+ * overlay element is shown, its parent is changed. Hiding the overlay
+ * restores its original parentage.
+ *
+ */
+tv.exportTo('tv.b.ui', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  /**
+   * Creates a new overlay element. It will not be visible until shown.
+   * @constructor
+   * @extends {HTMLDivElement}
+   */
+  var Overlay = tv.b.ui.define('overlay');
+
+  Overlay.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    /**
+     * Initializes the overlay element.
+     */
+    decorate: function() {
+      this.classList.add('overlay');
+
+      this.parentEl_ = this.ownerDocument.body;
+
+      this.visible_ = false;
+      this.userCanClose_ = true;
+
+      this.onKeyDown_ = this.onKeyDown_.bind(this);
+      this.onClick_ = this.onClick_.bind(this);
+      this.onFocusIn_ = this.onFocusIn_.bind(this);
+      this.onDocumentClick_ = this.onDocumentClick_.bind(this);
+      this.onClose_ = this.onClose_.bind(this);
+
+      this.addEventListener('visibleChange',
+          tv.b.ui.Overlay.prototype.onVisibleChange_.bind(this), true);
+
+      // Setup the shadow root
+      var createShadowRoot = this.createShadowRoot ||
+          this.webkitCreateShadowRoot;
+      this.shadow_ = createShadowRoot.call(this);
+      this.shadow_.appendChild(tv.b.instantiateTemplate('#overlay-template',
+                                                        THIS_DOC));
+
+      this.closeBtn_ = this.shadow_.querySelector('close-button');
+      this.closeBtn_.addEventListener('click', this.onClose_);
+
+      this.shadow_
+          .querySelector('overlay-frame')
+          .addEventListener('click', this.onClick_);
+
+      this.observer_ = new WebKitMutationObserver(
+          this.didButtonBarMutate_.bind(this));
+      this.observer_.observe(this.shadow_.querySelector('button-bar'),
+                             { childList: true });
+
+      // title is a variable on regular HTMLElements. However, we want to
+      // use it for something more useful.
+      Object.defineProperty(
+          this, 'title', {
+            get: function() {
+              return this.shadow_.querySelector('title').textContent;
+            },
+            set: function(title) {
+              this.shadow_.querySelector('title').textContent = title;
+            }
+          });
+    },
+
+    set userCanClose(userCanClose) {
+      this.userCanClose_ = userCanClose;
+      this.closeBtn_.style.display =
+          userCanClose ? 'block' : 'none';
+    },
+
+    get buttons() {
+      return this.shadow_.querySelector('button-bar');
+    },
+
+    get visible() {
+      return this.visible_;
+    },
+
+    set visible(newValue) {
+      if (this.visible_ === newValue)
+        return;
+
+      tv.b.setPropertyAndDispatchChange(this, 'visible', newValue);
+    },
+
+    onVisibleChange_: function() {
+      this.visible_ ? this.show_() : this.hide_();
+    },
+
+    show_: function() {
+      this.parentEl_.appendChild(this);
+
+      if (this.userCanClose_) {
+        this.addEventListener('keydown', this.onKeyDown_.bind(this));
+        this.addEventListener('click', this.onDocumentClick_.bind(this));
+      }
+
+      this.parentEl_.addEventListener('focusin', this.onFocusIn_);
+      this.tabIndex = 0;
+
+      // Focus the first thing we find that makes sense. (Skip the close button
+      // as it doesn't make sense as the first thing to focus.)
+      var focusEl = undefined;
+      var elList = this.querySelectorAll('button, input, list, select, a');
+      if (elList.length > 0) {
+        if (elList[0] === this.closeBtn_) {
+          if (elList.length > 1)
+            focusEl = elList[1];
+        } else {
+          focusEl = elList[0];
+        }
+      }
+      if (focusEl === undefined)
+        focusEl = this;
+      focusEl.focus();
+    },
+
+    hide_: function() {
+      this.parentEl_.removeChild(this);
+
+      this.parentEl_.removeEventListener('focusin', this.onFocusIn_);
+
+      if (this.closeBtn_)
+        this.closeBtn_.removeEventListener(this.onClose_);
+
+      document.removeEventListener('keydown', this.onKeyDown_);
+      document.removeEventListener('click', this.onDocumentClick_);
+    },
+
+    onClose_: function(e) {
+      this.visible = false;
+      if ((e.type != 'keydown') ||
+          (e.type === 'keydown' && e.keyCode === 27))
+        e.stopPropagation();
+      e.preventDefault();
+      tv.b.dispatchSimpleEvent(this, 'closeclick');
+    },
+
+    onFocusIn_: function(e) {
+      if (e.target === this)
+        return;
+
+      window.setTimeout(function() { this.focus(); }, 0);
+      e.preventDefault();
+      e.stopPropagation();
+    },
+
+    didButtonBarMutate_: function(e) {
+      var hasButtons = this.buttons.children.length > 0;
+      if (hasButtons)
+        this.shadow_.querySelector('button-bar').style.display = undefined;
+      else
+        this.shadow_.querySelector('button-bar').style.display = 'none';
+    },
+
+    onKeyDown_: function(e) {
+      // Disallow shift-tab back to another element.
+      if (e.keyCode === 9 &&  // tab
+          e.shiftKey &&
+          e.target === this) {
+        e.preventDefault();
+        return;
+      }
+
+      if (e.keyCode !== 27)  // escape
+        return;
+
+      this.onClose_(e);
+    },
+
+    onClick_: function(e) {
+      e.stopPropagation();
+    },
+
+    onDocumentClick_: function(e) {
+      if (!this.userCanClose_)
+        return;
+
+      this.onClose_(e);
+    }
+  };
+
+  Overlay.showError = function(msg, opt_err) {
+    var o = new Overlay();
+    o.title = 'Error';
+    o.textContent = msg;
+    if (opt_err) {
+      var e = tv.b.normalizeException(opt_err);
+
+      var stackDiv = document.createElement('pre');
+      stackDiv.textContent = e.stack;
+      stackDiv.style.paddingLeft = '8px';
+      stackDiv.style.margin = 0;
+      o.appendChild(stackDiv);
+    }
+    var b = document.createElement('button');
+    b.textContent = 'OK';
+    b.addEventListener('click', function() {
+      o.visible = false;
+    });
+    o.buttons.appendChild(b);
+    o.visible = true;
+    return o;
+  }
+
+  return {
+    Overlay: Overlay
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/overlay_test.html b/trace-viewer/trace_viewer/base/ui/overlay_test.html
new file mode 100644
index 0000000..4065d78
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/overlay_test.html
@@ -0,0 +1,124 @@
+<!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/ui/overlay.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function addShowButtonForDialog(dlg) {
+    var btn = document.createElement('button');
+    btn.textContent = 'Launch Overlay';
+    btn.addEventListener('click', function(e) {
+      dlg.visible = true;
+      e.stopPropagation();
+    });
+
+    this.addHTMLOutput(btn);
+  }
+
+  function makeButton(title) {
+    var btn = document.createElement('button');
+    btn.textContent = title;
+    return btn;
+  }
+
+  function makeCloseButton(dlg) {
+    var btn = makeButton('close');
+    btn.addEventListener('click', function(e) {
+      dlg.onClose_(e);
+    });
+    return btn;
+  }
+
+  test('instantiate', function() {
+    var dlg = new tv.b.ui.Overlay();
+    dlg.classList.add('example-overlay');
+    dlg.title = 'ExampleOverlay';
+    dlg.innerHTML = 'hello';
+    dlg.buttons.appendChild(makeButton('i am a button'));
+    dlg.buttons.appendChild(makeCloseButton(dlg));
+    dlg.buttons.appendChild(tv.b.ui.createSpan(
+        {textContent: 'i am a span'}));
+    addShowButtonForDialog.call(this, dlg);
+  });
+
+  test('instantiate_noButtons', function() {
+    var dlg = new tv.b.ui.Overlay();
+    dlg.classList.add('example-overlay');
+    dlg.title = 'ExampleOverlay';
+    dlg.innerHTML = 'hello';
+    addShowButtonForDialog.call(this, dlg);
+  });
+
+  test('instantiate_disableUserClose', function() {
+    var dlg = new tv.b.ui.Overlay();
+    dlg.classList.add('example-overlay');
+    dlg.userCanClose = false;
+    dlg.title = 'Unclosable';
+    dlg.innerHTML = 'This has no close X button.';
+    dlg.buttons.appendChild(makeCloseButton(dlg));
+    addShowButtonForDialog.call(this, dlg);
+  });
+
+  test('instantiateTall', function() {
+    var dlg = new tv.b.ui.Overlay();
+    dlg.title = 'TallContent';
+    var contentEl = document.createElement('div');
+    contentEl.style.overflowY = 'auto';
+    dlg.appendChild(contentEl);
+
+    for (var i = 0; i < 1000; i++) {
+      var el = document.createElement('div');
+      el.textContent = 'line ' + i;
+      contentEl.appendChild(el);
+    }
+
+
+    dlg.buttons.appendChild(makeButton('i am a button'));
+    addShowButtonForDialog.call(this, dlg);
+  });
+
+  test('instantiateTallWithManyDirectChildren', function() {
+    var dlg = new tv.b.ui.Overlay();
+    dlg.title = 'TallContent';
+    for (var i = 0; i < 100; i++) {
+      var el = document.createElement('div');
+      el.style.webkitFlex = '1 0 auto';
+      el.textContent = 'line ' + i;
+      dlg.appendChild(el);
+    }
+
+    dlg.buttons.appendChild(makeButton('i am a button'));
+    addShowButtonForDialog.call(this, dlg);
+  });
+
+  test('closeclickEvent', function() {
+    var dlg = new tv.b.ui.Overlay();
+    dlg.title = 'Test closeclick event';
+    var closeBtn = makeCloseButton(dlg);
+    dlg.buttons.appendChild(closeBtn);
+
+    var closeClicked = false;
+    dlg.addEventListener('closeclick', function() {
+      closeClicked = true;
+    });
+
+    return new Promise(function(resolve, reject) {
+      function pressClose() {
+        closeBtn.click();
+        if (closeClicked)
+          resolve();
+        else
+          reject(new Error('closeclick event is not dispatched'));
+      }
+      dlg.visible = true;
+      setTimeout(pressClose, 60);
+    });
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/pie_chart.css b/trace-viewer/trace_viewer/base/ui/pie_chart.css
new file mode 100644
index 0000000..e6dc135
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/pie_chart.css
@@ -0,0 +1,16 @@
+/* Copyright 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.
+ */
+* /deep/ .pie-chart .arc-text {
+  font-size: 8pt;
+}
+
+* /deep/ .pie-chart .label {
+  font-size: 10pt;
+}
+
+* /deep/ .pie-chart polyline {
+  fill: none;
+  stroke: black;
+}
diff --git a/trace-viewer/trace_viewer/base/ui/pie_chart.html b/trace-viewer/trace_viewer/base/ui/pie_chart.html
new file mode 100644
index 0000000..261f174
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/pie_chart.html
@@ -0,0 +1,277 @@
+<!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">
+<link rel="import" href="/base/ui/d3.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/chart_base.html">
+<link rel="stylesheet" href="/base/ui/pie_chart.css">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+  var ChartBase = tv.b.ui.ChartBase;
+  var getColorOfKey = tv.b.ui.getColorOfKey;
+
+  var MIN_RADIUS = 100;
+
+  /**
+   * @constructor
+   */
+  var PieChart = tv.b.ui.define('pie-chart', ChartBase);
+
+  PieChart.prototype = {
+    __proto__: ChartBase.prototype,
+
+    decorate: function() {
+      ChartBase.prototype.decorate.call(this);
+      this.classList.add('pie-chart');
+
+      this.data_ = undefined;
+      this.seriesKeys_ = undefined;
+
+      var chartAreaSel = d3.select(this.chartAreaElement);
+      var pieGroupSel = chartAreaSel.append('g')
+        .attr('class', 'pie-group');
+      this.pieGroup_ = pieGroupSel.node();
+
+      this.pathsGroup_ = pieGroupSel.append('g')
+        .attr('class', 'paths')
+        .node();
+      this.labelsGroup_ = pieGroupSel.append('g')
+        .attr('class', 'labels')
+        .node();
+      this.linesGroup_ = pieGroupSel.append('g')
+        .attr('class', 'lines')
+        .node();
+    },
+
+    get data() {
+      return this.data_;
+    },
+
+
+    /**
+     * @param {Array} data Data for the chart, where each element in the array
+     * must be of the form {label: str, value: number}.
+     */
+    set data(data) {
+      if (data !== undefined) {
+        // Figure out the label values in the data set. E.g. from
+        //   [{label: 'a', ...}, {label: 'b', ...}]
+        // we would commpute ['a', 'y']. These become the series keys.
+        var seriesKeys = [];
+        var seenSeriesKeys = {};
+        data.forEach(function(d) {
+          var k = d.label;
+          if (seenSeriesKeys[k])
+            throw new Error('Label ' + k + ' has been used already');
+          seriesKeys.push(k);
+          seenSeriesKeys[k] = true;
+        }, this);
+        this.seriesKeys_ = seriesKeys;
+      } else {
+        this.seriesKeys_ = undefined;
+      }
+      this.data_ = data;
+      this.updateContents_();
+    },
+
+    get margin() {
+      var margin = {top: 0, right: 0, bottom: 0, left: 0};
+      if (this.chartTitle_)
+        margin.top += 40;
+      return margin;
+    },
+
+    getMinSize: function() {
+      this.updateContents_();
+
+      var labelSel = d3.select(this.labelsGroup_).selectAll('.label');
+      var maxLabelWidth = -Number.MAX_VALUE;
+      var leftTextHeightSum = 0;
+      var rightTextHeightSum = 0;
+      labelSel.each(function(l) {
+        var r = this.getBoundingClientRect();
+        maxLabelWidth = Math.max(maxLabelWidth, r.width + 32);
+        if (this.style.textAnchor == 'end') {
+          leftTextHeightSum += r.height;
+        } else {
+          rightTextHeightSum += r.height;
+        }
+      });
+
+      var titleWidth = this.querySelector(
+          '#title').getBoundingClientRect().width;
+      var margin = this.margin;
+      var marginWidth = margin.left + margin.right;
+      var marginHeight = margin.top + margin.bottom;
+      return {
+        width: Math.max(2 * MIN_RADIUS + 2 * maxLabelWidth,
+                        titleWidth * 1.1) + marginWidth,
+        height: marginHeight + Math.max(2 * MIN_RADIUS,
+                                        leftTextHeightSum,
+                                        rightTextHeightSum) * 1.25
+      };
+    },
+
+
+    getLegendKeys_: function() {
+      // This class creates its own legend, instead of using ChartBase.
+      return undefined;
+    },
+
+    updateScales_: function(width, height) {
+      if (this.data_ === undefined)
+        return;
+    },
+
+    updateContents_: function() {
+      ChartBase.prototype.updateContents_.call(this);
+      if (!this.data_)
+        return;
+
+      var width = this.chartAreaSize.width;
+      var height = this.chartAreaSize.height;
+      var radius = Math.max(MIN_RADIUS, Math.min(width, height * 0.95) / 2);
+
+      d3.select(this.pieGroup_).attr(
+          'transform',
+          'translate(' + width / 2 + ',' + height / 2 + ')');
+
+      // Bind the pie layout to its data
+      var pieLayout = d3.layout.pie()
+        .value(function(d) { return d.value; })
+        .sort(null);
+
+      var piePathsSel = d3.select(this.pathsGroup_)
+          .datum(this.data_)
+          .selectAll('path')
+          .data(pieLayout);
+
+      function midAngle(d) {
+        return d.startAngle + (d.endAngle - d.startAngle) / 2;
+      }
+
+      var pathsArc = d3.svg.arc()
+        .innerRadius(0)
+        .outerRadius(radius - 30);
+
+      var valueLabelArc = d3.svg.arc()
+        .innerRadius(radius - 100)
+        .outerRadius(radius - 30);
+
+      var lineBeginArc = d3.svg.arc()
+        .innerRadius(radius - 50)
+        .outerRadius(radius - 50);
+
+      var lineEndArc = d3.svg.arc()
+        .innerRadius(radius)
+        .outerRadius(radius);
+
+      // Paths.
+      piePathsSel.enter().append('path')
+        .attr('class', 'arc')
+        .attr('fill', function(d, i) {
+            var origData = this.data_[i];
+            var highlighted = (origData.label ===
+                               this.currentHighlightedLegendKey);
+            return getColorOfKey(origData.label, highlighted);
+          }.bind(this))
+        .attr('d', pathsArc)
+        .on('click', function(d, i) {
+            var origData = this.data_[i];
+            var event = new Event('item-click');
+            event.data = origData;
+            event.index = i;
+            this.dispatchEvent(event);
+            d3.event.stopPropagation();
+          }.bind(this))
+        .on('mouseenter', function(d, i) {
+            var origData = this.data_[i];
+            this.pushTempHighlightedLegendKey(origData.label);
+          }.bind(this))
+        .on('mouseleave', function(d, i) {
+            var origData = this.data_[i];
+            this.popTempHighlightedLegendKey(origData.label);
+          }.bind(this));
+
+      // Value labels.
+      piePathsSel.enter().append('text')
+        .attr('class', 'arc-text')
+        .attr('transform', function(d) {
+            return 'translate(' + valueLabelArc.centroid(d) + ')';
+          })
+        .attr('dy', '.35em')
+        .style('text-anchor', 'middle')
+        .text(function(d, i) {
+            var origData = this.data_[i];
+            if (origData.valueText === undefined)
+              return '';
+
+            if (d.endAngle - d.startAngle < 0.4)
+              return '';
+            return origData.valueText;
+          }.bind(this));
+
+      piePathsSel.exit().remove();
+
+      // Labels.
+      var labelSel = d3.select(this.labelsGroup_).selectAll('.label')
+          .data(pieLayout(this.data_));
+      labelSel.enter()
+          .append('text')
+          .attr('class', 'label')
+          .attr('dy', '.35em');
+
+      labelSel.text(function(d) {
+        if (d.data.label.length > 40)
+          return d.data.label.substr(0, 40) + '...';
+        return d.data.label;
+      });
+      labelSel.attr('transform', function(d) {
+        var pos = lineEndArc.centroid(d);
+        pos[0] = radius * (midAngle(d) < Math.PI ? 1 : -1);
+        return 'translate(' + pos + ')';
+      });
+      labelSel.style('text-anchor', function(d) {
+        return midAngle(d) < Math.PI ? 'start' : 'end';
+      });
+
+      // Lines.
+      var lineSel = d3.select(this.linesGroup_).selectAll('.line')
+          .data(pieLayout(this.data_));
+      lineSel.enter()
+        .append('polyline')
+        .attr('class', 'line')
+        .attr('dy', '.35em');
+      lineSel.attr('points', function(d) {
+        var pos = lineEndArc.centroid(d);
+        pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1);
+        return [lineBeginArc.centroid(d), lineEndArc.centroid(d), pos];
+      });
+    },
+
+    updateHighlight_: function() {
+      ChartBase.prototype.updateHighlight_.call(this);
+      // Update color of pie segments.
+      var pathsGroupSel = d3.select(this.pathsGroup_);
+      var that = this;
+      pathsGroupSel.selectAll('.arc').each(function(d, i) {
+        var origData = that.data_[i];
+        var highlighted = origData.label == that.currentHighlightedLegendKey;
+        var color = getColorOfKey(origData.label, highlighted);
+        this.style.fill = color;
+      });
+    }
+  };
+
+  return {
+    PieChart: PieChart
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/pie_chart_test.html b/trace-viewer/trace_viewer/base/ui/pie_chart_test.html
new file mode 100644
index 0000000..a6ca228
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/pie_chart_test.html
@@ -0,0 +1,207 @@
+<!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/ui/pie_chart.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('simple', function() {
+    var chart = new tv.b.ui.PieChart();
+    chart.width = 400;
+    chart.height = 200;
+    assert.equal(chart.getAttribute('width'), '400');
+    assert.equal(chart.getAttribute('height'), '200');
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {label: 'a', value: 100},
+      {label: 'b', value: 200},
+      {label: 'c', value: 300}
+    ];
+    chart.data = data;
+    chart.highlightedLegendKey = 'a';
+    chart.pushTempHighlightedLegendKey('b');
+    chart.highlightedLegendKey = 'c';
+    assert.equal(chart.currentHighlightedLegendKey, 'b');
+    chart.popTempHighlightedLegendKey('b');
+    assert.equal(chart.highlightedLegendKey, 'c');
+    this.addHTMLOutput(chart);
+  });
+
+  test('withValueText', function() {
+    var chart = new tv.b.ui.PieChart();
+    chart.width = 400;
+    chart.height = 200;
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {label: 'a', value: 100, valueText: '100ms'},
+      {label: 'b', value: 200, valueText: '200ms'},
+      {label: 'c', value: 300, valueText: '300ms'}
+    ];
+    chart.data = data;
+    this.addHTMLOutput(chart);
+  });
+
+  test('clickEvent', function() {
+    var chart = new tv.b.ui.PieChart();
+    chart.width = 400;
+    chart.height = 200;
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {label: 'a', value: 100, foo: 42},
+      {label: 'b', value: 200, foo: 43}
+    ];
+    chart.data = data;
+
+    var didGetClick = false;
+    chart.addEventListener('item-click', function(event) {
+      assert.equal(event.index, 1);
+      assert.equal(event.data.foo, 43);
+      didGetClick = true;
+    });
+
+    var arc0 = chart.querySelectorAll('.paths > path')[1];
+    tv.b.dispatchSimpleEvent(arc0, 'click');
+    assert.isTrue(didGetClick);
+  });
+
+  test('lotsOfValues', function() {
+    var chart = new tv.b.ui.PieChart();
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {label: 'a', value: 100},
+      {label: 'bb', value: 200},
+      {label: 'cccc', value: 300},
+      {label: 'dd', value: 50},
+      {label: 'eeeee', value: 250},
+      {label: 'fffffff', value: 120},
+      {label: 'ggg', value: 90},
+      {label: 'hhhh', value: 175},
+      {label: 'toolongiiiiiiiiiiiiiiiiiiiiiiiii', value: 325},
+      {label: 'jjjjjj', value: 140},
+      {label: 'kkkkkkkkk', value: 170},
+      {label: 'lll', value: 220}
+    ];
+    chart.data = data;
+    this.addHTMLOutput(chart);
+
+    var minSize = chart.getMinSize();
+    chart.setSize(chart.getMinSize());
+  });
+
+  test('denseValues', function() {
+    var chart = new tv.b.ui.PieChart();
+    chart.chartTitle = 'Chart title';
+    var data = [
+      {
+        valueText: '2.855ms',
+        value: 2.854999999999997,
+        label: '156959'
+      },
+      {
+        valueText: '9.949ms',
+        value: 9.948999999999998,
+        label: '16131'
+      },
+      {
+        valueText: '42.314ms',
+        value: 42.314000000000725,
+        label: '51511'
+      },
+      {
+        valueText: '31.069ms',
+        value: 31.06900000000028,
+        label: 'AudioOutputDevice'
+      },
+      {
+        valueText: '1.418ms',
+        value: 1.418,
+        label: 'BrowserBlockingWorker2/50951'
+      },
+      {
+        valueText: '0.044ms',
+        value: 0.044,
+        label: 'BrowserBlockingWorker3/50695'
+      },
+      {
+        valueText: '18.526ms',
+        value: 18.52599999999993,
+        label: 'Chrome_ChildIOThread'
+      },
+      {
+        valueText: '2.888ms',
+        value: 2.888,
+        label: 'Chrome_FileThread'
+      },
+      {
+        valueText: '0.067ms',
+        value: 0.067,
+        label: 'Chrome_HistoryThread'
+      },
+      {
+        valueText: '25.421ms',
+        value: 25.421000000000046,
+        label: 'Chrome_IOThread'
+      },
+      {
+        valueText: '0.019ms',
+        value: 0.019,
+        label: 'Chrome_ProcessLauncherThread'
+      },
+      {
+        valueText: '643.088ms',
+        value: 643.087999999995,
+        label: 'Compositor'
+      },
+      {
+        valueText: '4.05ms',
+        value: 4.049999999999973,
+        label: 'CompositorRasterWorker1/22031'
+      },
+      {
+        valueText: '50.04ms',
+        value: 50.040000000000106,
+        label: 'CrBrowserMain'
+      },
+      {
+        valueText: '1256.513ms',
+        value: 1256.5130000000042,
+        label: 'CrGpuMain'
+      },
+      {
+        valueText: '5502.195ms',
+        value: 5502.19499999999,
+        label: 'CrRendererMain'
+      },
+      {
+        valueText: '15.553ms',
+        value: 15.552999999999862,
+        label: 'FFmpegDemuxer'
+      },
+      {
+        valueText: '63.706ms',
+        value: 63.706000000001524,
+        label: 'Media'
+      },
+      {
+        valueText: '2.742ms',
+        value: 2.7419999999999987,
+        label: 'PowerSaveBlocker'
+      },
+      {
+        valueText: '0.115ms',
+        value: 0.11500000000000005,
+        label: 'Watchdog'
+      }
+    ];
+    chart.data = data;
+    this.addHTMLOutput(chart);
+
+    var minSize = chart.getMinSize();
+    chart.setSize(chart.getMinSize());
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/quad_stack_view.html b/trace-viewer/trace_viewer/base/ui/quad_stack_view.html
new file mode 100644
index 0000000..c80af49
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/quad_stack_view.html
@@ -0,0 +1,686 @@
+<!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/bbox2.html">
+<link rel="import" href="/base/gl_matrix.html">
+<link rel="import" href="/base/quad.html">
+<link rel="import" href="/base/raf.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/base/settings.html">
+<link rel="import" href="/base/ui/camera.html">
+<link rel="import" href="/base/ui/mouse_mode_selector.html">
+<link rel="import" href="/base/ui/mouse_tracker.html">
+
+<style>
+quad-stack-view {
+  display: block;
+  float: left;
+  height: 100%;
+  overflow: hidden;
+  position: relative; /* For the absolute positioned mouse-mode-selector */
+  width: 100%;
+}
+
+quad-stack-view > #header {
+  position: absolute;
+  font-size: 70%;
+  top: 10px;
+  left: 10px;
+  width: 800px;
+}
+quad-stack-view > #stacking-distance-slider {
+  position: absolute;
+  font-size: 70%;
+  top: 10px;
+  right: 10px;
+}
+
+quad-stack-view > #chrome-left {
+  content: url('../images/chrome-left.png');
+  display: none;
+}
+
+quad-stack-view > #chrome-mid {
+  content: url('../images/chrome-mid.png');
+  display: none;
+}
+
+quad-stack-view > #chrome-right {
+  content: url('../images/chrome-right.png');
+  display: none;
+}
+</style>
+
+<template id='quad-stack-view-template'>
+  <div id="header"></div>
+  <input id="stacking-distance-slider" type="range" min=1 max=400 step=1>
+  </input>
+  <canvas id='canvas'></canvas>
+  <img id='chrome-left'/>
+  <img id='chrome-mid'/>
+  <img id='chrome-right'/>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview QuadStackView controls the content and viewing angle a
+ * QuadStack.
+ */
+tv.exportTo('tv.b.ui', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  var constants = {};
+  constants.IMAGE_LOAD_RETRY_TIME_MS = 500;
+  constants.SUBDIVISION_MINIMUM = 1;
+  constants.SUBDIVISION_RECURSION_DEPTH = 3;
+  constants.SUBDIVISION_DEPTH_THRESHOLD = 100;
+  constants.FAR_PLANE_DISTANCE = 10000;
+
+  // Care of bckenney@ via
+  // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120
+  function drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2) {
+    var tmp_p0 = [p0[0], p0[1]];
+    var tmp_p1 = [p1[0], p1[1]];
+    var tmp_p2 = [p2[0], p2[1]];
+    var tmp_t0 = [t0[0], t0[1]];
+    var tmp_t1 = [t1[0], t1[1]];
+    var tmp_t2 = [t2[0], t2[1]];
+
+    ctx.beginPath();
+    ctx.moveTo(tmp_p0[0], tmp_p0[1]);
+    ctx.lineTo(tmp_p1[0], tmp_p1[1]);
+    ctx.lineTo(tmp_p2[0], tmp_p2[1]);
+    ctx.closePath();
+
+    tmp_p1[0] -= tmp_p0[0];
+    tmp_p1[1] -= tmp_p0[1];
+    tmp_p2[0] -= tmp_p0[0];
+    tmp_p2[1] -= tmp_p0[1];
+
+    tmp_t1[0] -= tmp_t0[0];
+    tmp_t1[1] -= tmp_t0[1];
+    tmp_t2[0] -= tmp_t0[0];
+    tmp_t2[1] -= tmp_t0[1];
+
+    var det = 1 / (tmp_t1[0] * tmp_t2[1] - tmp_t2[0] * tmp_t1[1]),
+
+        // linear transformation
+        a = (tmp_t2[1] * tmp_p1[0] - tmp_t1[1] * tmp_p2[0]) * det,
+        b = (tmp_t2[1] * tmp_p1[1] - tmp_t1[1] * tmp_p2[1]) * det,
+        c = (tmp_t1[0] * tmp_p2[0] - tmp_t2[0] * tmp_p1[0]) * det,
+        d = (tmp_t1[0] * tmp_p2[1] - tmp_t2[0] * tmp_p1[1]) * det,
+
+        // translation
+        e = tmp_p0[0] - a * tmp_t0[0] - c * tmp_t0[1],
+        f = tmp_p0[1] - b * tmp_t0[0] - d * tmp_t0[1];
+
+    ctx.save();
+    ctx.transform(a, b, c, d, e, f);
+    ctx.clip();
+    ctx.drawImage(img, 0, 0);
+    ctx.restore();
+  }
+
+  function drawTriangleSub(
+      ctx, img, p0, p1, p2, t0, t1, t2, opt_recursion_depth) {
+    var depth = opt_recursion_depth || 0;
+
+    // We may subdivide if we are not at the limit of recursion.
+    var subdivisionIndex = 0;
+    if (depth < constants.SUBDIVISION_MINIMUM) {
+      subdivisionIndex = 7;
+    } else if (depth < constants.SUBDIVISION_RECURSION_DEPTH) {
+      if (Math.abs(p0[2] - p1[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
+        subdivisionIndex += 1;
+      if (Math.abs(p0[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
+        subdivisionIndex += 2;
+      if (Math.abs(p1[2] - p2[2]) > constants.SUBDIVISION_DEPTH_THRESHOLD)
+        subdivisionIndex += 4;
+    }
+
+    // These need to be created every time, since temporaries
+    // outside of the scope will be rewritten in recursion.
+    var p01 = vec4.create();
+    var p02 = vec4.create();
+    var p12 = vec4.create();
+    var t01 = vec2.create();
+    var t02 = vec2.create();
+    var t12 = vec2.create();
+
+    // Calculate the position before w-divide.
+    for (var i = 0; i < 2; ++i) {
+      p0[i] *= p0[2];
+      p1[i] *= p1[2];
+      p2[i] *= p2[2];
+    }
+
+    // Interpolate the 3d position.
+    for (var i = 0; i < 4; ++i) {
+      p01[i] = (p0[i] + p1[i]) / 2;
+      p02[i] = (p0[i] + p2[i]) / 2;
+      p12[i] = (p1[i] + p2[i]) / 2;
+    }
+
+    // Re-apply w-divide to the original points and the interpolated ones.
+    for (var i = 0; i < 2; ++i) {
+      p0[i] /= p0[2];
+      p1[i] /= p1[2];
+      p2[i] /= p2[2];
+
+      p01[i] /= p01[2];
+      p02[i] /= p02[2];
+      p12[i] /= p12[2];
+    }
+
+    // Interpolate the texture coordinates.
+    for (var i = 0; i < 2; ++i) {
+      t01[i] = (t0[i] + t1[i]) / 2;
+      t02[i] = (t0[i] + t2[i]) / 2;
+      t12[i] = (t1[i] + t2[i]) / 2;
+    }
+
+    // Based on the index, we subdivide the triangle differently.
+    // Assuming the triangle is p0, p1, p2 and points between i j
+    // are represented as pij (that is, a point between p2 and p0
+    // is p02, etc), then the new triangles are defined by
+    // the 3rd 4th and 5th arguments into the function.
+    switch (subdivisionIndex) {
+      case 1:
+        drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
+        drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
+        break;
+      case 2:
+        drawTriangleSub(ctx, img, p0, p1, p02, t0, t1, t02, depth + 1);
+        drawTriangleSub(ctx, img, p1, p02, p2, t1, t02, t2, depth + 1);
+        break;
+      case 3:
+        drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
+        drawTriangleSub(ctx, img, p02, p01, p2, t02, t01, t2, depth + 1);
+        drawTriangleSub(ctx, img, p01, p1, p2, t01, t1, t2, depth + 1);
+        break;
+      case 4:
+        drawTriangleSub(ctx, img, p0, p12, p2, t0, t12, t2, depth + 1);
+        drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
+        break;
+      case 5:
+        drawTriangleSub(ctx, img, p0, p01, p2, t0, t01, t2, depth + 1);
+        drawTriangleSub(ctx, img, p2, p01, p12, t2, t01, t12, depth + 1);
+        drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
+        break;
+      case 6:
+        drawTriangleSub(ctx, img, p0, p12, p02, t0, t12, t02, depth + 1);
+        drawTriangleSub(ctx, img, p0, p1, p12, t0, t1, t12, depth + 1);
+        drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
+        break;
+      case 7:
+        drawTriangleSub(ctx, img, p0, p01, p02, t0, t01, t02, depth + 1);
+        drawTriangleSub(ctx, img, p01, p12, p02, t01, t12, t02, depth + 1);
+        drawTriangleSub(ctx, img, p01, p1, p12, t01, t1, t12, depth + 1);
+        drawTriangleSub(ctx, img, p02, p12, p2, t02, t12, t2, depth + 1);
+        break;
+      default:
+        // In the 0 case and all other cases, we simply draw the triangle.
+        drawTexturedTriangle(ctx, img, p0, p1, p2, t0, t1, t2);
+        break;
+    }
+  }
+
+  // Created to avoid creating garbage when doing bulk transforms.
+  var tmp_vec4 = vec4.create();
+  function transform(transformed, point, matrix, viewport) {
+    vec4.set(tmp_vec4, point[0], point[1], 0, 1);
+    vec4.transformMat4(tmp_vec4, tmp_vec4, matrix);
+
+    var w = tmp_vec4[3];
+    if (w < 1e-6) w = 1e-6;
+
+    transformed[0] = ((tmp_vec4[0] / w) + 1) * viewport.width / 2;
+    transformed[1] = ((tmp_vec4[1] / w) + 1) * viewport.height / 2;
+    transformed[2] = w;
+  }
+
+  function drawProjectedQuadBackgroundToContext(
+      quad, p1, p2, p3, p4, ctx, quadCanvas) {
+    if (quad.imageData) {
+      quadCanvas.width = quad.imageData.width;
+      quadCanvas.height = quad.imageData.height;
+      quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0);
+      var quadBBox = new tv.b.BBox2();
+      quadBBox.addQuad(quad);
+      var iw = quadCanvas.width;
+      var ih = quadCanvas.height;
+      drawTriangleSub(
+          ctx, quadCanvas,
+          p1, p2, p4,
+          [0, 0], [iw, 0], [0, ih]);
+      drawTriangleSub(
+          ctx, quadCanvas,
+          p2, p3, p4,
+          [iw, 0], [iw, ih], [0, ih]);
+    }
+
+    if (quad.backgroundColor) {
+      ctx.fillStyle = quad.backgroundColor;
+      ctx.beginPath();
+      ctx.moveTo(p1[0], p1[1]);
+      ctx.lineTo(p2[0], p2[1]);
+      ctx.lineTo(p3[0], p3[1]);
+      ctx.lineTo(p4[0], p4[1]);
+      ctx.closePath();
+      ctx.fill();
+    }
+  }
+
+  function drawProjectedQuadOutlineToContext(
+      quad, p1, p2, p3, p4, ctx, quadCanvas) {
+    ctx.beginPath();
+    ctx.moveTo(p1[0], p1[1]);
+    ctx.lineTo(p2[0], p2[1]);
+    ctx.lineTo(p3[0], p3[1]);
+    ctx.lineTo(p4[0], p4[1]);
+    ctx.closePath();
+    ctx.save();
+    if (quad.borderColor)
+      ctx.strokeStyle = quad.borderColor;
+    else
+      ctx.strokeStyle = 'rgb(128,128,128)';
+
+    if (quad.shadowOffset) {
+      ctx.shadowColor = 'rgb(0, 0, 0)';
+      ctx.shadowOffsetX = quad.shadowOffset[0];
+      ctx.shadowOffsetY = quad.shadowOffset[1];
+      if (quad.shadowBlur)
+        ctx.shadowBlur = quad.shadowBlur;
+    }
+
+    if (quad.borderWidth)
+      ctx.lineWidth = quad.borderWidth;
+    else
+      ctx.lineWidth = 1;
+
+    ctx.stroke();
+    ctx.restore();
+  }
+
+  function drawProjectedQuadSelectionOutlineToContext(
+      quad, p1, p2, p3, p4, ctx, quadCanvas) {
+    if (!quad.upperBorderColor)
+      return;
+
+    ctx.lineWidth = 8;
+    ctx.strokeStyle = quad.upperBorderColor;
+
+    ctx.beginPath();
+    ctx.moveTo(p1[0], p1[1]);
+    ctx.lineTo(p2[0], p2[1]);
+    ctx.lineTo(p3[0], p3[1]);
+    ctx.lineTo(p4[0], p4[1]);
+    ctx.closePath();
+    ctx.stroke();
+  }
+
+  function drawProjectedQuadToContext(
+      passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) {
+    if (passNumber === 0) {
+      drawProjectedQuadBackgroundToContext(
+          quad, p1, p2, p3, p4, ctx, quadCanvas);
+    } else if (passNumber === 1) {
+      drawProjectedQuadOutlineToContext(
+          quad, p1, p2, p3, p4, ctx, quadCanvas);
+    } else if (passNumber === 2) {
+      drawProjectedQuadSelectionOutlineToContext(
+          quad, p1, p2, p3, p4, ctx, quadCanvas);
+    } else {
+      throw new Error('Invalid pass number');
+    }
+  }
+
+  var tmp_p1 = vec3.create();
+  var tmp_p2 = vec3.create();
+  var tmp_p3 = vec3.create();
+  var tmp_p4 = vec3.create();
+  function transformAndProcessQuads(
+      matrix, viewport, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) {
+
+    for (var passNumber = 0; passNumber < numPasses; passNumber++) {
+      for (var i = 0; i < quads.length; i++) {
+        var quad = quads[i];
+        transform(tmp_p1, quad.p1, matrix, viewport);
+        transform(tmp_p2, quad.p2, matrix, viewport);
+        transform(tmp_p3, quad.p3, matrix, viewport);
+        transform(tmp_p4, quad.p4, matrix, viewport);
+        handleQuadFunc(passNumber, quad,
+                       tmp_p1, tmp_p2, tmp_p3, tmp_p4,
+                       opt_arg1, opt_arg2);
+      }
+    }
+  }
+
+  /**
+   * @constructor
+   */
+  var QuadStackView = tv.b.ui.define('quad-stack-view');
+
+  QuadStackView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.className = 'quad-stack-view';
+
+      var node = tv.b.instantiateTemplate('#quad-stack-view-template',
+                                          THIS_DOC);
+      this.appendChild(node);
+      this.updateHeaderVisibility_();
+      this.canvas_ = this.querySelector('#canvas');
+      this.chromeImages_ = {
+        left: this.querySelector('#chrome-left'),
+        mid: this.querySelector('#chrome-mid'),
+        right: this.querySelector('#chrome-right')
+      };
+
+      var stackingDistanceSlider = this.querySelector(
+          '#stacking-distance-slider');
+      stackingDistanceSlider.value = tv.b.Settings.get(
+          'quadStackView.stackingDistance', 45);
+      stackingDistanceSlider.addEventListener(
+          'change', this.onStackingDistanceChange_.bind(this));
+      stackingDistanceSlider.addEventListener(
+          'input', this.onStackingDistanceChange_.bind(this));
+
+      this.trackMouse_();
+
+      this.camera_ = new tv.b.ui.Camera(this.mouseModeSelector_);
+      this.camera_.addEventListener('renderrequired',
+          this.onRenderRequired_.bind(this));
+      this.cameraWasReset_ = false;
+      this.camera_.canvas = this.canvas_;
+
+      this.viewportRect_ = tv.b.Rect.fromXYWH(0, 0, 0, 0);
+
+      this.pixelRatio_ = window.devicePixelRatio || 1;
+    },
+
+    updateHeaderVisibility_: function() {
+      if (this.headerText)
+        this.querySelector('#header').style.display = '';
+      else
+        this.querySelector('#header').style.display = 'none';
+    },
+
+    get headerText() {
+      return this.querySelector('#header').textContent;
+    },
+
+    set headerText(headerText) {
+      this.querySelector('#header').textContent = headerText;
+      this.updateHeaderVisibility_();
+    },
+
+    onStackingDistanceChange_: function(e) {
+      tv.b.Settings.set('quadStackView.stackingDistance',
+                        this.stackingDistance);
+      this.scheduleRender();
+      e.stopPropagation();
+    },
+
+    get stackingDistance() {
+      return this.querySelector('#stacking-distance-slider').value;
+    },
+
+    get mouseModeSelector() {
+      return this.mouseModeSelector_;
+    },
+
+    get camera() {
+      return this.camera_;
+    },
+
+    set quads(q) {
+      this.quads_ = q;
+      this.scheduleRender();
+    },
+
+    set deviceRect(rect) {
+      if (!rect || rect.equalTo(this.deviceRect_))
+        return;
+
+      this.deviceRect_ = rect;
+      this.camera_.deviceRect = rect;
+      this.chromeQuad_ = undefined;
+    },
+
+    resize: function() {
+      if (!this.offsetParent)
+        return true;
+
+      var width = parseInt(window.getComputedStyle(this.offsetParent).width);
+      var height = parseInt(window.getComputedStyle(this.offsetParent).height);
+      var rect = tv.b.Rect.fromXYWH(0, 0, width, height);
+
+      if (rect.equalTo(this.viewportRect_))
+        return false;
+
+      this.viewportRect_ = rect;
+      this.style.width = width + 'px';
+      this.style.height = height + 'px';
+      this.canvas_.style.width = width + 'px';
+      this.canvas_.style.height = height + 'px';
+      this.canvas_.width = this.pixelRatio_ * width;
+      this.canvas_.height = this.pixelRatio_ * height;
+      if (!this.cameraWasReset_) {
+        this.camera_.resetCamera();
+        this.cameraWasReset_ = true;
+      }
+      return true;
+    },
+
+    readyToDraw: function() {
+      // If src isn't set yet, set it to ensure we can use
+      // the image to draw onto a canvas.
+      if (!this.chromeImages_.left.src) {
+        var leftContent =
+            window.getComputedStyle(this.chromeImages_.left).content;
+        leftContent = leftContent.replace(/url\((.*)\)/, '$1');
+
+        var midContent =
+            window.getComputedStyle(this.chromeImages_.mid).content;
+        midContent = midContent.replace(/url\((.*)\)/, '$1');
+
+        var rightContent =
+            window.getComputedStyle(this.chromeImages_.right).content;
+        rightContent = rightContent.replace(/url\((.*)\)/, '$1');
+
+        this.chromeImages_.left.src = leftContent;
+        this.chromeImages_.mid.src = midContent;
+        this.chromeImages_.right.src = rightContent;
+      }
+
+      // If all of the images are loaded (height > 0), then
+      // we are ready to draw.
+      return (this.chromeImages_.left.height > 0) &&
+             (this.chromeImages_.mid.height > 0) &&
+             (this.chromeImages_.right.height > 0);
+    },
+
+    get chromeQuad() {
+      if (this.chromeQuad_)
+        return this.chromeQuad_;
+
+      // Draw the chrome border into a separate canvas.
+      var chromeCanvas = document.createElement('canvas');
+      var offsetY = this.chromeImages_.left.height;
+
+      chromeCanvas.width = this.deviceRect_.width;
+      chromeCanvas.height = this.deviceRect_.height + offsetY;
+
+      var leftWidth = this.chromeImages_.left.width;
+      var midWidth = this.chromeImages_.mid.width;
+      var rightWidth = this.chromeImages_.right.width;
+
+      var chromeCtx = chromeCanvas.getContext('2d');
+      chromeCtx.drawImage(this.chromeImages_.left, 0, 0);
+
+      chromeCtx.save();
+      chromeCtx.translate(leftWidth, 0);
+
+      // Calculate the scale of the mid image.
+      var s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth;
+      chromeCtx.scale(s, 1);
+
+      chromeCtx.drawImage(this.chromeImages_.mid, 0, 0);
+      chromeCtx.restore();
+
+      chromeCtx.drawImage(
+          this.chromeImages_.right, leftWidth + s * midWidth, 0);
+
+      // Construct the quad.
+      var chromeRect = tv.b.Rect.fromXYWH(
+          this.deviceRect_.x,
+          this.deviceRect_.y - offsetY,
+          this.deviceRect_.width,
+          this.deviceRect_.height + offsetY);
+      var chromeQuad = tv.b.Quad.fromRect(chromeRect);
+      chromeQuad.stackingGroupId = this.maxStackingGroupId_ + 1;
+      chromeQuad.imageData = chromeCtx.getImageData(
+          0, 0, chromeCanvas.width, chromeCanvas.height);
+      chromeQuad.shadowOffset = [0, 0];
+      chromeQuad.shadowBlur = 5;
+      chromeQuad.borderWidth = 3;
+      this.chromeQuad_ = chromeQuad;
+      return this.chromeQuad_;
+    },
+
+    scheduleRender: function() {
+      if (this.redrawScheduled_)
+        return false;
+      this.redrawScheduled_ = true;
+      tv.b.requestAnimationFrame(this.render, this);
+    },
+
+    onRenderRequired_: function(e) {
+      this.scheduleRender();
+    },
+
+    stackTransformAndProcessQuads_: function(
+        numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) {
+      var mv = this.camera_.modelViewMatrix;
+      var p = this.camera_.projectionMatrix;
+
+      var viewport = tv.b.Rect.fromXYWH(
+          0, 0, this.canvas_.width, this.canvas_.height);
+
+      // Calculate the quad stacks.
+      var quadStacks = [];
+      for (var i = 0; i < this.quads_.length; ++i) {
+        var quad = this.quads_[i];
+        var stackingId = quad.stackingGroupId || 0;
+        while (stackingId >= quadStacks.length)
+          quadStacks.push([]);
+
+        quadStacks[stackingId].push(quad);
+      }
+
+      var mvp = mat4.create();
+      this.maxStackingGroupId_ = quadStacks.length;
+      var effectiveStackingDistance =
+          this.stackingDistance * this.camera_.stackingDistanceDampening;
+
+      // Draw the quad stacks, raising each subsequent level.
+      mat4.multiply(mvp, p, mv);
+      for (var i = 0; i < quadStacks.length; ++i) {
+        transformAndProcessQuads(mvp, viewport, quadStacks[i],
+                                 numPasses, handleQuadFunc,
+                                 opt_arg1, opt_arg2);
+
+        mat4.translate(mv, mv, [0, 0, effectiveStackingDistance]);
+        mat4.multiply(mvp, p, mv);
+      }
+
+      if (includeChromeQuad && this.deviceRect_) {
+        transformAndProcessQuads(mvp, viewport, [this.chromeQuad],
+                                 numPasses, drawProjectedQuadToContext,
+                                 opt_arg1, opt_arg2);
+      }
+    },
+
+    render: function() {
+      this.redrawScheduled_ = false;
+
+      if (!this.readyToDraw()) {
+        setTimeout(this.scheduleRender.bind(this),
+                   constants.IMAGE_LOAD_RETRY_TIME_MS);
+        return;
+      }
+
+      if (!this.quads_)
+        return;
+
+      var canvasCtx = this.canvas_.getContext('2d');
+      if (!this.resize())
+        canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
+
+      var quadCanvas = document.createElement('canvas');
+      this.stackTransformAndProcessQuads_(
+          3, drawProjectedQuadToContext, true,
+          canvasCtx, quadCanvas);
+      quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources.
+    },
+
+    trackMouse_: function() {
+      this.mouseModeSelector_ = new tv.b.ui.MouseModeSelector(this.canvas_);
+      this.mouseModeSelector_.supportedModeMask =
+          tv.b.ui.MOUSE_SELECTOR_MODE.SELECTION |
+          tv.b.ui.MOUSE_SELECTOR_MODE.PANSCAN |
+          tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM |
+          tv.b.ui.MOUSE_SELECTOR_MODE.ROTATE;
+      this.mouseModeSelector_.mode = tv.b.ui.MOUSE_SELECTOR_MODE.PANSCAN;
+      this.mouseModeSelector_.pos = {x: 0, y: 100};
+      this.appendChild(this.mouseModeSelector_);
+      this.mouseModeSelector_.settingsKey =
+          'quadStackView.mouseModeSelector';
+
+      this.mouseModeSelector_.setModifierForAlternateMode(
+          tv.b.ui.MOUSE_SELECTOR_MODE.ROTATE, tv.b.ui.MODIFIER.SHIFT);
+      this.mouseModeSelector_.setModifierForAlternateMode(
+          tv.b.ui.MOUSE_SELECTOR_MODE.PANSCAN, tv.b.ui.MODIFIER.SPACE);
+      this.mouseModeSelector_.setModifierForAlternateMode(
+          tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM, tv.b.ui.MODIFIER.CMD_OR_CTRL);
+
+      this.mouseModeSelector_.addEventListener('updateselection',
+          this.onSelectionUpdate_.bind(this));
+      this.mouseModeSelector_.addEventListener('endselection',
+          this.onSelectionUpdate_.bind(this));
+    },
+
+    extractRelativeMousePosition_: function(e) {
+      var br = this.canvas_.getBoundingClientRect();
+      return [
+        this.pixelRatio_ * (e.clientX - this.canvas_.offsetLeft - br.left),
+        this.pixelRatio_ * (e.clientY - this.canvas_.offsetTop - br.top)
+      ];
+    },
+
+    onSelectionUpdate_: function(e) {
+      var mousePos = this.extractRelativeMousePosition_(e);
+      var res = [];
+      function handleQuad(passNumber, quad, p1, p2, p3, p4) {
+        if (tv.b.pointInImplicitQuad(mousePos, p1, p2, p3, p4))
+          res.push(quad);
+      }
+      this.stackTransformAndProcessQuads_(1, handleQuad, false);
+      var e = new Event('selectionchange', false, false);
+      e.quads = res;
+      this.dispatchEvent(e);
+    }
+  };
+
+  return {
+    QuadStackView: QuadStackView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/sortable_table.css b/trace-viewer/trace_viewer/base/ui/sortable_table.css
new file mode 100644
index 0000000..aa4ba52
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/sortable_table.css
@@ -0,0 +1,8 @@
+/* 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.
+ */
+
+.sortable-table > thead > tr > td {
+  cursor: pointer !important;
+}
diff --git a/trace-viewer/trace_viewer/base/ui/sortable_table.html b/trace-viewer/trace_viewer/base/ui/sortable_table.html
new file mode 100644
index 0000000..41de2e3
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/sortable_table.html
@@ -0,0 +1,186 @@
+<!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/ui.html">
+<link rel="stylesheet" href="/base/ui/sortable_table.css">
+<script>
+'use strict';
+
+/**
+ * @fileoverview A sortable table with history states.
+ */
+tv.exportTo('tv.b.ui', function() {
+  /**
+   * @constructor
+   */
+  var SortableTable = tv.b.ui.define('sortable-table');
+
+  var UNSORTED_ARROW = '&#x25BF';
+  var SORT_ASCENDING_ARROW = '&#x25BE';
+  var SORT_DESCENDING_ARROW = '&#x25B4';
+  var SORT_DIR_ASCENDING = 'downward';
+  var SORT_DIR_DESCENDING = 'upward';
+
+  SortableTable.prototype = {
+    __proto__: HTMLTableElement.prototype,
+
+    decorate: function() {
+      this.classList.add('sortable-table');
+      if (!this.tHead)
+        return;
+      var headerRow = this.tHead.rows[0];
+      var currentState = window.history.state;
+      for (var i = 0; i < headerRow.cells.length; i++) {
+        headerRow.cells[i].addEventListener('click',
+                                            this.onItemClicked_, true);
+        headerRow.cells[i].innerHTML += '&nbsp;' + UNSORTED_ARROW;
+      }
+
+      if (currentState && currentState.tableSorting) {
+        var hashCode = this.sortingHashCode_();
+        if (currentState.tableSorting[hashCode]) {
+          this.sort(currentState.tableSorting[hashCode].col,
+                    currentState.tableSorting[hashCode].sortDirection);
+        }
+      }
+    },
+
+    onItemClicked_: function(e) {
+      // 'this' refers to the table cell that has been clicked.
+      var headerRow = this.parentNode;
+      var table = headerRow.parentNode.parentNode;
+      var colIndex = Array.prototype.slice.call(headerRow.cells).indexOf(this);
+      var sortDirection = table.sort(colIndex);
+      var currentState = history.state;
+      if (!currentState.tableSorting)
+        currentState.tableSorting = {};
+      currentState.tableSorting[table.sortingHashCode_()] = {
+        col: colIndex,
+        sortDirection: sortDirection
+      };
+      window.history.pushState(currentState, '');
+    },
+
+    sort: function(colIndex, opt_sortDirection) {
+      var headerRow = this.tHead.rows[0];
+      var headerCell = headerRow.cells[colIndex];
+
+      if (!headerCell.hasAttribute('sort')) {
+        // we are either sorting a new column (not previously sorted),
+        // or sorting based on a given sort direction (opt_sortDirection).
+        return sortByColumn_(headerRow, headerCell, colIndex,
+                             opt_sortDirection);
+      } else {
+        // resort the current sort column in the other direction
+        return reverseSortDirection_(headerRow, headerCell, opt_sortDirection);
+      }
+      return sortDirection;
+    },
+
+    // A very simple hash function, based only on the header row and
+    // the table location. It is used to check that table loaded
+    // can be sorted according to the given history information.
+    sortingHashCode_: function() {
+      if (this.sortingHashValue_)
+        return this.sortingHashValue_;
+      var headerText = this.tHead.rows[0].innerText;
+      var hash = 0;
+      for (var i = 0; i < headerText.length; i++) {
+        if (headerText.charCodeAt(i) < 127)
+          hash += headerText.charCodeAt(i);
+      }
+
+      // use the table index as well in case the same table
+      // is displayed more than once on a single page.
+      var tableIndex = Array.prototype.slice.call(
+          document.getElementsByClassName('sortable-table')).indexOf(this);
+      this.sortingHashValue_ = tableIndex + '' + hash;
+      return this.sortingHashValue_;
+    }
+  };
+
+  function compareAscending_(a, b) {
+    return compare_(a, b);
+  }
+
+  function compareDescending_(a, b) {
+    return compare_(b, a);
+  }
+
+  function compare_(a, b) {
+    var a1 = parseFloat(a);
+    var b1 = parseFloat(b);
+    if (isNaN(a1) && isNaN(b1))
+      return a.toString().localeCompare(b.toString());
+    if (isNaN(a1))
+      return -1;
+    if (isNaN(b1))
+      return 1;
+    return a1 - b1;
+  }
+
+  function sortByColumn_(headerRow, headerCell, colIndex, opt_sortDirection) {
+    var sortDirection = opt_sortDirection || SORT_DIR_ASCENDING;
+    // remove sort attribute from other header elements.
+    for (var i = 0; i < headerRow.cells.length; i++) {
+      if (headerRow.cells[i].getAttribute('sort')) {
+        headerRow.cells[i].removeAttribute('sort');
+        var headerStr = headerRow.cells[i].innerHTML;
+        headerRow.cells[i].innerHTML =
+            headerStr.substr(0, headerStr.length - 2) + UNSORTED_ARROW;
+      }
+    }
+
+    var headerStr = headerRow.cells[colIndex].innerHTML;
+    headerCell.innerHTML = headerStr.substr(0, headerStr.length - 2) +
+                           (sortDirection == SORT_DIR_ASCENDING ?
+                            SORT_ASCENDING_ARROW : SORT_DESCENDING_ARROW);
+
+    headerCell.setAttribute('sort', sortDirection);
+    var rows = headerRow.parentNode.parentNode.tBodies[0].rows;
+    var tempRows = [];
+    for (var i = 0; i < rows.length; i++) {
+      tempRows.push([rows[i].cells[colIndex].innerText, rows[i]]);
+    }
+
+    tempRows.sort(sortDirection == SORT_DIR_ASCENDING ?
+                      compareAscending_ : compareDescending_);
+
+    for (var j = 0; j < tempRows.length; j++) {
+      headerRow.parentNode.parentNode.tBodies[0].
+          appendChild(tempRows[j][1]);
+    }
+    return sortDirection;
+  }
+
+  function reverseSortDirection_(headerRow, headerCell, opt_sortDirection) {
+    var sortDirection = headerCell.getAttribute('sort');
+    // if it is already sorted in the correct direction, do nothing.
+    if (opt_sortDirection == sortDirection)
+      return sortDirection;
+    sortDirection = sortDirection == SORT_DIR_DESCENDING ?
+                    SORT_DIR_ASCENDING : SORT_DIR_DESCENDING;
+    headerCell.setAttribute('sort', sortDirection);
+    var headerStr = headerCell.innerHTML;
+    headerCell.innerHTML = headerStr.substr(0, headerStr.length - 2) +
+                           (sortDirection == SORT_DIR_ASCENDING ?
+                            SORT_ASCENDING_ARROW : SORT_DESCENDING_ARROW);
+    // instead of re-sorting, we reverse the sorted rows.
+    var headerRow = headerCell.parentNode;
+    var tbody = headerRow.parentNode.parentNode.tBodies[0];
+    var tempRows = [];
+    for (var i = 0; i < tbody.rows.length; i++)
+      tempRows[tempRows.length] = tbody.rows[i];
+    for (var i = tempRows.length - 1; i >= 0; i--)
+      tbody.appendChild(tempRows[i]);
+    return sortDirection;
+  }
+
+  return {
+    SortableTable: SortableTable
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/sortable_table_test.html b/trace-viewer/trace_viewer/base/ui/sortable_table_test.html
new file mode 100644
index 0000000..736acdb
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/sortable_table_test.html
@@ -0,0 +1,236 @@
+<!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/utils.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/base/ui/sortable_table.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var SortableTable = tv.b.ui.SortableTable;
+
+  function convertToHTML(s) {
+    var res = '';
+    for (var i = 0; i < s.length; i++) {
+      res += s.charCodeAt(i) > 127 ?
+             '&#x' + s.charCodeAt(i).toString(16).toUpperCase() + ';' :
+             s.charAt(i);
+    }
+    return res;
+  }
+
+  function SimpleTable() {
+    var table = document.createElement('table');
+    var thead = table.createTHead();
+    var tfoot = table.createTFoot();
+    var tbody = table.createTBody();
+    var headerRow = thead.insertRow(0);
+    headerRow.insertCell(0).appendChild(document.createTextNode('Name'));
+    headerRow.insertCell(1).appendChild(document.createTextNode('Value'));
+    var row1 = tbody.insertRow(0);
+    row1.insertCell(0).appendChild(document.createTextNode('First'));
+    row1.insertCell(1).appendChild(document.createTextNode('2'));
+    var row2 = tbody.insertRow(1);
+    row2.insertCell(0).appendChild(document.createTextNode('Middle'));
+    row2.insertCell(1).appendChild(document.createTextNode('18'));
+    var row3 = tbody.insertRow(2);
+    row3.insertCell(0).appendChild(document.createTextNode('Last'));
+    row3.insertCell(1).appendChild(document.createTextNode('1'));
+    var footerRow = tfoot.insertRow(0);
+    footerRow.insertCell(0).appendChild(document.createTextNode('Average'));
+    footerRow.insertCell(1).appendChild(document.createTextNode('7'));
+    return table;
+  }
+
+  test('instantiate', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25BF;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BF;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'First');
+    assert.equal(tableRows[0].cells[1].innerText, '2');
+    assert.equal(tableRows[1].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[1].cells[1].innerText, '18');
+    assert.equal(tableRows[2].cells[0].innerText, 'Last');
+    assert.equal(tableRows[2].cells[1].innerText, '1');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+
+  test('sortOnAlphabeticColumnAscending', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    table.sort(0 /*, 'downward' */);
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25BE;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BF;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'First');
+    assert.equal(tableRows[0].cells[1].innerText, '2');
+    assert.equal(tableRows[1].cells[0].innerText, 'Last');
+    assert.equal(tableRows[1].cells[1].innerText, '1');
+    assert.equal(tableRows[2].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[2].cells[1].innerText, '18');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+
+  test('sortOnAlphabeticColumnDescending', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    table.sort(0 , 'upward');
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25B4;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BF;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[0].cells[1].innerText, '18');
+    assert.equal(tableRows[1].cells[0].innerText, 'Last');
+    assert.equal(tableRows[1].cells[1].innerText, '1');
+    assert.equal(tableRows[2].cells[0].innerText, 'First');
+    assert.equal(tableRows[2].cells[1].innerText, '2');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+
+  test('sortOnNumericColumnAscending', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    table.sort(1 /*, 'downward' */);
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25BF;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BE;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'Last');
+    assert.equal(tableRows[0].cells[1].innerText, '1');
+    assert.equal(tableRows[1].cells[0].innerText, 'First');
+    assert.equal(tableRows[1].cells[1].innerText, '2');
+    assert.equal(tableRows[2].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[2].cells[1].innerText, '18');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+
+  test('sortOnNumericColumnDescending', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    table.sort(1 , 'upward');
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25BF;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25B4;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[0].cells[1].innerText, '18');
+    assert.equal(tableRows[1].cells[0].innerText, 'First');
+    assert.equal(tableRows[1].cells[1].innerText, '2');
+    assert.equal(tableRows[2].cells[0].innerText, 'Last');
+    assert.equal(tableRows[2].cells[1].innerText, '1');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+
+  test('sortOnAColumnThenReverseIt', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    table.sort(0);
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25BE;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BF;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'First');
+    assert.equal(tableRows[0].cells[1].innerText, '2');
+    assert.equal(tableRows[1].cells[0].innerText, 'Last');
+    assert.equal(tableRows[1].cells[1].innerText, '1');
+    assert.equal(tableRows[2].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[2].cells[1].innerText, '18');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+    table.sort(0);
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25B4;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BF;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[0].cells[1].innerText, '18');
+    assert.equal(tableRows[1].cells[0].innerText, 'Last');
+    assert.equal(tableRows[1].cells[1].innerText, '1');
+    assert.equal(tableRows[2].cells[0].innerText, 'First');
+    assert.equal(tableRows[2].cells[1].innerText, '2');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+
+  test('sortOnAColumnThenOnAnotherColumn', function() {
+    var table = SimpleTable();
+    SortableTable.decorate(table);
+    table.sort(0 , 'upward');
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25B4;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BF;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[0].cells[1].innerText, '18');
+    assert.equal(tableRows[1].cells[0].innerText, 'Last');
+    assert.equal(tableRows[1].cells[1].innerText, '1');
+    assert.equal(tableRows[2].cells[0].innerText, 'First');
+    assert.equal(tableRows[2].cells[1].innerText, '2');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+    table.sort(1 /*, 'downward' */);
+    var headerRow = table.tHead.rows[0];
+    assert.equal(convertToHTML(headerRow.cells[0].innerHTML),
+                 'Name&nbsp;&#x25BF;');
+    assert.equal(convertToHTML(headerRow.cells[1].innerHTML),
+                 'Value&nbsp;&#x25BE;');
+    var tableRows = table.tBodies[0].rows;
+    assert.equal(tableRows[0].cells[0].innerText, 'Last');
+    assert.equal(tableRows[0].cells[1].innerText, '1');
+    assert.equal(tableRows[1].cells[0].innerText, 'First');
+    assert.equal(tableRows[1].cells[1].innerText, '2');
+    assert.equal(tableRows[2].cells[0].innerText, 'Middle');
+    assert.equal(tableRows[2].cells[1].innerText, '18');
+    // the footer should never change.
+    var footerRow = table.tFoot.rows[0];
+    assert.equal(footerRow.cells[0].innerText, 'Average');
+    assert.equal(footerRow.cells[1].innerText, '7');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/sunburst_chart.css b/trace-viewer/trace_viewer/base/ui/sunburst_chart.css
new file mode 100644
index 0000000..2595aed
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/sunburst_chart.css
@@ -0,0 +1,20 @@
+/* Copyright 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.
+ */
+.sunburst-chart .arc-text {
+  font-size: 8pt;
+}
+
+.sunburst-chart .label {
+  font-size: 10pt;
+}
+
+.sunburst-chart polyline {
+  fill: none;
+  stroke: black;
+}
+
+.sunburst-chart path {
+  stroke: #fff;
+}
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/base/ui/sunburst_chart.html b/trace-viewer/trace_viewer/base/ui/sunburst_chart.html
new file mode 100644
index 0000000..6a00e46
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/sunburst_chart.html
@@ -0,0 +1,449 @@
+<!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">
+<link rel="import" href="/base/ui/d3.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/chart_base.html">
+<link rel="stylesheet" href="/base/ui/sunburst_chart.css">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.ui', function() {
+  var ChartBase = tv.b.ui.ChartBase;
+  var getColorOfKey = tv.b.ui.getColorOfKey;
+
+  var MIN_RADIUS = 100;
+
+  /**
+   * @constructor
+   */
+  var SunburstChart = tv.b.ui.define('sunburst-chart', ChartBase);
+
+  SunburstChart.prototype = {
+    __proto__: ChartBase.prototype,
+
+    decorate: function() {
+      ChartBase.prototype.decorate.call(this);
+      this.classList.add('sunburst-chart');
+
+      this.data_ = undefined;
+      this.seriesKeys_ = undefined;
+
+      this.yDomainMin_ = 0.0;
+      this.yDomainMax_ = 0.0;
+      this.xDomainScale_ = undefined;
+      this.yDomainScale_ = undefined;
+      this.radius_ = undefined;
+      this.arc_ = undefined;
+      this.selectedNode_ = null;
+      this.vis_ = undefined;
+      this.nodes_ = undefined;
+      this.minX_ = 0.0;
+      this.maxX_ = 1.0;
+      this.minY_ = 0.0;
+      this.clickedY_ = 0;
+
+      var chartAreaSel = d3.select(this.chartAreaElement);
+      this.legendSel_ = chartAreaSel.append('g');
+
+      var pieGroupSel = chartAreaSel.append('g')
+        .attr('class', 'pie-group');
+      this.pieGroup_ = pieGroupSel.node();
+
+      this.backSel_ = pieGroupSel.append('g');
+
+
+      this.pathsGroup_ = pieGroupSel.append('g')
+        .attr('class', 'paths')
+        .node();
+    },
+
+    get data() {
+      return this.data_;
+    },
+
+
+    /**
+     * @param {Data} Data for the chart, where data must be of the
+     * form {category: str, name: str, (size: number or children: [])} .
+     */
+    set data(data) {
+      this.data_ = data;
+      this.updateContents_();
+    },
+
+    get margin() {
+      var margin = {top: 0, right: 0, bottom: 0, left: 0};
+      if (this.chartTitle_)
+        margin.top += 40;
+      return margin;
+    },
+
+    set selectedNodeID(id) {
+      this.zoomToID_(id);
+    },
+
+    get selectedNodeID() {
+      if (this.selectedNode_ != null)
+        return this.selectedNode_.id;
+      return null;
+    },
+
+    get selectedNode() {
+      if (this.selectedNode_ != null)
+        return this.selectedNode_;
+      return null;
+    },
+
+    getMinSize: function() {
+      if (!tv.b.ui.isElementAttachedToDocument(this))
+        throw new Error('Cannot measure when unattached');
+      this.updateContents_();
+
+      var titleWidth = this.querySelector(
+          '#title').getBoundingClientRect().width;
+      var margin = this.margin;
+      var marginWidth = margin.left + margin.right;
+      var marginHeight = margin.top + margin.bottom;
+
+      // TODO(vmiura): Calc this when we're done with layout.
+      return {
+        width: 600,
+        height: 600
+      };
+    },
+
+    getLegendKeys_: function() {
+      // This class creates its own legend, instead of using ChartBase.
+      return undefined;
+    },
+
+    updateScales_: function(width, height) {
+      if (this.data_ === undefined)
+        return;
+    },
+
+    // Interpolate the scales!
+    arcTween_: function(minX, maxX, minY) {
+      var that = this;
+      var xd, yd, yr;
+
+      if (minY > 0) {
+        xd = d3.interpolate(that.xDomainScale_.domain(), [minX, maxX]);
+        yd = d3.interpolate(
+            that.yDomainScale_.domain(), [minY, that.yDomainMax_]);
+        yr = d3.interpolate(that.yDomainScale_.range(), [50, that.radius_]);
+      }
+      else {
+        xd = d3.interpolate(that.xDomainScale_.domain(), [minX, maxX]);
+        yd = d3.interpolate(that.yDomainScale_.domain(),
+                            [that.yDomainMin_, that.yDomainMax_]);
+        yr = d3.interpolate(that.yDomainScale_.range(), [50, that.radius_]);
+      }
+
+      return function(d, i) {
+        return i ? function(t) { return that.arc_(d); }
+            : function(t) {
+              that.xDomainScale_.domain(xd(t));
+              that.yDomainScale_.domain(yd(t)).range(yr(t));
+              return that.arc_(d);
+            };
+      };
+    },
+
+    getNodeById_: function(id) {
+      if (!this.nodes_)
+        return null;
+
+      if (id < 0 || id > this.nodes_.length)
+        return null;
+
+      return this.nodes_[id];
+    },
+
+    zoomOut_: function() {
+      window.history.back();
+    },
+
+    // This function assumes that, till the given depth,
+    // the tree is linear. (i.e, a single string with no branches.)
+    zoomToDepth: function(depth) {
+      var node = this.data_.nodes;
+      while (node.depth !== depth) {
+        if (node.children.length !== 1)
+          throw new Error('zoomToDepth requires the tree to be linear ' +
+                          'till the specified depth.');
+        node = node.children[0];
+      }
+      return this.zoomToID_(node.id);
+    },
+
+    zoomToID_: function(id) {
+      var d = this.getNodeById_(id);
+
+      if (d) {
+        this.clickedY_ = d.y;
+        this.minX_ = d.x;
+        this.maxX_ = d.x + d.dx;
+        this.minY_ = d.y;
+      }
+      else {
+        this.clickedY_ = -1;
+        this.minX_ = 0.0;
+        this.maxX_ = 1.0;
+        this.minY_ = 0.0;
+      }
+
+      this.selectedNode_ = d;
+      this.redrawSegments_(this.minX_, this.maxX_, this.minY_);
+      var path = this.vis_.selectAll('path');
+
+      path.transition()
+        .duration(750)
+        .attrTween('d', this.arcTween_(this.minX_, this.maxX_, this.minY_));
+
+      this.showBreadcrumbs_(d);
+
+      var e = new Event('node-selected');
+      e.node = d;
+      this.dispatchEvent(e);
+    },
+
+    click_: function(d) {
+      if (d3.event.shiftKey) {
+        // Zoom partially onto the selected range
+        var diff_x = (this.maxX_ - this.minX_) * 0.5;
+        this.minX_ = d.x + d.dx * 0.5 - diff_x * 0.5;
+        this.minX_ = this.minX_ < 0.0 ? 0.0 : this.minX_;
+        this.maxX_ = this.minX_ + diff_x;
+        this.maxX_ = this.maxX_ > 1.0 ? 1.0 : this.maxX_;
+        this.minX_ = this.maxX_ - diff_x;
+
+        this.selectedNode_ = d;
+        this.redrawSegments_(this.minX_, this.maxX_, this.minY_);
+
+        var path = this.vis_.selectAll('path');
+        path.transition()
+          .duration(750)
+          .attrTween('d', this.arcTween_(this.minX_, this.maxX_, this.minY_));
+
+        return;
+      }
+
+      this.selectedNodeID = d.id;
+
+      var e = new Event('node-clicked');
+      e.node = d;
+      this.dispatchEvent(e);
+    },
+
+    // Given a node in a partition layout, return an array of all of its
+    // ancestor nodes, highest first, but excluding the root.
+    getAncestors_: function(node) {
+      var path = [];
+      var current = node;
+      while (current.parent) {
+        path.unshift(current);
+        current = current.parent;
+      }
+      return path;
+    },
+
+    showBreadcrumbs_: function(d) {
+      var sequenceArray = this.getAncestors_(d);
+
+      // Fade all the segments.
+      this.vis_.selectAll('path')
+        .style('opacity', function(d) {
+            return sequenceArray.indexOf(d) >= 0 ? 0.7 : 1.0;
+          });
+
+      var e = new Event('node-highlighted');
+      e.node = d;
+      this.dispatchEvent(e);
+
+      //if (this.data_.onNodeHighlighted != undefined)
+      //  this.data_.onNodeHighlighted(this, d);
+    },
+
+    mouseOver_: function(d) {
+      this.showBreadcrumbs_(d);
+    },
+
+    // Restore everything to full opacity when moving off the
+    // visualization.
+    mouseLeave_: function(d) {
+      var that = this;
+      // Hide the breadcrumb trail
+      if (that.selectedNode_ != null)
+        that.showBreadcrumbs_(that.selectedNode_);
+      else {
+        // Deactivate all segments during transition.
+        that.vis_.selectAll('path')
+          .on('mouseover', null);
+
+        // Transition each segment to full opacity and then reactivate it.
+        that.vis_.selectAll('path')
+          .transition()
+          .duration(300)
+          .style('opacity', 1)
+          .each('end', function() {
+              d3.select(that).on('mouseover', function(d) {
+                that.mouseOver_(d);
+              });
+            });
+      }
+    },
+
+    // Update visible segments between new min/max ranges.
+    redrawSegments_: function(minX, maxX, minY) {
+      var that = this;
+      var scale = maxX - minX;
+      var visible_nodes = that.nodes_.filter(function(d) {
+        return d.depth &&
+            (d.y >= minY) &&
+            (d.x < maxX) &&
+            (d.x + d.dx > minX) &&
+            (d.dx / scale > 0.001);
+      });
+      var path = that.vis_.data([that.data_.nodes]).selectAll('path')
+        .data(visible_nodes, function(d) { return d.id; });
+
+      path.enter().insert('svg:path')
+        .attr('d', that.arc_)
+        .attr('fill-rule', 'evenodd')
+        .style('fill', function(dd) { return getColorOfKey(dd.category); })
+        .style('opacity', 1.0)
+        .on('mouseover', function(d) { that.mouseOver_(d); })
+        .on('click', function(d) { that.click_(d); });
+
+      path.exit().remove();
+      return path;
+    },
+
+    updateContents_: function() {
+      ChartBase.prototype.updateContents_.call(this);
+      if (!this.data_)
+        return;
+
+      var that = this;
+
+      // Partition data into d3 nodes.
+      var partition = d3.layout.partition()
+          .size([1, 1])
+          .value(function(d) { return d.size; });
+      that.nodes_ = partition.nodes(that.data_.nodes);
+
+      // Allocate an id to each node.  Gather all categories.
+      var categoryDict = {};
+      that.nodes_.forEach(function f(d, i) {
+        d.id = i;
+        categoryDict[d.category] = null;
+      });
+
+      // Create legend.
+      var li = {
+        w: 85, h: 20, s: 3, r: 3
+      };
+
+      var legend = that.legendSel_.append('svg:svg')
+          .attr('width', li.w)
+          .attr('height', d3.keys(categoryDict).length * (li.h + li.s));
+
+      var g = legend.selectAll('g')
+          .data(d3.keys(categoryDict))
+          .enter().append('svg:g')
+          .attr('transform', function(d, i) {
+            return 'translate(0,' + i * (li.h + li.s) + ')';
+          });
+
+      g.append('svg:rect')
+          .attr('rx', li.r)
+          .attr('ry', li.r)
+          .attr('width', li.w)
+          .attr('height', li.h)
+          .style('fill', function(d) { return getColorOfKey(d); });
+
+      g.append('svg:text')
+          .attr('x', li.w / 2)
+          .attr('y', li.h / 2)
+          .attr('dy', '0.35em')
+          .attr('text-anchor', 'middle')
+          .attr('fill', '#fff')
+          .attr('font-size', '12px')
+          .text(function(d) { return d; });
+
+      // Create sunburst visualization.
+      var width = that.chartAreaSize.width;
+      var height = that.chartAreaSize.height;
+      that.radius_ = Math.max(MIN_RADIUS, Math.min(width, height) / 2);
+
+      d3.select(that.pieGroup_).attr(
+          'transform',
+          'translate(' + width / 2 + ',' + height / 2 + ')');
+
+      that.selectedNode_ = null;
+
+      var depth = 1.0 + d3.max(that.nodes_, function(d) { return d.depth; });
+      that.yDomainMin_ = 1.0 / depth;
+      that.yDomainMax_ = Math.min(Math.max(depth, 20), 50) / depth;
+
+      that.xDomainScale_ = d3.scale.linear()
+          .range([0, 2 * Math.PI]);
+
+      that.yDomainScale_ = d3.scale.sqrt()
+          .domain([that.yDomainMin_, that.yDomainMax_])
+          .range([50, that.radius_]);
+
+      that.arc_ = d3.svg.arc()
+          .startAngle(function(d) {
+            return Math.max(0, Math.min(2 * Math.PI, that.xDomainScale_(d.x)));
+          })
+          .endAngle(function(d) {
+            return Math.max(0,
+                Math.min(2 * Math.PI, that.xDomainScale_(d.x + d.dx)));
+          })
+          .innerRadius(function(d) {
+            return Math.max(0, that.yDomainScale_((d.y)));
+          })
+          .outerRadius(function(d) {
+            return Math.max(0, that.yDomainScale_((d.y + d.dy)));
+          });
+
+
+      // Bounding circle underneath the sunburst, to make it easier to detect
+      // when the mouse leaves the parent g.
+      that.backSel_.append('svg:circle')
+          .attr('r', that.radius_)
+          .style('opacity', 0.0)
+          .on('click', function() { that.zoomOut_(); });
+
+
+      that.vis_ = d3.select(that.pathsGroup_);
+      that.selectedNodeID = 0;
+      that.vis_.on('mouseleave', function(d) { that.mouseLeave_(d); });
+    },
+
+    updateHighlight_: function() {
+      ChartBase.prototype.updateHighlight_.call(this);
+      // Update color of pie segments.
+      var pathsGroupSel = d3.select(this.pathsGroup_);
+      var that = this;
+      pathsGroupSel.selectAll('.arc').each(function(d, i) {
+        var origData = that.data_[i];
+        var highlighted = origData.label == that.currentHighlightedLegendKey;
+        var color = getColorOfKey(origData.label, highlighted);
+        this.style.fill = getColorOfKey(origData.label, highlighted);
+      });
+    }
+  };
+
+  return {
+    SunburstChart: SunburstChart
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/sunburst_chart_test.html b/trace-viewer/trace_viewer/base/ui/sunburst_chart_test.html
new file mode 100644
index 0000000..8a8c41a
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/sunburst_chart_test.html
@@ -0,0 +1,65 @@
+<!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/ui/sunburst_chart.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('simple', function() {
+    var chart = new tv.b.ui.SunburstChart();
+    chart.width = 600;
+    chart.height = 600;
+    assert.equal(chart.getAttribute('width'), '600');
+    assert.equal(chart.getAttribute('height'), '600');
+    chart.chartTitle = 'Chart title';
+    var nodes = {
+      category: 'root',
+      name: '<All Threads>',
+      children: [
+        {
+          category: 'Thread',
+          name: 'Thread 1',
+          children: [
+            {
+              category: 'Chrome',
+              name: 'foo()',
+              children: [
+                {
+                  category: 'Chrome',
+                  name: 'foo()',
+                  size: 150
+                },
+                {
+                  category: 'Chrome',
+                  name: 'bar()',
+                  size: 200
+                }]
+            },
+            {
+              category: 'Chrome',
+              name: 'bar()',
+              size: 200
+            }]
+        },
+        {
+          category: 'Thread',
+          name: 'Thread 2',
+          children: [
+            {
+              category: 'Java',
+              name: 'Java',
+              size: 100
+            }]
+        }]
+    };
+    chart.data = {
+      nodes: nodes
+    };
+    this.addHTMLOutput(chart);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/ui/tool_button.css b/trace-viewer/trace_viewer/base/ui/tool_button.css
new file mode 100644
index 0000000..bb9abd3
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui/tool_button.css
@@ -0,0 +1,17 @@
+/* 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.
+ */
+
+.tool-button {
+  background-position: center center;
+  background-repeat: no-repeat;
+  border-bottom: 1px solid #BCBCBC;
+  border-top: 1px solid #F1F1F1;
+  cursor: pointer;
+  height: 30px;
+}
+
+.tool-button.active {
+  cursor: auto;
+}
diff --git a/trace-viewer/trace_viewer/base/ui_test.html b/trace-viewer/trace_viewer/base/ui_test.html
new file mode 100644
index 0000000..20234d4
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/ui_test.html
@@ -0,0 +1,243 @@
+<!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/ui.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var TestElement = tv.b.ui.define('div');
+  TestElement.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function() {
+      if (!this.decorateCallCount)
+        this.decorateCallCount = 0;
+      this.decorateCallCount++;
+    }
+  };
+
+  var Base = tv.b.ui.define('div');
+  Base.prototype = {
+    __proto__: HTMLDivElement.prototype,
+    decorate: function() {
+      this.decoratedAsBase = true;
+    },
+    set baseProperty(v) {
+      this.basePropertySet = v;
+    }
+  };
+
+  test('decorateOnceViaNew', function() {
+    var testElement = new TestElement();
+    assert.equal(testElement.decorateCallCount, 1);
+  });
+
+  test('decorateOnceDirectly', function() {
+    var testElement = document.createElement('div');
+    tv.b.ui.decorate(testElement, TestElement);
+    assert.equal(testElement.decorateCallCount, 1);
+  });
+
+  test('componentToString', function() {
+    assert.equal(Base.toString(), 'div');
+
+    var Sub = tv.b.ui.define('Sub', Base);
+    assert.equal(Sub.toString(), 'div::sub');
+
+    var SubSub = tv.b.ui.define('Marine', Sub);
+    assert.equal(SubSub.toString(), 'div::sub::marine');
+  });
+
+  test('basicDefines', function() {
+    var baseInstance = new Base();
+    assert.instanceOf(baseInstance, Base);
+    assert.isTrue(baseInstance.decoratedAsBase);
+
+    assert.equal(baseInstance.constructor, Base);
+    assert.equal(baseInstance.constructor.toString(), 'div');
+
+    baseInstance.basePropertySet = 7;
+    assert.equal(baseInstance.basePropertySet, 7);
+  });
+
+  test('subclassing', function() {
+    var Sub = tv.b.ui.define('sub', Base);
+    Sub.prototype = {
+      __proto__: Base.prototype,
+      decorate: function() {
+        this.decoratedAsSub = true;
+      }
+    };
+
+    var subInstance = new Sub();
+    assert.instanceOf(subInstance, Sub);
+    assert.isTrue(subInstance.decoratedAsSub);
+
+    assert.instanceOf(subInstance, Base);
+    assert.isUndefined(subInstance.decoratedAsBase);
+
+    assert.equal(subInstance.constructor, Sub);
+    assert.equal(subInstance.constructor.toString(), 'div::sub');
+
+    subInstance.baseProperty = true;
+    assert.isTrue(subInstance.basePropertySet);
+  });
+
+  var NoArgs = tv.b.ui.define('div');
+  NoArgs.prototype = {
+    __proto__: HTMLDivElement.prototype,
+    decorate: function() {
+      this.noArgsDecorated_ = true;
+    },
+    get noArgsDecorated() {
+      return this.noArgsDecorated_;
+    }
+  };
+
+  var Args = tv.b.ui.define('args', NoArgs);
+  Args.prototype = {
+    __proto__: NoArgs.prototype,
+    decorate: function(first) {
+      this.first_ = first;
+      this.argsDecorated_ = true;
+    },
+    get first() {
+      return this.first_;
+    },
+    get argsDecorated() {
+      return this.argsDecorated_;
+    }
+  };
+
+  var ArgsChild = tv.b.ui.define('args-child', Args);
+  ArgsChild.prototype = {
+    __proto__: Args.prototype,
+    decorate: function(_, second) {
+      this.second_ = second;
+      this.argsChildDecorated_ = true;
+    },
+    get second() {
+      return this.second_;
+    },
+    get decorated() {
+      return this.decorated_;
+    },
+    get argsChildDecorated() {
+      return this.argsChildDecorated_ = true;
+    }
+  };
+
+  var ArgsDecoratingChild = tv.b.ui.define('args-decorating-child', Args);
+  ArgsDecoratingChild.prototype = {
+    __proto__: Args.prototype,
+    decorate: function(first, second) {
+      Args.prototype.decorate.call(this, first);
+      this.second_ = second;
+      this.argsDecoratingChildDecorated_ = true;
+    },
+    get second() {
+      return this.second_;
+    },
+    get decorated() {
+      return this.decorated_;
+    },
+    get argsDecoratingChildDecorated() {
+      return this.argsChildDecorated_ = true;
+    }
+  };
+
+  test('decorate_noArguments', function() {
+    var noArgs;
+    assert.doesNotThrow(function() {
+      noArgs = new NoArgs();
+    });
+    assert.isTrue(noArgs.noArgsDecorated);
+  });
+
+  test('decorate_arguments', function() {
+    var args = new Args('this is first');
+    assert.equal(args.first, 'this is first');
+    assert.isTrue(args.argsDecorated);
+    assert.isUndefined(args.noArgsDecorated);
+  });
+
+  test('decorate_subclassArguments', function() {
+    var argsChild = new ArgsChild('this is first', 'and second');
+    assert.isUndefined(argsChild.first);
+    assert.equal(argsChild.second, 'and second');
+
+    assert.isTrue(argsChild.argsChildDecorated);
+    assert.isUndefined(argsChild.argsDecorated);
+    assert.isUndefined(argsChild.noArgsDecorated);
+  });
+
+  test('decorate_subClassCallsParentDecorate', function() {
+    var argsDecoratingChild = new ArgsDecoratingChild(
+        'this is first', 'and second');
+    assert.equal(argsDecoratingChild.first, 'this is first');
+    assert.equal(argsDecoratingChild.second, 'and second');
+    assert.isTrue(argsDecoratingChild.argsDecoratingChildDecorated);
+    assert.isTrue(argsDecoratingChild.argsDecorated);
+    assert.isUndefined(argsDecoratingChild.noArgsDecorated);
+  });
+
+  test('defineWithNamespace', function() {
+    var svgNS = 'http://www.w3.org/2000/svg';
+    var cls = tv.b.ui.define('svg', undefined, svgNS);
+    cls.prototype = {
+      __proto__: HTMLUnknownElement.prototype,
+
+      decorate: function() {
+        this.setAttribute('width', 200);
+        this.setAttribute('height', 200);
+        this.setAttribute('viewPort', '0 0 200 200');
+        var rectEl = document.createElementNS(svgNS, 'rect');
+        rectEl.setAttribute('x', 10);
+        rectEl.setAttribute('y', 10);
+        rectEl.setAttribute('width', 180);
+        rectEl.setAttribute('height', 180);
+        this.appendChild(rectEl);
+      }
+    };
+    var el = new cls();
+    assert.equal(el.tagName, 'svg');
+    assert.equal(el.namespaceURI, svgNS);
+    this.addHTMLOutput(el);
+  });
+
+  test('defineSubclassWithNamespace', function() {
+    var svgNS = 'http://www.w3.org/2000/svg';
+    var cls = tv.b.ui.define('svg', undefined, svgNS);
+    cls.prototype = {
+      __proto__: HTMLUnknownElement.prototype,
+
+      decorate: function() {
+        this.setAttribute('width', 200);
+        this.setAttribute('height', 200);
+        this.setAttribute('viewPort', '0 0 200 200');
+        var rectEl = document.createElementNS(svgNS, 'rect');
+        rectEl.setAttribute('x', 10);
+        rectEl.setAttribute('y', 10);
+        rectEl.setAttribute('width', 180);
+        rectEl.setAttribute('height', 180);
+        this.appendChild(rectEl);
+      }
+    };
+
+    var subCls = tv.b.ui.define('sub', cls);
+    subCls.prototype = {
+      __proto__: cls.prototype
+    };
+    assert.equal(subCls.toString(), 'svg::sub');
+
+    var el = new subCls();
+    this.addHTMLOutput(el);
+    assert.equal(el.tagName, 'svg');
+    assert.equal(el.namespaceURI, svgNS);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest.html b/trace-viewer/trace_viewer/base/unittest.html
new file mode 100644
index 0000000..97ddef4
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest.html
@@ -0,0 +1,27 @@
+<!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.
+-->
+<script src="/chai/chai.js"></script>
+<script>
+  'use strict';
+  var assert = chai.assert;
+</script>
+
+
+<link rel="import" href="/base/base.html">
+<link rel="import" href="/base/unittest/test_error.html">
+<link rel="import" href="/base/unittest/assertions.html">
+<link rel="import" href="/base/unittest/suite_loader.html">
+<link rel="import" href="/base/unittest/test_case.html">
+<link rel="import" href="/base/unittest/test_suite.html">
+<link rel="import" href="/base/unittest/test_runner.html">
+<script>
+'use strict';
+tv.exportTo('tv.b.unittest', function() {
+  return {
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/assertions.html b/trace-viewer/trace_viewer/base/unittest/assertions.html
new file mode 100644
index 0000000..40f8320
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/assertions.html
@@ -0,0 +1,264 @@
+<!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/unittest/test_error.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  function forAllAssertMethodsIn_(prototype, fn) {
+    for (var fieldName in prototype) {
+      if (fieldName.indexOf('assert') != 0)
+        continue;
+      var fieldValue = prototype[fieldName];
+      if (typeof fieldValue != 'function')
+        continue;
+      fn(fieldName, fieldValue);
+    }
+  }
+
+  var Assertions = {};
+  Assertions.prototype = {
+    assertTrue: function(a, opt_message) {
+      if (a)
+        return;
+      var message = opt_message || 'Expected true, got ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertFalse: function(a, opt_message) {
+      if (!a)
+        return;
+      var message = opt_message || 'Expected false, got ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertUndefined: function(a, opt_message) {
+      if (a === undefined)
+        return;
+      var message = opt_message || 'Expected undefined, got ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertNotUndefined: function(a, opt_message) {
+      if (a !== undefined)
+        return;
+      var message = opt_message || 'Expected not undefined, got ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertNull: function(a, opt_message) {
+      if (a === null)
+        return;
+      var message = opt_message || 'Expected null, got ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertNotNull: function(a, opt_message) {
+      if (a !== null)
+        return;
+      var message = opt_message || 'Expected non-null, got ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertEquals: function(a, b, opt_message) {
+      if (a === b)
+        return;
+      if (opt_message)
+        throw new tv.b.unittest.TestError(opt_message);
+
+      var message = 'Expected\n"';
+      if (typeof(a) === 'string' || a instanceof String)
+        message += a;
+      else {
+        try {
+          message += JSON.stringify(a);
+        } catch (e) {
+          message += a;
+        }
+      }
+
+      message += '"\n\ngot\n\n"';
+      if (typeof(b) === 'string' || b instanceof String)
+        message += b;
+      else {
+        try {
+          message += JSON.stringify(b);
+        } catch (e) {
+          message += b;
+        }
+      }
+
+      message += '"';
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertNotEquals: function(a, b, opt_message) {
+      if (a !== b)
+        return;
+      var message = opt_message || 'Expected something not equal to ' + b;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertArrayEquals: function(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);
+    },
+
+    assertArrayShallowEquals: function(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 ' + b + ', got array ' + a;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertArrayBufferEquals: function(a, b, opt_message) {
+      if (a.byteLength === b.byteLength) {
+        var ok = true;
+        a = new Uint8Array(a);
+        b = new Uint8Array(b);
+        for (var i = 0; i < a.length; i++) {
+          ok &= (a[i] === b[i]);
+        }
+        if (ok)
+          return;
+      }
+
+      var message = opt_message || 'Array buffers mismatch';
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertAlmostEquals: function(a, b, opt_message) {
+      if (Math.abs(a - b) < 0.00001)
+        return;
+      var message = opt_message || 'Expected almost ' + a + ', got ' + b;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertVec2Equals: function(a, b, opt_message) {
+      if (a[0] === b[0] &&
+          a[1] === b[1])
+        return;
+      var message = opt_message || 'Expected (' + a[0] + ',' + a[1] +
+          ') but got (' + b[0] + ',' + b[1] + ')';
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertVec3Equals: function(a, b, opt_message) {
+      if (a[0] === b[0] &&
+          a[1] === b[1] &&
+          a[2] === b[2])
+        return;
+      var message = opt_message || 'Expected ' + vec3.toString(a) +
+          ' but got ' + vec3.toString(b);
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertQuadEquals: function(a, b, opt_message) {
+      var ok = true;
+      ok &= a.p1[0] === b.p1[0] && a.p1[1] === b.p1[1];
+      ok &= a.p2[0] === b.p2[0] && a.p2[1] === b.p2[1];
+      ok &= a.p3[0] === b.p3[0] && a.p3[1] === b.p3[1];
+      ok &= a.p4[0] === b.p4[0] && a.p4[1] === b.p4[1];
+      if (ok)
+        return;
+      var message = opt_message || 'Expected "' + a.toString() +
+          '", got "' + b.toString() + '"';
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertRectEquals: function(a, b, opt_message) {
+      var ok = true;
+      if (a.x === b.x && a.y === b.y &&
+          a.width === b.width && a.height === b.height) {
+        return;
+      }
+
+      var message = opt_message || 'Expected "' + a.toString() +
+          '", got "' + b.toString() + '"';
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertObjectEquals: function(a, b, opt_message) {
+      var a_json = JSON.stringify(a);
+      var b_json = JSON.stringify(b);
+      if (a_json === b_json)
+        return;
+      var message = opt_message || 'Expected ' + a_json + ', got ' + b_json;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertThrows: function(fn, opt_message) {
+      try {
+        fn();
+      } catch (e) {
+        return;
+      }
+      var message = opt_message || 'Expected throw from ' + fn;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertDoesNotThrow: function(fn, opt_message) {
+      try {
+        fn();
+      } catch (e) {
+        var message = opt_message || 'Expected to not throw from ' + fn +
+            ' but got: ' + e;
+        throw new tv.b.unittest.TestError(message);
+      }
+    },
+
+    assertApproxEquals: function(a, b, opt_epsilon, opt_message) {
+      if (a === b)
+        return;
+      var epsilon = opt_epsilon || 0.000001; // 6 digits.
+      a = Math.abs(a);
+      b = Math.abs(b);
+      var relative_error = Math.abs(a - b) / (a + b);
+      if (relative_error < epsilon)
+        return;
+      var message = opt_message || 'Expect ' + a + ' and ' + b +
+          ' to be within ' + epsilon + ' was ' + relative_error;
+      throw new tv.b.unittest.TestError(message);
+    },
+
+    assertVisible: function(elt) {
+      if (!elt.offsetHeight || !elt.offsetWidth)
+        throw new tv.b.unittest.TestError('Expected element to be visible');
+    }
+  };
+
+  function bindGlobals_() {
+    forAllAssertMethodsIn_(Assertions.prototype,
+        function(fieldName, fieldValue) {
+          global[fieldName] = fieldValue.bind(this);
+        });
+  };
+  bindGlobals_();
+
+  return {
+    Assertions: Assertions
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/assertions_test.html b/trace-viewer/trace_viewer/base/unittest/assertions_test.html
new file mode 100644
index 0000000..ce01b7e
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/assertions_test.html
@@ -0,0 +1,296 @@
+<!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/unittest/assertions.html">
+<link rel="import" href="/base/quad.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/base/gl_matrix.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function assertionTestSetup() {
+    global.rawAssertThrows = function(fn) {
+      try {
+        fn();
+      } catch (e) {
+        if (e instanceof tv.b.unittest.TestError)
+          return;
+        throw new Error('Unexpected error from <' + fn + '>: ' + e);
+      }
+      throw new Error('Expected <' + fn + '> to throw');
+    };
+
+    global.rawAssertNotThrows = function(fn) {
+      try {
+        fn();
+      } catch (e) {
+        throw new Error('Expected <' + fn + '> to not throw: ' + e.message);
+      }
+    };
+  }
+
+  function assertionTestTeardown() {
+    global.rawAssertThrows = undefined;
+    global.rawAssertNotThrows = undefined;
+  }
+
+  function assertionTest(name, testFn) {
+    test(name, testFn, {
+      setUp: assertionTestSetup,
+      tearDown: assertionTestTeardown
+    });
+  }
+
+  assertionTest('assertTrue', function() {
+    rawAssertThrows(function() {
+      assertTrue(false);
+    });
+    rawAssertNotThrows(function() {
+      assertTrue(true);
+    });
+  });
+
+  assertionTest('assertFalse', function() {
+    rawAssertThrows(function() {
+      assertFalse(true);
+    });
+    rawAssertNotThrows(function() {
+      assertFalse(false);
+    });
+  });
+
+  assertionTest('assertUndefined', function() {
+    rawAssertThrows(function() {
+      assertUndefined('');
+    });
+    rawAssertNotThrows(function() {
+      assertUndefined(undefined);
+    });
+  });
+
+  assertionTest('assertNotUndefined', function() {
+    rawAssertThrows(function() {
+      assertNotUndefined(undefined);
+    });
+    rawAssertNotThrows(function() {
+      assertNotUndefined('');
+    });
+  });
+
+  assertionTest('assertNull', function() {
+    rawAssertThrows(function() {
+      assertNull('');
+    });
+    rawAssertNotThrows(function() {
+      assertNull(null);
+    });
+  });
+
+  assertionTest('assertNotNull', function() {
+    rawAssertThrows(function() {
+      assertNotNull(null);
+    });
+    rawAssertNotThrows(function() {
+      assertNotNull('');
+    });
+  });
+
+  assertionTest('assertEquals', function() {
+    rawAssertThrows(function() {
+      assertEquals(1, 2);
+    });
+    rawAssertNotThrows(function() {
+      assertEquals(1, 1);
+    });
+
+    try {
+      var f = {};
+      f.foo = f;
+      assertEquals(1, f);
+      throw new tv.b.unittest.TestError('Failed to throw');
+    } catch (e) {
+      assertNotEquals('Converting circular structure to JSON', e.message);
+    }
+
+    try {
+      var f = {};
+      f.foo = f;
+      assertEquals(f, 1);
+      throw new tv.b.unittest.TestError('Failed to throw');
+    } catch (e) {
+      assertNotEquals('Converting circular structure to JSON', e.message);
+    }
+  });
+
+  assertionTest('assertNotEquals', function() {
+    rawAssertThrows(function() {
+      assertNotEquals(1, 1);
+    });
+    rawAssertNotThrows(function() {
+      assertNotEquals(1, 2);
+    });
+  });
+
+  assertionTest('assertArrayEquals', function() {
+    rawAssertThrows(function() {
+      assertArrayEquals([2, 3], [2, 4]);
+    });
+    rawAssertThrows(function() {
+      assertArrayEquals([1], [1, 2]);
+    });
+    rawAssertNotThrows(function() {
+      assertArrayEquals(['a', 'b'], ['a', 'b']);
+    });
+  });
+
+  assertionTest('assertArrayEqualsShallow', function() {
+    rawAssertThrows(function() {
+      assertArrayShallowEquals([2, 3], [2, 4]);
+    });
+    rawAssertThrows(function() {
+      assertArrayShallowEquals([1], [1, 2]);
+    });
+    rawAssertNotThrows(function() {
+      assertArrayShallowEquals(['a', 'b'], ['a', 'b']);
+    });
+  });
+
+  assertionTest('assertAlmostEquals', function() {
+    rawAssertThrows(function() {
+      assertAlmostEquals(1, 0);
+    });
+    rawAssertThrows(function() {
+      assertAlmostEquals(1, 1.000011);
+    });
+
+    rawAssertNotThrows(function() {
+      assertAlmostEquals(1, 1);
+    });
+    rawAssertNotThrows(function() {
+      assertAlmostEquals(1, 1.000001);
+    });
+    rawAssertNotThrows(function() {
+      assertAlmostEquals(1, 1 - 0.000001);
+    });
+  });
+
+  assertionTest('assertVec2Equals', function() {
+    rawAssertThrows(function() {
+      assertVec2Equals(vec2.fromValues(0, 1), vec2.fromValues(0, 2));
+    });
+    rawAssertThrows(function() {
+      assertVec2Equals(vec2.fromValues(1, 2), vec2.fromValues(2, 2));
+    });
+    rawAssertNotThrows(function() {
+      assertVec2Equals(vec2.fromValues(1, 1), vec2.fromValues(1, 1));
+    });
+  });
+
+  assertionTest('assertVec3Equals', function() {
+    rawAssertThrows(function() {
+      assertVec3Equals(vec3.fromValues(0, 1, 2), vec3.fromValues(0, 1, 3));
+    });
+    rawAssertThrows(function() {
+      assertVec3Equals(vec3.fromValues(0, 1, 2), vec3.fromValues(0, 3, 2));
+    });
+    rawAssertThrows(function() {
+      assertVec3Equals(vec3.fromValues(0, 1, 2), vec3.fromValues(3, 1, 2));
+    });
+    rawAssertNotThrows(function() {
+      assertVec3Equals(vec3.fromValues(1, 2, 3), vec3.fromValues(1, 2, 3));
+    });
+  });
+
+  assertionTest('assertQuadEquals', function() {
+    rawAssertThrows(function() {
+      assertQuadEquals(
+          tv.b.Quad.fromXYWH(1, 1, 2, 2), tv.b.Quad.fromXYWH(1, 1, 2, 3));
+    });
+    rawAssertNotThrows(function() {
+      assertQuadEquals(
+          tv.b.Quad.fromXYWH(1, 1, 2, 2), tv.b.Quad.fromXYWH(1, 1, 2, 2));
+    });
+  });
+
+  assertionTest('assertRectEquals', function() {
+    rawAssertThrows(function() {
+      assertRectEquals(
+          tv.b.Rect.fromXYWH(1, 1, 2, 2), tv.b.Rect.fromXYWH(1, 1, 2, 3));
+    });
+    rawAssertNotThrows(function() {
+      assertRectEquals(
+          tv.b.Rect.fromXYWH(1, 1, 2, 2), tv.b.Rect.fromXYWH(1, 1, 2, 2));
+    });
+  });
+
+  assertionTest('assertObjectEquals', function() {
+    rawAssertThrows(function() {
+      assertObjectEquals({a: 1}, {a: 2});
+    });
+    rawAssertThrows(function() {
+      assertObjectEquals({a: 1}, []);
+    });
+    rawAssertThrows(function() {
+      assertObjectEquals({a: 1, b: {}}, {a: 1, c: {}, b: {}});
+    });
+    rawAssertNotThrows(function() {
+      assertObjectEquals({}, {});
+    });
+    rawAssertNotThrows(function() {
+      assertObjectEquals({a: 1}, {a: 1});
+    });
+  });
+
+  assertionTest('assertThrows', function() {
+    rawAssertThrows(function() {
+      assertThrows(function() {
+      });
+    });
+    rawAssertNotThrows(function() {
+      assertThrows(function() {
+        throw new Error('expected_error');
+      });
+    });
+  });
+
+  assertionTest('assertDoesNotThrow', function() {
+    rawAssertThrows(function() {
+      assertDoesNotThrow(function() {
+        throw new Error('expected_error');
+      });
+    });
+    rawAssertNotThrows(function() {
+      assertDoesNotThrow(function() {
+      });
+    });
+  });
+
+  assertionTest('assertApproxEquals', function() {
+    rawAssertThrows(function() {
+      assertApproxEquals(1, 5, 0.5);
+    });
+    rawAssertNotThrows(function() {
+      assertApproxEquals(1, 2, 1);
+    });
+  });
+
+  assertionTest('assertVisible', function() {
+    rawAssertThrows(function() {
+      assertVisible({});
+    });
+    rawAssertThrows(function() {
+      assertVisible({offsetHeight: 0});
+    });
+    rawAssertThrows(function() {
+      assertVisible({offsetWidth: 0});
+    });
+    rawAssertNotThrows(function() {
+      assertVisible({offsetWidth: 1, offsetHeight: 1});
+    });
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/common.css b/trace-viewer/trace_viewer/base/unittest/common.css
new file mode 100644
index 0000000..6a40a02
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/common.css
@@ -0,0 +1,32 @@
+/* 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.
+ */
+
+.unittest-pending {
+  color: orange;
+}
+.unittest-running {
+  color: orange;
+  font-weight: bold;
+}
+
+.unittest-passed {
+  color: darkgreen;
+}
+
+.unittest-failed {
+  color: darkred;
+  font-weight: bold;
+}
+
+.unittest-exception {
+  color: red;
+  font-weight: bold;
+}
+
+.unittest-failure {
+  border: 1px solid grey;
+  border-radius: 5px;
+  padding: 5px;
+}
diff --git a/trace-viewer/trace_viewer/base/unittest/constants.html b/trace-viewer/trace_viewer/base/unittest/constants.html
new file mode 100644
index 0000000..709ddb5
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/constants.html
@@ -0,0 +1,28 @@
+<!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.b.unittest', function() {
+  var TestStatus = {
+    PENDING: 'pending-status',
+    RUNNING: 'running-status',
+    DONE_RUNNING: 'done-running-status'
+  };
+
+  var TestTypes = {
+    UNITTEST: 'unittest-type',
+    PERFTEST: 'perftest-type'
+  };
+
+  return {
+    TestStatus: TestStatus,
+    TestTypes: TestTypes
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/html_test_results.html b/trace-viewer/trace_viewer/base/unittest/html_test_results.html
new file mode 100644
index 0000000..d178b2b
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/html_test_results.html
@@ -0,0 +1,425 @@
+<!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/events.html">
+<link rel="import" href="/base/utils.html">
+<link rel="import" href="/base/unittest/constants.html">
+<link rel="import" href="/base/ui.html">
+<link rel="stylesheet" href="/base/unittest/common.css">
+<style>
+  x-tv.b.unittest-test-resultsbase
+    display: -webkit-flex;
+    -webkit-flex-direction: column;
+  }
+
+  x-tv.b.unittest-test-results > x-html-test-case-result.dark {
+    background-color: #eee;
+  }
+
+  x-html-test-case-result {
+    display: block;
+  }
+  x-html-test-case-result > #title,
+  x-html-test-case-result > #status,
+  x-html-test-case-result > #details > x-html-test-case-error > #message,
+  x-html-test-case-result > #details > x-html-test-case-error > #stack,
+  x-html-test-case-result > #details > x-html-test-case-error > #return-value {
+    -webkit-user-select: auto;
+  }
+
+  x-html-test-case-result > #details > x-html-test-case-error {
+    display: block;
+    border: 1px solid grey;
+    border-radius: 5px;
+    font-family: monospace;
+    margin-bottom: 14px;
+  }
+
+  x-html-test-case-result > #details > x-html-test-case-error > #message,
+  x-html-test-case-result > #details > x-html-test-case-error > #stack {
+    white-space: pre;
+  }
+
+  x-html-test-case-result > #details > x-html-test-case-html-result {
+    display: block;
+  }
+
+</style>
+<template id="x-html-test-case-result-template">
+  <span id="title"></span> <span id="status"></span> <span id="return-value"></span>
+  <div id="details"></div>
+</template>
+
+<template id="x-html-test-case-error-template">
+  <div id="stack"></div>
+</template>
+
+<script>
+'use strict';
+tv.exportTo('tv.b.unittest', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  var TestStatus = tv.b.unittest.TestStatus;
+  var TestTypes = tv.b.unittest.TestTypes;
+
+  /**
+   * @constructor
+   */
+  var HTMLTestCaseResult = tv.b.ui.define('x-html-test-case-result');
+
+  HTMLTestCaseResult.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.appendChild(tv.b.instantiateTemplate(
+          '#x-html-test-case-result-template', THIS_DOC));
+      this.testCase_ = undefined;
+      this.testCaseHRef_ = undefined;
+      this.duration_ = undefined;
+      this.testStatus_ = TestStatus.PENDING;
+      this.testReturnValue_ = undefined;
+      this.showHTMLOutput_ = false;
+      this.updateColorAndStatus_();
+    },
+
+    get showHTMLOutput() {
+      return this.showHTMLOutput_;
+    },
+
+    set showHTMLOutput(showHTMLOutput) {
+      this.showHTMLOutput_ = showHTMLOutput;
+      this.updateHTMLOutputDisplayState_();
+    },
+
+    get testCase() {
+      return this.testCase_;
+    },
+
+    set testCase(testCase) {
+      this.testCase_ = testCase;
+      this.updateTitle_();
+    },
+
+    get testCaseHRef() {
+      return this.testCaseHRef_;
+    },
+
+    set testCaseHRef(href) {
+      this.testCaseHRef_ = href;
+      this.updateTitle_();
+    },
+    updateTitle_: function() {
+      var titleEl = this.querySelector('#title');
+      if (this.testCase_ === undefined) {
+        titleEl.textContent = '';
+        return;
+      }
+
+      if (this.testCaseHRef_) {
+        titleEl.innerHTML = '<a href="' + this.testCaseHRef_ + '">' +
+            this.testCase_.fullyQualifiedName + '</a>';
+      } else {
+        titleEl.textContent = this.testCase_.fullyQualifiedName;
+      }
+    },
+
+    addError: function(normalizedException) {
+      var errorEl = document.createElement('x-html-test-case-error');
+      errorEl.appendChild(tv.b.instantiateTemplate(
+          '#x-html-test-case-error-template', THIS_DOC));
+      errorEl.querySelector('#stack').textContent = normalizedException.stack;
+      this.querySelector('#details').appendChild(errorEl);
+      this.updateColorAndStatus_();
+    },
+
+    addHTMLOutput: function(element) {
+      var htmlResultEl = document.createElement('x-html-test-case-html-result');
+      htmlResultEl.appendChild(element);
+      this.querySelector('#details').appendChild(htmlResultEl);
+
+      var bounds = element.getBoundingClientRect();
+      assert(bounds.width !== 0, 'addHTMLOutput element as 0 width');
+      assert(bounds.height !== 0, 'addHTMLOutput element has 0 height');
+    },
+
+    updateHTMLOutputDisplayState_: function() {
+      var htmlResults = this.querySelectorAll('x-html-test-case-html-result');
+      var display;
+      if (this.showHTMLOutput)
+        display = '';
+      else
+        display = (this.testStatus_ == TestStatus.RUNNING) ? '' : 'none';
+      for (var i = 0; i < htmlResults.length; i++)
+        htmlResults[i].style.display = display;
+    },
+
+    get hadErrors() {
+      return !!this.querySelector('x-html-test-case-error');
+    },
+
+    get duration() {
+      return this.duration_;
+    },
+
+    set duration(duration) {
+      this.duration_ = duration;
+      this.updateColorAndStatus_();
+    },
+
+    get testStatus() {
+      return this.testStatus_;
+    },
+
+    set testStatus(testStatus) {
+      this.testStatus_ = testStatus;
+      this.updateColorAndStatus_();
+      this.updateHTMLOutputDisplayState_();
+    },
+
+    updateColorAndStatus_: function() {
+      var colorCls;
+      var status;
+      if (this.hadErrors) {
+        colorCls = 'unittest-failed';
+        status = 'failed';
+      } else if (this.testStatus_ == TestStatus.PENDING) {
+        colorCls = 'unittest-pending';
+        status = 'pending';
+      } else if (this.testStatus_ == TestStatus.RUNNING) {
+        colorCls = 'unittest-running';
+        status = 'running';
+      } else { // DONE_RUNNING and no errors
+        colorCls = 'unittest-passed';
+        status = 'passed';
+      }
+
+      var statusEl = this.querySelector('#status');
+      if (this.duration_)
+        statusEl.textContent = status + ' (' +
+            this.duration_.toFixed(2) + 'ms)';
+      else
+        statusEl.textContent = status;
+      statusEl.className = colorCls;
+    },
+
+    get testReturnValue() {
+      return this.testReturnValue_;
+    },
+
+    set testReturnValue(testReturnValue) {
+      this.testReturnValue_ = testReturnValue;
+      this.querySelector('#return-value').textContent = testReturnValue;
+    }
+  };
+
+
+
+
+  /**
+   * @constructor
+   */
+  var HTMLTestResults = tv.b.ui.define('x-tv.b.unittest-test-results');
+
+  HTMLTestResults.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.currentTestCaseStartTime_ = undefined;
+      this.totalRunTime_ = 0;
+      this.numTestsThatPassed_ = 0;
+      this.numTestsThatFailed_ = 0;
+      this.showHTMLOutput_ = false;
+      this.showPendingAndPassedTests_ = false;
+      this.linkifyCallback_ = undefined;
+      this.headless_ = false;
+    },
+
+    get headless() {
+      return this.headless_;
+    },
+
+    set headless(headless) {
+      this.headless_ = headless;
+    },
+
+    getHRefForTestCase: function(testCase) {
+      /* Override this to create custom links */
+      return undefined;
+    },
+
+    get showHTMLOutput() {
+      return this.showHTMLOutput_;
+    },
+
+    set showHTMLOutput(showHTMLOutput) {
+      this.showHTMLOutput_ = showHTMLOutput;
+      var testCaseResults = this.querySelectorAll('x-html-test-case-result');
+      for (var i = 0; i < testCaseResults.length; i++)
+        testCaseResults[i].showHTMLOutput = showHTMLOutput;
+    },
+
+    get showPendingAndPassedTests() {
+      return this.showPendingAndPassedTests_;
+    },
+
+    set showPendingAndPassedTests(showPendingAndPassedTests) {
+      this.showPendingAndPassedTests_ = showPendingAndPassedTests;
+
+      var testCaseResults = this.querySelectorAll('x-html-test-case-result');
+      for (var i = 0; i < testCaseResults.length; i++)
+        this.updateDisplayStateForResult_(testCaseResults[i]);
+    },
+
+    updateDisplayStateForResult_: function(res) {
+      var display;
+      if (this.showPendingAndPassedTests_) {
+        if (res.testStatus == TestStatus.RUNNING ||
+            res.hadErrors) {
+          display = '';
+        } else {
+          display = 'none';
+        }
+      } else {
+        display = '';
+      }
+      res.style.display = display;
+
+      // This bit of mess gives res objects a dark class based on whether their
+      // last visible sibling was not dark. It relies on the
+      // updateDisplayStateForResult_ being called on all previous siblings of
+      // an element before being called on the element itself. Yay induction.
+      var dark;
+      if (!res.previousSibling) {
+        dark = true;
+      } else {
+        var lastVisible;
+        for (var cur = res.previousSibling;
+             cur;
+             cur = cur.previousSibling) {
+          if (cur.style.display == '') {
+            lastVisible = cur;
+            break;
+          }
+        }
+        if (lastVisible) {
+          dark = !lastVisible.classList.contains('dark');
+        } else {
+          dark = true;
+        }
+      }
+
+      if (dark)
+        res.classList.add('dark');
+      else
+        res.classList.remove('dark');
+    },
+
+    willRunTest: function(testCase) {
+      this.currentTestCaseResult_ = new HTMLTestCaseResult();
+      this.currentTestCaseResult_.showHTMLOutput = this.showHTMLOutput_;
+      this.currentTestCaseResult_.testCase = testCase;
+      var href = this.getHRefForTestCase(testCase);
+      if (href)
+        this.currentTestCaseResult_.testCaseHRef = href;
+      this.currentTestCaseResult_.testStatus = TestStatus.RUNNING;
+      this.currentTestCaseStartTime_ = window.performance.now();
+      this.appendChild(this.currentTestCaseResult_);
+      this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+      this.log_(testCase.fullyQualifiedName + ': ');
+    },
+
+    addErrorForCurrentTest: function(error) {
+      this.log_('\n');
+
+      var normalizedException = tv.b.normalizeException(error);
+      this.log_('Exception: ' + normalizedException.message + '\n' +
+          normalizedException.stack);
+
+      this.currentTestCaseResult_.addError(normalizedException);
+      this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+      if (this.headless_)
+        this.notifyTestResultToDevServer_('EXCEPT', normalizedException.stack);
+    },
+
+    addHTMLOutputForCurrentTest: function(element) {
+      this.currentTestCaseResult_.addHTMLOutput(element);
+      this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+    },
+
+    setReturnValueFromCurrentTest: function(returnValue) {
+      this.currentTestCaseResult_.testReturnValue = returnValue;
+    },
+
+    didCurrentTestEnd: function() {
+      var testCaseResult = this.currentTestCaseResult_;
+      var testCaseDuration = window.performance.now() -
+          this.currentTestCaseStartTime_;
+      this.currentTestCaseResult_.testStatus = TestStatus.DONE_RUNNING;
+      testCaseResult.duration = testCaseDuration;
+      this.totalRunTime_ += testCaseDuration;
+      if (testCaseResult.hadErrors) {
+        this.log_('[FAILED]\n');
+        this.numTestsThatFailed_ += 1;
+        tv.b.dispatchSimpleEvent(this, 'testfailed');
+      } else {
+        this.log_('[PASSED]\n');
+        this.numTestsThatPassed_ += 1;
+        tv.b.dispatchSimpleEvent(this, 'testpassed');
+      }
+
+      if (this.headless_) {
+        this.notifyTestResultToDevServer_(testCaseResult.hadErrors ?
+                                          'FAILED' : 'PASSED');
+      }
+
+      this.updateDisplayStateForResult_(this.currentTestCaseResult_);
+      this.currentTestCaseResult_ = undefined;
+    },
+
+    didRunTests: function() {
+      this.log_('[DONE]\n');
+      if (this.headless_)
+        this.notifyTestCompletionToDevServer_();
+    },
+
+    getStats: function() {
+      return {
+        numTestsThatPassed: this.numTestsThatPassed_,
+        numTestsThatFailed: this.numTestsThatFailed_,
+        totalRunTime: this.totalRunTime_
+      };
+    },
+
+    notifyTestResultToDevServer_: function(result, extra_msg) {
+      var req = new XMLHttpRequest();
+      var testName = this.currentTestCaseResult_.testCase.fullyQualifiedName;
+      req.open('POST', '/test_automation/notify_test_result', true);
+      req.send(result + '  ' + testName + ' ' + (extra_msg || ''));
+    },
+
+    notifyTestCompletionToDevServer_: function() {
+      if (this.numTestsThatPassed_ + this.numTestsThatFailed_ == 0)
+        return;
+      var data = this.numTestsThatFailed_ == 0 ? 'ALL_PASSED' : 'HAD_FAILURES';
+      data += '\nPassed tests: ' + this.numTestsThatPassed_ +
+              '  Failed tests: ' + this.numTestsThatFailed_;
+
+      var req = new XMLHttpRequest();
+      req.open('POST', '/test_automation/notify_completion', true);
+      req.send(data);
+    },
+
+    log_: function(msg) {
+      //this.textContent += msg;
+      tv.b.dispatchSimpleEvent(this, 'statschange');
+    }
+  };
+
+  return {
+    HTMLTestResults: HTMLTestResults
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/interactive_test_runner.html b/trace-viewer/trace_viewer/base/unittest/interactive_test_runner.html
new file mode 100644
index 0000000..7044aab
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/interactive_test_runner.html
@@ -0,0 +1,433 @@
+<!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/unittest.html">
+<link rel="import" href="/base/unittest/suite_loader.html">
+<link rel="import" href="/base/unittest/test_runner.html">
+<link rel="import" href="/base/unittest/html_test_results.html">
+<link rel="stylesheet" href="/base/unittest/common.css">
+<style>
+  x-base-interactive-test-runner {
+    display: -webkit-flex;
+    -webkit-flex-direction: column;
+  }
+
+  x-base-interactive-test-runner > * {
+    -webkit-flex: 0 0 auto;
+  }
+  x-base-interactive-test-runner > #title {
+    font-size: 16pt;
+  }
+
+  x-base-interactive-test-runner {
+    font-family: sans-serif;
+  }
+
+  x-base-interactive-test-runner > h1 {
+    margin: 5px 0px 10px 0px;
+  }
+
+  x-base-interactive-test-runner > #stats {
+  }
+
+  x-base-interactive-test-runner > #controls {
+    display: block;
+    margin-bottom: 5px;
+  }
+
+  x-base-interactive-test-runner > #controls > ul {
+    list-style-type: none;
+    padding: 0;
+    margin: 0;
+  }
+
+  x-base-interactive-test-runner > #controls > ul > li {
+    float: left;
+    margin-right: 10px;
+    padding-top: 5px;
+    padding-bottom: 5px;
+  }
+
+  x-base-interactive-test-runner > #shortform-results {
+    word-break: break-all;
+    height; 40px;
+  }
+
+  x-base-interactive-test-runner > #results-container {
+    -webkit-flex: 1 1 auto;
+    min-height: 0;
+    overflow: auto;
+    padding: 0 4px 0 4px;
+  }
+</style>
+
+<template id="x-base-interactive-test-runner-template">
+  <h1 id="title">Tests</h1>
+  <div id="stats"></div>
+  <div id="controls">
+    <ul id="links">
+    </ul>
+    <div style="clear: both;"></div>
+
+    <div>
+      <span>
+        <input type="radio" name="test-type-to-run" value="unit" />
+        Run unit tests
+      </span>
+      <span>
+        <input type="radio" name="test-type-to-run" value="perf" />
+        Run perf tests
+      </span>
+      <span>
+        <input type="radio" name="test-type-to-run" value="all" />
+        Run all tests
+      </span>
+    </div>
+    <span>
+      <input type="checkbox" id="short-format" /> Short format
+    </span>
+  </div>
+  <div id="shortform-results">
+  </div>
+  <div id="results-container">
+  </div>
+</template>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+  var ALL_TEST_TYPES = 'all';
+
+  /**
+   * @constructor
+   */
+  var InteractiveTestRunner = tv.b.ui.define('x-base-interactive-test-runner');
+
+  InteractiveTestRunner.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.allTests_ = undefined;
+
+      this.suppressStateChange_ = false;
+
+      this.testFilterString_ = '';
+      this.testTypeToRun_ = tv.b.unittest.TestTypes.UNITTEST;
+      this.shortFormat_ = false;
+      this.testSuiteName_ = '';
+
+      this.rerunPending_ = false;
+      this.runner_ = undefined;
+      this.results_ = undefined;
+      this.headless_ = false;
+
+      this.onResultsStatsChanged_ = this.onResultsStatsChanged_.bind(this);
+      this.onTestFailed_ = this.onTestFailed_.bind(this);
+      this.onTestPassed_ = this.onTestPassed_.bind(this);
+
+
+      this.appendChild(tv.b.instantiateTemplate(
+          '#x-base-interactive-test-runner-template', THIS_DOC));
+
+      this.querySelector(
+          'input[name=test-type-to-run][value=unit]').checked = true;
+      var testTypeToRunEls = tv.b.asArray(this.querySelectorAll(
+          'input[name=test-type-to-run]'));
+
+      testTypeToRunEls.forEach(
+          function(inputEl) {
+            inputEl.addEventListener(
+                'click', this.onTestTypeToRunClick_.bind(this));
+          }, this);
+
+      var shortFormatEl = this.querySelector('#short-format');
+      shortFormatEl.checked = this.shortFormat_;
+      shortFormatEl.addEventListener(
+          'click', this.onShortFormatClick_.bind(this));
+      this.updateShortFormResultsDisplay_();
+
+      // Oh, DOM, how I love you. Title is such a convenient property name and I
+      // refuse to change my worldview because of tooltips.
+      this.__defineSetter__(
+          'title',
+          function(title) {
+            this.querySelector('#title').textContent = title;
+          });
+    },
+
+    get allTests() {
+      return this.allTests_;
+    },
+
+    set allTests(allTests) {
+      this.allTests_ = allTests;
+      this.scheduleRerun_();
+    },
+
+    get testLinks() {
+      return this.testLinks_;
+    },
+    set testLinks(testLinks) {
+      this.testLinks_ = testLinks;
+      var linksEl = this.querySelector('#links');
+      linksEl.textContent = '';
+      this.testLinks_.forEach(function(l) {
+        var link = document.createElement('a');
+        link.href = l.linkPath;
+        link.textContent = l.title;
+
+        var li = document.createElement('li');
+        li.appendChild(link);
+
+        linksEl.appendChild(li);
+      }, this);
+    },
+
+    get testFilterString() {
+      return this.testFilterString_;
+    },
+
+    set testFilterString(testFilterString) {
+      this.testFilterString_ = testFilterString;
+      this.scheduleRerun_();
+      if (!this.suppressStateChange_)
+        tv.b.dispatchSimpleEvent(this, 'statechange');
+    },
+
+    get shortFormat() {
+      return this.shortFormat_;
+    },
+
+    set shortFormat(shortFormat) {
+      this.shortFormat_ = shortFormat;
+      this.querySelector('#short-format').checked = shortFormat;
+      if (this.results_)
+        this.results_.shortFormat = shortFormat;
+      if (!this.suppressStateChange_)
+        tv.b.dispatchSimpleEvent(this, 'statechange');
+    },
+
+    onShortFormatClick_: function(e) {
+      this.shortFormat_ = this.querySelector('#short-format').checked;
+      this.updateShortFormResultsDisplay_();
+      this.updateResultsGivenShortFormat_();
+      if (!this.suppressStateChange_)
+        tv.b.dispatchSimpleEvent(this, 'statechange');
+    },
+
+    updateShortFormResultsDisplay_: function() {
+      var display = this.shortFormat_ ? '' : 'none';
+      this.querySelector('#shortform-results').style.display = display;
+    },
+
+    updateResultsGivenShortFormat_: function() {
+      if (!this.results_)
+        return;
+
+      if (this.testFilterString_.length || this.testSuiteName_.length)
+        this.results_.showHTMLOutput = true;
+      else
+        this.results_.showHTMLOutput = false;
+      this.results_.showPendingAndPassedTests = this.shortFormat_;
+    },
+
+    get testTypeToRun() {
+      return this.testTypeToRun_;
+    },
+
+    set testTypeToRun(testTypeToRun) {
+      this.testTypeToRun_ = testTypeToRun;
+      var sel;
+      switch (testTypeToRun) {
+        case tv.b.unittest.TestTypes.UNITTEST:
+          sel = 'input[name=test-type-to-run][value=unit]';
+          break;
+        case tv.b.unittest.TestTypes.PERFTEST:
+          sel = 'input[name=test-type-to-run][value=perf]';
+          break;
+        case ALL_TEST_TYPES:
+          sel = 'input[name=test-type-to-run][value=all]';
+          break;
+        default:
+          throw new Error('Invalid test type to run: ' + testTypeToRun);
+      }
+      this.querySelector(sel).checked = true;
+      this.scheduleRerun_();
+      if (!this.suppressStateChange_)
+        tv.b.dispatchSimpleEvent(this, 'statechange');
+    },
+
+    onTestTypeToRunClick_: function(e) {
+      switch (e.target.value) {
+        case 'unit':
+          this.testTypeToRun_ = tv.b.unittest.TestTypes.UNITTEST;
+          break;
+        case 'perf':
+          this.testTypeToRun_ = tv.b.unittest.TestTypes.PERFTEST;
+          break;
+        case 'all':
+          this.testTypeToRun_ = ALL_TEST_TYPES;
+          break;
+        default:
+          throw new Error('Inalid test type: ' + e.target.value);
+      }
+
+      this.scheduleRerun_();
+      if (!this.suppressStateChange_)
+        tv.b.dispatchSimpleEvent(this, 'statechange');
+    },
+
+    onTestPassed_: function() {
+      this.querySelector('#shortform-results').textContent += '.';
+    },
+
+    onTestFailed_: function() {
+      this.querySelector('#shortform-results').textContent += 'F';
+    },
+
+    onResultsStatsChanged_: function() {
+      var statsEl = this.querySelector('#stats');
+      var stats = this.results_.getStats();
+      var numTestsOverall = this.runner_.testCases.length;
+      var numTestsThatRan = stats.numTestsThatPassed + stats.numTestsThatFailed;
+      statsEl.innerHTML =
+          '<span>' + numTestsThatRan + '/' + numTestsOverall +
+          '</span> tests run, ' +
+          '<span class="unittest-failed">' + stats.numTestsThatFailed +
+          '</span> failures, ' +
+          ' in ' + stats.totalRunTime.toFixed(2) + 'ms.';
+    },
+
+    scheduleRerun_: function() {
+      if (this.rerunPending_)
+        return;
+      if (this.runner_) {
+        this.rerunPending_ = true;
+        this.runner_.beginToStopRunning();
+        var doRerun = function() {
+          this.rerunPending_ = false;
+          this.scheduleRerun_();
+        }.bind(this);
+        this.runner_.runCompletedPromise.then(
+            doRerun, doRerun);
+        return;
+      }
+      this.beginRunning_();
+    },
+
+    beginRunning_: function() {
+      var resultsContainer = this.querySelector('#results-container');
+      if (this.results_) {
+        this.results_.removeEventListener('testpassed',
+                                          this.onTestPassed_);
+        this.results_.removeEventListener('testfailed',
+                                          this.onTestFailed_);
+        this.results_.removeEventListener('statschange',
+                                          this.onResultsStatsChanged_);
+        delete this.results_.getHRefForTestCase;
+        resultsContainer.removeChild(this.results_);
+      }
+
+      this.results_ = new tv.b.unittest.HTMLTestResults();
+      this.results_.headless = this.headless_;
+      this.results_.getHRefForTestCase = this.getHRefForTestCase.bind(this);
+      this.updateResultsGivenShortFormat_();
+
+      this.results_.shortFormat = this.shortFormat_;
+      this.results_.addEventListener('testpassed',
+                                     this.onTestPassed_);
+      this.results_.addEventListener('testfailed',
+                                     this.onTestFailed_);
+      this.results_.addEventListener('statschange',
+                                     this.onResultsStatsChanged_);
+      resultsContainer.appendChild(this.results_);
+
+      var tests = this.allTests_.filter(function(test) {
+        var i = test.fullyQualifiedName.indexOf(this.testFilterString_);
+        if (i == -1)
+          return false;
+        if (this.testTypeToRun_ !== ALL_TEST_TYPES &&
+            test.testType !== this.testTypeToRun_)
+          return false;
+        return true;
+      }, this);
+
+      this.runner_ = new tv.b.unittest.TestRunner(this.results_, tests);
+      this.runner_.beginRunning();
+
+      this.runner_.runCompletedPromise.then(
+          this.runCompleted_.bind(this),
+          this.runCompleted_.bind(this));
+    },
+
+    setState: function(state, opt_suppressStateChange) {
+      this.suppressStateChange_ = true;
+      if (state.testFilterString !== undefined)
+        this.testFilterString = state.testFilterString;
+      else
+        this.testFilterString = '';
+
+      if (state.shortFormat === undefined)
+        this.shortFormat = false;
+      else
+        this.shortFormat = state.shortFormat;
+
+      if (state.testTypeToRun === undefined)
+        this.testTypeToRun = tv.b.unittest.TestTypes.UNITTEST;
+      else
+        this.testTypeToRun = state.testTypeToRun;
+
+      this.testSuiteName_ = state.testSuiteName || '';
+      this.headless_ = state.headless || false;
+
+      if (!opt_suppressStateChange)
+        this.suppressStateChange_ = false;
+
+      this.onShortFormatClick_();
+      this.scheduleRerun_();
+      this.suppressStateChange_ = false;
+    },
+
+    getDefaultState: function() {
+      return {
+        testFilterString: '',
+        testSuiteName: '',
+        shortFormat: false,
+        testTypeToRun: tv.b.unittest.TestTypes.UNITTEST
+      };
+    },
+
+    getState: function() {
+      return {
+        testFilterString: this.testFilterString_,
+        testSuiteName: this.testSuiteName_,
+        shortFormat: this.shortFormat_,
+        testTypeToRun: this.testTypeToRun_
+      };
+    },
+
+    getHRefForTestCase: function(testCases) {
+      return undefined;
+    },
+
+    runCompleted_: function() {
+      this.runner_ = undefined;
+      if (this.results_.getStats().numTestsThatFailed > 0) {
+        this.querySelector('#shortform-results').textContent +=
+            '[THERE WERE FAILURES]';
+      } else {
+        this.querySelector('#shortform-results').textContent += '[DONE]';
+      }
+    }
+  };
+
+  return {
+    InteractiveTestRunner: InteractiveTestRunner
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/module_test_case_runner.html b/trace-viewer/trace_viewer/base/unittest/module_test_case_runner.html
new file mode 100644
index 0000000..122225f
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/module_test_case_runner.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<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.
+-->
+<head>
+  <title>trace-viewer/module_test_case_backend.html</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+  <script src="/components/webcomponentsjs/webcomponents.js"></script>
+
+  <link rel="import" href="/components/polymer/polymer.html">
+  <link rel="import" href="/base/unittest.html">
+  <link rel="import" href="/base/unittest/text_test_results.html">
+  <link rel="import" href="/base/unittest/suite_loader.html">
+  <link rel="import" href="/base/unittest/test_runner.html">
+</head>
+<body>
+  <h1 id="status">
+  </h1>
+  <script>
+    'use strict';
+    /**
+     * Polled by tv.module_test_case
+     */
+    window.__readyToRun = false;
+
+    var statusEl = document.querySelector('#status');
+    function discoverTestsInModules(testModuleNames) {
+      statusEl.textContent = 'Discovering tests...';
+      var loader = new tv.b.unittest.SuiteLoader(testModuleNames);
+      return loader.allSuitesLoadedPromise.then(function() {
+        return loader.getAllTests().filter(function(test) {
+          return test.testType == tv.b.unittest.TestTypes.UNITTEST;
+        }).map(function(test) {
+          statusEl.textContent = 'Idle';
+          return test.fullyQualifiedName;
+        });
+      });
+    }
+
+    function runTestNamed(fullyQualifiedTestName) {
+      statusEl.textContent = 'Running ' + fullyQualifiedTestName;
+      function cleanup() {
+        statusEl.textContent = '';
+      }
+      return _runTestNamedImpl(fullyQualifiedTestName).then(
+        cleanup, cleanup);
+    }
+
+    function _runTestNamedImpl(fullyQualifiedTestName) {
+      var p = tv.b.unittest.TestCase.parseFullyQualifiedName(
+          fullyQualifiedTestName);
+      var suiteNameToLoad = p.suiteName;
+      var testCaseNameToRun = p.testCaseName;
+
+      var runTestResolver;
+      var runTestPromise = new Promise(function(resolve, reject) {
+        runTestResolver = {
+          resolve: resolve,
+          reject: reject
+        };
+      });
+
+      var loader = new tv.b.unittest.SuiteLoader([suiteNameToLoad]);
+      loader.allSuitesLoadedPromise.then(
+        beginRunningTestCase,
+        loadSuiteFailed);
+
+      var results = new tv.b.unittest.TextTestResults();
+      function loadSuiteFailed(e) {
+        var normalizedException = tv.b.normalizeException(e);
+        runTestResolver.reject(e);
+      }
+
+      function beginRunningTestCase() {
+        var testCase = loader.findTestWithFullyQualifiedName(
+            fullyQualifiedTestName);
+        var runner = new tv.b.unittest.TestRunner(results, [testCase]);
+        runner.beginRunning();
+        return runner.runCompletedPromise.then(
+          runTestCaseComplete,
+          runTestCaseComplete);
+      }
+
+      function runTestCaseComplete() {
+        if (results.numTestsThatFailed === 0) {
+          runTestResolver.resolve();
+          return;
+        }
+
+        runTestResolver.reject(results.buffer);
+      }
+      return runTestPromise;
+    }
+
+    window.addEventListener('load', function() {
+      window.__readyToRun = true;
+    });
+  </script>
+</body>
+</html>
diff --git a/trace-viewer/trace_viewer/base/unittest/suite_loader.html b/trace-viewer/trace_viewer/base/unittest/suite_loader.html
new file mode 100644
index 0000000..2012f22
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/suite_loader.html
@@ -0,0 +1,239 @@
+<!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/event_target.html">
+<link rel="import" href="/base/xhr.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/base/unittest/test_suite.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  function TestLink(linkPath, title) {
+    this.linkPath = linkPath;
+    this.title = title;
+  }
+
+  function SuiteLoader(opt_suiteNamesToLoad) {
+    tv.b.EventTarget.call(this);
+
+    this.testSuiteGUIDs_ = {};
+    this.testSuites = [];
+    this.testLinks = [];
+
+    if (opt_suiteNamesToLoad) {
+      this.allSuitesLoadedPromise = this.beginLoadingModules_(
+          opt_suiteNamesToLoad);
+    } else {
+      var p;
+      p = tv.b.getAsync('/tv/json/tests');
+      p = p.then(
+          function(data) {
+            var testMetadata = JSON.parse(data);
+            var testModuleNames = testMetadata.test_module_names;
+            return this.beginLoadingModules_(testModuleNames, testMetadata);
+          }.bind(this));
+      this.allSuitesLoadedPromise = p;
+    }
+  }
+
+  function loadModule(moduleName) {
+    return new Promise(function(resolve, reject) {
+      var parts = moduleName.split('.');
+      var href = '/' + parts.join('/') + '.html';
+
+      var importEl = document.createElement('link');
+      importEl.moduleName = moduleName;
+      importEl.setAttribute('rel', 'import');
+      importEl.setAttribute('href', href);
+      tv.doc.head.appendChild(importEl);
+
+      importEl.addEventListener('load', function() {
+        resolve(importEl);
+      });
+      importEl.addEventListener('error', function(e) {
+        reject('Error loading &#60;link rel="import" href="' + href + '"');
+      });
+    });
+  }
+
+  function loadModules(moduleNames) {
+    var promises = moduleNames.map(function(moduleName) {
+      return loadModule(moduleName);
+    });
+
+  }
+
+  SuiteLoader.prototype = {
+    __proto__: tv.b.EventTarget.prototype,
+
+    beginLoadingModules_: function(testModuleNames, opt_testMetadata) {
+      if (opt_testMetadata) {
+        var testMetadata = opt_testMetadata;
+        for (var i = 0; i < testMetadata.test_links.length; i++) {
+          var tl = testMetadata.test_links[i];
+          this.testLinks.push(new TestLink(tl['path'],
+                                           tl['title']));
+        }
+      }
+
+      // Hooks!
+      this.bindGlobalHooks_();
+
+      // Load the modules.
+      var modulePromises = [];
+      for (var i = 0; i < testModuleNames.length; i++) {
+        var p = loadModule(testModuleNames[i]);
+        p = p.then(
+            function(importEl) {
+              this.didImportElementGetLoaded_(importEl);
+            }.bind(this));
+            p.x = 7;
+        modulePromises.push(p);
+      }
+
+      var allModulesLoadedPromise = new Promise(function(resolve, reject) {
+        var remaining = modulePromises.length;
+        var resolved = false;
+        function oneMoreLoaded() {
+          if (resolved)
+            return;
+          remaining--;
+          if (remaining > 0)
+            return;
+          resolved = true;
+          resolve();
+        }
+
+        function oneRejected(e) {
+          if (resolved)
+            return;
+          resolved = true;
+          reject(e);
+        }
+
+        modulePromises.forEach(function(modulePromise) {
+          modulePromise.then(oneMoreLoaded, oneRejected);
+        });
+      });
+
+      // Script errors errors abort load;
+      var scriptErrorPromise = new Promise(function(xresolve, xreject) {
+        this.scriptErrorPromiseResolver_ = {
+          resolve: xresolve,
+          reject: xreject
+        };
+      }.bind(this));
+      var donePromise = Promise.race([
+        allModulesLoadedPromise,
+        scriptErrorPromise
+      ]);
+
+      // Cleanup.
+      return donePromise.then(
+        function() {
+          this.scriptErrorPromiseResolver_ = undefined;
+          this.unbindGlobalHooks_();
+        }.bind(this),
+        function(e) {
+          this.scriptErrorPromiseResolver_ = undefined;
+          this.unbindGlobalHooks_();
+          throw e;
+        }.bind(this));
+    },
+
+    bindGlobalHooks_: function() {
+      this.oldWindowOnError_ = window.onerror;
+      window.onerror = function(errorMsg, url, lineNumber) {
+        this.scriptErrorPromiseResolver_.reject(
+            new Error(errorMsg + '\n' + url + ':' + lineNumber));
+        if (this.oldWindowOnError_)
+          return this.oldWindowOnError_(errorMsg, url, lineNumber);
+        return false;
+      }.bind(this);
+    },
+
+    unbindGlobalHooks_: function() {
+      window.onerror = this.oldWindowOnError_;
+      this.oldWindowOnError_ = undefined;
+    },
+
+    didImportElementGetLoaded_: function(importEl) {
+      // The global tv.testSute function stashes test suites
+      // onto the _tv array.
+      var importDoc = importEl.import;
+      var suites = allTestSuitesByModuleURL[importDoc.URL];
+      suites.forEach(function(testSuite) {
+        if (this.testSuiteGUIDs_[testSuite.guid])
+          return;
+        this.testSuiteGUIDs_[testSuite.guid] = true;
+        this.testSuites.push(testSuite);
+
+        var e = new Event('suite-loaded');
+        e.testSuite = testSuite;
+        this.dispatchEvent(e);
+      }, this);
+    },
+
+    getAllTests: function() {
+      var tests = [];
+      this.testSuites.forEach(function(suite) {
+        tests.push.apply(tests, suite.tests);
+      });
+      return tests;
+    },
+
+    findTestWithFullyQualifiedName: function(fullyQualifiedName) {
+      for (var i = 0; i < this.testSuites.length; i++) {
+        var suite = this.testSuites[i];
+        for (var j = 0; j < suite.tests.length; j++) {
+          var test = suite.tests[j];
+          if (test.fullyQualifiedName == fullyQualifiedName)
+            return test;
+        }
+      }
+      throw new Error('Test ' + fullyQualifiedName +
+                      'not found amongst ' + this.testSuites.length);
+    }
+  };
+
+  var allTestSuitesByModuleURL = [];
+
+  function _guessModuleNameFromURL(url) {
+    var m = /.+?:\/\/.+?(\/.+)/.exec(url);
+    if (!m)
+      throw new Error('Guessing module name failed');
+    var path = m[1];
+    if (path[0] != '/')
+      throw new Error('malformed path');
+    if (path.substring(path.length - 5) != '.html')
+      throw new Error('Cannot define testSuites outside html imports');
+    var parts = path.substring(1, path.length - 5).split('/');
+    return parts.join('.');
+  }
+
+  function testSuite(suiteConstructor) {
+    var linkDoc = document.currentScript.ownerDocument;
+    var url = linkDoc.URL;
+    var name = _guessModuleNameFromURL(url);
+    if (!document.currentScript)
+      throw new Error('Cannot call testSuite except during load.');
+
+    var testSuite = new tv.b.unittest.TestSuite(
+      name, suiteConstructor);
+
+
+    if (allTestSuitesByModuleURL[url] === undefined)
+      allTestSuitesByModuleURL[url] = [];
+    allTestSuitesByModuleURL[url].push(testSuite);
+  }
+
+  return {
+    SuiteLoader: SuiteLoader,
+    testSuite: testSuite
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/test_case.html b/trace-viewer/trace_viewer/base/unittest/test_case.html
new file mode 100644
index 0000000..4f79e80
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/test_case.html
@@ -0,0 +1,87 @@
+<!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/unittest/constants.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  var TestTypes = tv.b.unittest.TestTypes;
+
+  function TestCase(suite, testType, name, test, options) {
+    this.guid_ = tv.b.GUID.allocate();
+    this.suite_ = suite;
+    this.testType_ = testType;
+    this.name_ = name;
+
+    this.options_ = options;
+
+    this.test_ = test;
+  }
+
+  TestCase.parseFullyQualifiedName = function(fqn) {
+    var i = fqn.lastIndexOf('.');
+    if (i == -1)
+      throw new Error('FullyQualifiedNames must have a period in them');
+    return {
+      suiteName: fqn.substr(0, i),
+      testCaseName: fqn.substr(i + 1)
+    };
+  };
+
+  TestCase.prototype = {
+    __proto__: Object.prototype,
+
+    get guid() {
+      return this.guid;
+    },
+
+    get suite() {
+      return this.suite_;
+    },
+
+    get testType() {
+      return this.testType_;
+    },
+
+    get name() {
+      return this.name_;
+    },
+
+    get fullyQualifiedName() {
+      return this.suite_.name + '.' + this.name_;
+    },
+
+    get options() {
+      return this.options_;
+    },
+
+    run: function(htmlHook) {
+      return this.test_();
+    },
+
+    // TODO(nduca): The routing of this is a bit awkward. Probably better
+    // to install a global function.
+    addHTMLOutput: function(element) {
+      tv.b.unittest.addHTMLOutputForCurrentTest(element);
+    }
+  };
+
+  function PerfTestCase(suite, name, test, options) {
+    TestCase.call(this, suite, TestTypes.PERFTEST, name, test, options);
+  }
+
+  PerfTestCase.prototype = {
+    __proto__: TestCase.prototype
+  };
+
+  return {
+    TestCase: TestCase,
+    PerfTestCase: PerfTestCase
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/test_case_test.html b/trace-viewer/trace_viewer/base/unittest/test_case_test.html
new file mode 100644
index 0000000..9d9f06c
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/test_case_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="/base/unittest/test_case.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('parseFullyQualifiedName', function() {
+    var p = tv.b.unittest.TestCase.parseFullyQualifiedName('foo.bar');
+    assert.equal(p.suiteName, 'foo');
+    assert.equal(p.testCaseName, 'bar');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/test_error.html b/trace-viewer/trace_viewer/base/unittest/test_error.html
new file mode 100644
index 0000000..4fd07f6
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/test_error.html
@@ -0,0 +1,27 @@
+<!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.b.unittest', function() {
+  function TestError(opt_message) {
+    var that = new Error(opt_message);
+    Error.captureStackTrace(that, TestError);
+    that.__proto__ = TestError.prototype;
+    return that;
+  }
+
+  TestError.prototype = {
+    __proto__: Error.prototype
+  };
+
+  return {
+    TestError: TestError
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/test_runner.html b/trace-viewer/trace_viewer/base/unittest/test_runner.html
new file mode 100644
index 0000000..874b7ba
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/test_runner.html
@@ -0,0 +1,208 @@
+<!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/raf.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  var realTvOnAnimationFrameError;
+  var realWindowOnError;
+  var realWindowHistoryPushState;
+
+  function installGlobalTestHooks(runner) {
+    realTvOnAnimationFrameError = tv.b.onAnimationFrameError;
+    tv.b.onAnimationFrameError = function(error) {
+      runner.results.addErrorForCurrentTest(error);
+    }
+
+    realWindowOnError = window.onerror;
+    window.onerror = function(errorMsg, url, lineNumber) {
+      runner.results.addErrorForCurrentTest(
+          errorMsg + ' at ' + url + ':' + lineNumber);
+      if (realWindowOnError)
+        return realWindowOnError(errorMsg, url, lineNumber);
+      return false;
+    }
+
+    realWindowHistoryPushState = window.history.pushState;
+    window.history.pushState = function() {
+    };
+
+    tv.b.unittest.addHTMLOutputForCurrentTest = function(element) {
+      runner.results.addHTMLOutputForCurrentTest(element);
+    }
+
+    global.sessionStorage.clear();
+    var e = new Event('tv-unittest-will-run');
+    document.head.dispatchEvent(e);
+  }
+
+  function uninstallGlobalTestHooks() {
+    window.onerror = realWindowOnError;
+    realWindowOnError = undefined;
+
+    tv.b.onAnimationFrameError = realTvOnAnimationFrameError;
+    realTvOnAnimationFrameError = undefined;
+
+    window.history.pushState = realWindowHistoryPushState;
+    realWindowHistoryPushState = undefined;
+
+    tv.b.unittest.addHTMLOutputForCurrentTest = undefined;
+  }
+
+
+  function TestRunner(results, testCases) {
+    this.results_ = results;
+    this.testCases_ = testCases;
+    this.pendingTestCases_ = [];
+
+    this.runOneTestCaseScheduled_ = false;
+
+    this.runCompletedPromise = undefined;
+    this.runCompletedResolver_ = undefined;
+
+    this.currentTestCase_ = undefined;
+  }
+
+  TestRunner.prototype = {
+    __proto__: Object.prototype,
+
+    beginRunning: function() {
+      if (this.pendingTestCases_.length)
+        throw new Error('Tests still running!');
+
+      this.runCompletedPromise = new Promise(function(resolve, reject) {
+        this.runCompletedResolver_ = {
+          resolve: resolve,
+          reject: reject
+        };
+      }.bind(this));
+
+      this.pendingTestCases_ = this.testCases_.slice(0);
+
+      this.scheduleRunOneTestCase_();
+
+      return this.runCompletedPromise;
+    },
+
+    beginToStopRunning: function() {
+      if (!this.runCompletedResolver_)
+        throw new Error('Still running');
+      this.pendingTestCases_ = [];
+      return this.runCompletedPromise;
+    },
+
+    get testCases() {
+      return this.testCases_;
+    },
+
+    get results() {
+      return this.results_;
+    },
+
+    scheduleRunOneTestCase_: function() {
+      if (this.runOneTestCaseScheduled_)
+        return;
+      this.runOneTestCaseScheduled_ = true;
+      tv.b.requestIdleCallback(this.runOneTestCase_, this);
+    },
+
+    runOneTestCase_: function() {
+      this.runOneTestCaseScheduled_ = false;
+
+      if (this.pendingTestCases_.length == 0) {
+        this.didFinishRunningAllTests_();
+        return;
+      }
+
+      this.currentTestCase_ = this.pendingTestCases_.splice(0, 1)[0];
+
+      this.results_.willRunTest(this.currentTestCase_);
+      if (!this.setUpCurrentTestCase_()) {
+        this.results_.didCurrentTestEnd();
+        this.currentTestCase_ = undefined;
+        this.scheduleRunOneTestCase_();
+        return;
+      }
+
+      this.runCurrentTestCase_().then(
+          function pass(result) {
+            this.tearDownCurrentTestCase_(true);
+            if (result)
+              this.results_.setReturnValueFromCurrentTest(result);
+            this.results_.didCurrentTestEnd();
+            this.currentTestCase_ = undefined;
+            this.scheduleRunOneTestCase_();
+          }.bind(this),
+          function fail(error) {
+            this.results_.addErrorForCurrentTest(error);
+            this.tearDownCurrentTestCase_(false);
+            this.results_.didCurrentTestEnd();
+            this.currentTestCase_ = undefined;
+            this.scheduleRunOneTestCase_();
+          }.bind(this));
+    },
+
+    setUpCurrentTestCase_: function() {
+      // Try setting it up. Return true if succeeded.
+      installGlobalTestHooks(this);
+      try {
+        if (this.currentTestCase_.options_.setUp)
+          this.currentTestCase_.options_.setUp.call(this.currentTestCase_);
+      } catch (error) {
+        this.results_.addErrorForCurrentTest(error);
+        return false;
+      }
+      return true;
+    },
+
+    runCurrentTestCase_: function() {
+      return new Promise(function(resolve, reject) {
+        try {
+          var maybePromise = this.currentTestCase_.run();
+        } catch (error) {
+          reject(error);
+          return;
+        }
+
+        if (maybePromise !== undefined && maybePromise.then) {
+          maybePromise.then(
+              function(result) {
+                resolve(result);
+              },
+              function(error) {
+                reject(error);
+              });
+        } else {
+          resolve(maybePromise);
+        }
+      }.bind(this));
+    },
+
+    tearDownCurrentTestCase_: function() {
+      try {
+        if (this.currentTestCase_.tearDown)
+          this.currentTestCase_.tearDown.call(this.currentTestCase_);
+      } catch (error) {
+        this.results_.addErrorForCurrentTest(error);
+      }
+
+      uninstallGlobalTestHooks();
+    },
+
+    didFinishRunningAllTests_: function() {
+      this.results.didRunTests();
+      this.runCompletedResolver_.resolve();
+      this.runCompletedResolver_ = undefined;
+    }
+  };
+
+  return {
+    TestRunner: TestRunner
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/test_suite.html b/trace-viewer/trace_viewer/base/unittest/test_suite.html
new file mode 100644
index 0000000..b5bb0a8
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/test_suite.html
@@ -0,0 +1,118 @@
+<!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/unittest/test_case.html">
+<link rel="import" href="/base/unittest/constants.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  var TestCase = tv.b.unittest.TestCase;
+  var PerfTestCase = tv.b.unittest.PerfTestCase;
+
+  var TestTypes = tv.b.unittest.TestTypes;
+
+  function TestSuite(name, suiteConstructor) {
+    this.guid = tv.b.GUID.allocate();
+    this.name_ = name;
+    this.tests_ = [];
+    this.testNames_ = {}; // For dupe checking.
+
+    global.test = function(name, test, options) {
+      if (test == undefined)
+        throw new Error('Must provide test');
+      options = options || {};
+      var testName = name;
+      // If the test cares about DPI settings then we first push a test
+      // that fakes the DPI as the low or hi Dpi version, depending on what
+      // we're current using.
+      if (options.dpiAware) {
+        var defaultDevicePixelRatio = window.devicePixelRatio;
+        var dpi = defaultDevicePixelRatio > 1 ? 1 : 2;
+
+        var testWrapper = function() {
+          window.devicePixelRatio = dpi;
+          try {
+            test.bind(this).call();
+          } finally {
+            window.devicePixelRatio = defaultDevicePixelRatio;
+          }
+        };
+
+        var newName = name;
+        if (dpi === 1) {
+          newName += '_loDPI';
+          testName += '_hiDPI';
+        } else {
+          newName += '_hiDPI';
+          testName += '_loDPI';
+        }
+
+        this.addTest(new TestCase(this, TestTypes.UNITTEST, newName,
+                                  testWrapper, options || {}));
+      }
+
+      this.addTest(new TestCase(this, TestTypes.UNITTEST, testName,
+                                test, options || {}));
+    }.bind(this);
+
+    global.perfTest = function(name, test, options) {
+      this.addTest(new PerfTestCase(this, name, test, options || {}));
+    }.bind(this);
+
+    global.timedPerfTest = function(name, test, options) {
+      if (options === undefined || options.iterations === undefined)
+        throw new Error('timedPerfTest must have iteration option provided.');
+
+      var testWrapper = function(results) {
+        results = [];
+        var durationSum = 0;
+        for (var i = 0; i < options.iterations; ++i) {
+          var start = window.performance.now();
+          test.call(this);
+          var duration = window.performance.now() - start;
+          durationSum += duration;
+          results.push(duration.toFixed(2) + 'ms');
+        }
+        var average = durationSum / options.iterations;
+        return results.join(', ') + ' [avg ' + average.toFixed(2) + 'ms]';
+      };
+
+      this.addTest(new PerfTestCase(this, name, testWrapper, options));
+    }.bind(this);
+
+    suiteConstructor.call();
+
+    global.test = undefined;
+    global.perfTest = undefined;
+    global.timedPerfTest = undefined;
+  }
+
+  TestSuite.prototype = {
+    __proto__: Object.prototype,
+
+    get tests() {
+      return this.tests_;
+    },
+
+    addTest: function(test) {
+      if (this.testNames_[test.name] !== undefined)
+        throw new Error('Test name already used');
+      this.testNames_[test.name] = true;
+      this.tests_.push(test);
+    },
+
+    get name() {
+      return this.name_;
+    }
+  };
+
+  return {
+    TestSuite: TestSuite
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest/text_test_results.html b/trace-viewer/trace_viewer/base/unittest/text_test_results.html
new file mode 100644
index 0000000..f90ed8e
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest/text_test_results.html
@@ -0,0 +1,81 @@
+<!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/utils.html">
+<link rel="import" href="/base/unittest/constants.html">
+<link rel="import" href="/base/ui.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b.unittest', function() {
+  /**
+   * @constructor
+   */
+  function TextTestResults() {
+    this.bufferParts_ = [];
+    this.numTestsThatPassed_ = 0;
+    this.numTestsThatFailed_ = 0;
+    this.currentTestCaseHadErrors_ = false;
+    this.curHTMLOutput_ = [];
+  }
+
+  TextTestResults.prototype = {
+    get buffer() {
+      return this.bufferParts_.join('');
+    },
+
+    get numTestsThatFailed() {
+      return this.numTestsThatFailed_;
+    },
+
+    get numTestsThatPassed() {
+      return this.numTestsThatPassed_;
+    },
+
+    willRunTest: function(testCase) {
+      this.currentTestCaseHadErrors_ = false;
+    },
+
+    addErrorForCurrentTest: function(error) {
+      var normalizedException = tv.b.normalizeException(error);
+      this.writeToBuffer('Exception: ' + normalizedException.message + '\n' +
+          normalizedException.stack + '\n');
+      this.currentTestCaseHadErrors_ = true;
+    },
+
+    addHTMLOutputForCurrentTest: function(element) {
+      this.curHTMLOutput_.push(element);
+      document.body.appendChild(element);
+    },
+
+    setReturnValueFromCurrentTest: function(returnValue) {
+      this.writeToBuffer('[RESULT] ' + JSON.stringify(returnValue) + '\n');
+    },
+
+    didCurrentTestEnd: function() {
+      for (var i = 0; i < this.curHTMLOutput_.length; i++)
+        document.body.removeChild(this.curHTMLOutput_[i]);
+      this.curHTMLOutput_ = [];
+
+      if (this.currentTestCaseHadErrors_)
+        this.numTestsThatFailed_ += 1;
+      else
+        this.numTestsThatPassed_ += 1;
+    },
+
+    didRunTests: function() {
+    },
+
+    writeToBuffer: function(msg) {
+      this.bufferParts_.push(msg);
+    }
+  };
+
+  return {
+    TextTestResults: TextTestResults
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/unittest_test.html b/trace-viewer/trace_viewer/base/unittest_test.html
new file mode 100644
index 0000000..3f8b183
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/unittest_test.html
@@ -0,0 +1,48 @@
+<!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/unittest.html">
+<link rel="import" href="/base/raf.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+
+  test('promise', function() {
+    return new Promise(function(resolve, reject) {
+      resolve();
+    });
+  });
+
+  test('async', function() {
+    return new Promise(function(resolve) {
+      tv.b.requestAnimationFrame(function() {
+        resolve();
+      });
+    });
+  });
+
+  /* To test failures remove comments
+  test('fail', function() {
+    assert.equal(true, false);
+  });
+
+  test('rejected-promise', function() {
+    return new Promise(function(resolve, reject){
+      reject("Failure by rejection");
+    });
+  });
+
+   test('promise-that-throws-after-resolver', function() {
+    return new Promise(function(resolve, rejet){
+      throw new Error('blah');
+    });
+  });
+
+  */
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/utils.html b/trace-viewer/trace_viewer/base/utils.html
new file mode 100644
index 0000000..7770909
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/utils.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="/base/iteration_helpers.html">
+<link rel="import" href="/base/rect.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.b', function() {
+  /**
+   * Adds a {@code getInstance} static method that always return the same
+   * instance object.
+   * @param {!Function} ctor The constructor for the class to add the static
+   *     method to.
+   */
+  function addSingletonGetter(ctor) {
+    ctor.getInstance = function() {
+      return ctor.instance_ || (ctor.instance_ = new ctor());
+    };
+  }
+
+  function instantiateTemplate(selector, doc) {
+    doc = doc || document;
+    var el = doc.querySelector(selector);
+    if (!el)
+      throw new Error('Element not found');
+    return el.createInstance();
+  }
+
+  function tracedFunction(fn, name, opt_this) {
+    function F() {
+      console.time(name);
+      try {
+        fn.apply(opt_this, arguments);
+      } finally {
+        console.timeEnd(name);
+      }
+    }
+    return F;
+  }
+
+  function normalizeException(e) {
+    if (typeof(e) == 'string') {
+      return {
+        message: e,
+        stack: ['<unknown>']
+      };
+    }
+
+    return {
+      message: e.message,
+      stack: e.stack ? e.stack : ['<unknown>']
+    };
+  }
+
+  function stackTrace() {
+    var stack = new Error().stack + '';
+    stack = stack.split('\n');
+    return stack.slice(2);
+  }
+
+  function windowRectForElement(element) {
+    var position = [element.offsetLeft, element.offsetTop];
+    var size = [element.offsetWidth, element.offsetHeight];
+    var node = element.offsetParent;
+    while (node) {
+      position[0] += node.offsetLeft;
+      position[1] += node.offsetTop;
+      node = node.offsetParent;
+    }
+    return tv.b.Rect.fromXYWH(position[0], position[1], size[0], size[1]);
+  }
+
+  function clamp(x, lo, hi) {
+    return Math.min(Math.max(x, lo), hi);
+  }
+
+  function lerp(percentage, lo, hi) {
+    var range = hi - lo;
+    return lo + percentage * range;
+  }
+
+  function deg2rad(deg) {
+    return (Math.PI * deg) / 180.0;
+  }
+
+  function scrollIntoViewIfNeeded(el) {
+    var pr = el.parentElement.getBoundingClientRect();
+    var cr = el.getBoundingClientRect();
+    if (cr.top < pr.top) {
+      el.scrollIntoView(true);
+    } else if (cr.bottom > pr.bottom) {
+      el.scrollIntoView(false);
+    }
+  }
+
+  function getUsingPath(path, from_dict) {
+    var parts = path.split('.');
+    var cur = from_dict;
+
+    for (var part; parts.length && (part = parts.shift());) {
+      if (!parts.length) {
+        return cur[part];
+      } else if (part in cur) {
+        cur = cur[part];
+      } else {
+        return undefined;
+      }
+    }
+    return undefined;
+  }
+
+  return {
+    addSingletonGetter: addSingletonGetter,
+
+    tracedFunction: tracedFunction,
+    normalizeException: normalizeException,
+    instantiateTemplate: instantiateTemplate,
+    stackTrace: stackTrace,
+
+    windowRectForElement: windowRectForElement,
+
+    scrollIntoViewIfNeeded: scrollIntoViewIfNeeded,
+
+    clamp: clamp,
+    lerp: lerp,
+    deg2rad: deg2rad,
+
+    getUsingPath: getUsingPath
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/utils_test.html b/trace-viewer/trace_viewer/base/utils_test.html
new file mode 100644
index 0000000..6a54a3f
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/utils_test.html
@@ -0,0 +1,79 @@
+<!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/utils.html">
+<polymer-element name="instantiate-template-polymer-element-test">
+  <template></template>
+  <script>
+  'use strict';
+  Polymer({
+    testProperty: 'Test'
+  });
+  </script>
+</polymer-element>
+
+<template id="instantiate-template-polymer-test">
+  <instantiate-template-polymer-element-test>
+  </instantiate-template-polymer-element-test>
+</template>
+
+<template id="multiple-template-test">
+  <template>
+    <instantiate-template-polymer-element-test>
+    </instantiate-template-polymer-element-test>
+    <span test-attribute='TestAttribute'>Foo</span>
+  </template>
+  <instantiate-template-polymer-element-test>
+  </instantiate-template-polymer-element-test>
+</template>
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var THIS_DOC = document._currentScript.ownerDocument;
+
+  test('clamping', function() {
+    assert.equal(tv.b.clamp(2, 1, 3), 2);
+    assert.equal(tv.b.clamp(1, 1, 3), 1);
+    assert.equal(tv.b.clamp(0, 1, 3), 1);
+    assert.equal(tv.b.clamp(3, 1, 3), 3);
+    assert.equal(tv.b.clamp(4, 1, 3), 3);
+  });
+
+  test('getUsingPath', function() {
+    var z = tv.b.getUsingPath('x.y.z', {'x': {'y': {'z': 3}}});
+    assert.equal(z, 3);
+
+    var w = tv.b.getUsingPath('x.w', {'x': {'y': {'z': 3}}});
+    assert.isUndefined(w);
+  });
+
+  test('instantiateTemplatePolymer', function() {
+    var e = tv.b.instantiateTemplate(
+                '#instantiate-template-polymer-test',
+                THIS_DOC);
+    assert.equal(e.children.length, 1);
+    assert.equal(e.children[0].testProperty, 'Test');
+  });
+
+  test('instantiateTemplateMultipleTemplates', function() {
+    var outerElement = tv.b.instantiateTemplate(
+                           '#multiple-template-test',
+                           THIS_DOC);
+    assert.equal(outerElement.children.length, 2);
+    assert.equal(outerElement.children[1].testProperty, 'Test');
+
+    // Make sure we can still instantiate inner templates, if we need them.
+    var innerElement = outerElement.children[0].createInstance();
+    assert.equal(innerElement.children.length, 2);
+    assert.equal(innerElement.children[0].testProperty, 'Test');
+    assert.equal(
+        innerElement.children[1].getAttribute('test-attribute'),
+        'TestAttribute');
+    assert.equal(innerElement.children[1].textContent, 'Foo');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/base/xhr.html b/trace-viewer/trace_viewer/base/xhr.html
new file mode 100644
index 0000000..1129c67
--- /dev/null
+++ b/trace-viewer/trace_viewer/base/xhr.html
@@ -0,0 +1,39 @@
+<!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.b', function() {
+  var THIS_DOC = document._currentScript.ownerDocument;
+
+  function getAsync(url, cb) {
+    return new Promise(function(resolve, reject) {
+      var req = new XMLHttpRequest();
+      req.open('GET', url, true);
+      req.onreadystatechange = function(aEvt) {
+        if (req.readyState == 4) {
+          window.setTimeout(function() {
+            if (req.status == 200) {
+              resolve(req.responseText);
+            } else {
+              reject(new Error('XHR failed with status ' + req.status));
+            }
+          }, 0);
+        }
+      };
+      req.send(null);
+    });
+  }
+
+  return {
+    getAsync: getAsync
+  };
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/build/__init__.py b/trace-viewer/trace_viewer/build/__init__.py
new file mode 100644
index 0000000..96196cf
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/__init__.py
@@ -0,0 +1,3 @@
+# 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.
diff --git a/trace-viewer/trace_viewer/build/benchmarks.py b/trace-viewer/trace_viewer/build/benchmarks.py
new file mode 100644
index 0000000..de8369c
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/benchmarks.py
@@ -0,0 +1,75 @@
+# 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.
+import cProfile, pstats, StringIO
+import inspect
+import optparse
+import sys
+
+from trace_viewer import trace_viewer_project
+
+class Bench(object):
+  def SetUp(self):
+    pass
+  def Run(self):
+    pass
+  def TearDown(self):
+    pass
+
+class CalcDepsBench(Bench):
+  def Run(self):
+    project = trace_viewer_project.TraceViewerProject()
+    load_sequence = project.CalcLoadSequenceForAllModules()
+
+class FindAllModuleFilenamesBench(Bench):
+  def Run(self):
+    project = trace_viewer_project.TraceViewerProject()
+    filenames = project.FindAllModuleFilenames()
+
+class DoGenerate(Bench):
+  def SetUp():
+    self.project = trace_viewer_project.TraceViewerProject()
+    self.load_sequence = project.CalcLoadSequenceForAllModules()
+
+  def Run(self):
+    self.deps = generate.GenerateDepsJS(
+      self.load_sequence, self.project)
+
+
+def Main(args):
+  parser = optparse.OptionParser()
+  parser.add_option('--repeat-count', type='int',
+                    default=10)
+  options, args = parser.parse_args(args)
+
+  benches = [g for g in globals().values()
+             if g != Bench and inspect.isclass(g) and Bench in inspect.getmro(g)]
+  if len(args) != 1:
+    sys.stderr.write('\n'.join([b.__name__ for b in benches]))
+    return 1
+
+  b = [b for b in benches if b.__name__ == args[0]]
+  if len(b) != 1:
+    sys.stderr.write('Oops')
+    return 1
+
+  bench = b[0]()
+  bench.SetUp()
+  try:
+    pr = cProfile.Profile()
+    pr.enable(builtins=False)
+    for i in range(options.repeat_count):
+      bench.Run()
+    pr.disable()
+    s = StringIO.StringIO()
+
+    sortby = 'cumulative'
+    ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
+    ps.print_stats()
+    print s.getvalue()
+    return 0
+  finally:
+    bench.TearDown()
+
+if __name__ == '__main__':
+  sys.exit(Main(sys.argv[1:]))
diff --git a/trace-viewer/trace_viewer/build/check_common.py b/trace-viewer/trace_viewer/build/check_common.py
new file mode 100644
index 0000000..ecf8388
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/check_common.py
@@ -0,0 +1,84 @@
+# 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.
+
+import os
+
+from trace_viewer import trace_viewer_project
+
+
+FILE_GROUPS = ["tracing_css_files",
+               "tracing_js_html_files",
+               "tracing_img_files"]
+
+def GetFileGroupFromFileName(filename):
+   extension = os.path.splitext(filename)[1]
+   return {
+     '.css': 'tracing_css_files',
+     '.html': 'tracing_js_html_files',
+     '.js': 'tracing_js_html_files',
+     '.png': 'tracing_img_files'
+   }[extension]
+
+def CheckListedFilesSorted(src_file, group_name, listed_files):
+  sorted_files = sorted(listed_files)
+  if sorted_files != listed_files:
+    mismatch = ''
+    for i in range(len(listed_files)):
+      if listed_files[i] != sorted_files[i]:
+        mismatch = listed_files[i]
+        break
+    what_is = '  ' + '\n  '.join(listed_files)
+    what_should_be = '  ' + '\n  '.join(sorted_files)
+    return '''In group {0} from file {1}, filenames aren't sorted.
+
+First mismatch:
+  {2}
+
+Current listing:
+{3}
+
+Correct listing:
+{4}\n\n'''.format(group_name, src_file, mismatch, what_is, what_should_be)
+  else:
+    return ''
+
+def GetKnownFiles():
+  p = trace_viewer_project.TraceViewerProject()
+  m = p.loader.LoadModule(module_name='extras.about_tracing.about_tracing')
+  absolute_filenames = m.GetAllDependentFilenamesRecursive(
+      include_raw_scripts=False)
+
+  return list(set([os.path.relpath(f, p.trace_viewer_path)
+                   for f in absolute_filenames]))
+
+def CheckCommon(file_name, listed_files):
+  project = trace_viewer_project.TraceViewerProject()
+  build_dir = os.path.join(project.src_path, 'build')
+
+  known_files = GetKnownFiles()
+  u = set(listed_files).union(set(known_files))
+  i = set(listed_files).intersection(set(known_files))
+  diff = list(u - i)
+
+  if len(diff) == 0:
+    return ''
+
+  error = 'Entries in ' + file_name + ' do not match files on disk:\n'
+  in_file_only = list(set(listed_files) - set(known_files))
+  in_known_only = list(set(known_files) - set(listed_files))
+
+  if len(in_file_only) > 0:
+    error += '  In file only:\n    ' + '\n    '.join(sorted(in_file_only))
+  if len(in_known_only) > 0:
+    if len(in_file_only) > 0:
+      error += '\n\n'
+    error += '  On disk only:\n    ' + '\n    '.join(sorted(in_known_only))
+
+  if in_file_only:
+    error += ('\n\n'
+        '  Note: only files actually used in about:tracing should\n'
+        '  be listed in the build files. Try running build/update_gyp_and_gn\n'
+        '  to update the files automatically.')
+
+  return error
diff --git a/trace-viewer/trace_viewer/build/check_common_unittest.py b/trace-viewer/trace_viewer/build/check_common_unittest.py
new file mode 100644
index 0000000..85e1349
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/check_common_unittest.py
@@ -0,0 +1,26 @@
+# 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.
+import unittest
+
+from trace_viewer.build import check_common
+
+class CheckCommonUnittTest(unittest.TestCase):
+  def test_filesSortedTest(self):
+    error = check_common.CheckListedFilesSorted('foo.gyp', 'tracing_pdf_files',
+                                                ['/dir/file.pdf',
+                                                 '/dir/another_file.pdf'])
+    expected_error = '''In group tracing_pdf_files from file foo.gyp,\
+ filenames aren't sorted.
+
+First mismatch:
+  /dir/file.pdf
+
+Current listing:
+  /dir/file.pdf
+  /dir/another_file.pdf
+
+Correct listing:
+  /dir/another_file.pdf
+  /dir/file.pdf\n\n'''
+    assert error == expected_error
diff --git a/trace-viewer/trace_viewer/build/check_gn.py b/trace-viewer/trace_viewer/build/check_gn.py
new file mode 100644
index 0000000..4e2d891
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/check_gn.py
@@ -0,0 +1,43 @@
+# 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.
+
+import os
+import re
+
+from trace_viewer.build import check_common
+
+
+GN_FILE = "BUILD.gn"
+
+
+def ItemToFilename(item):
+  assert item[0] == '"', "GN files use double-quotes, gyp uses single quotes"
+  assert item[-1] == '"', "GN files use double-quotes, gyp uses single quotes"
+  return item[1:-1]
+
+
+def GnCheck():
+  f = open(GN_FILE, 'r')
+  gn = f.read()
+  f.close()
+
+  listed_files = []
+  error = ""
+  for group in check_common.FILE_GROUPS:
+    expr = '%s = \[(.+?)\]\n' % group
+    m = re.search(expr, gn, re.DOTALL)
+    if not m:
+      raise Exception('%s is malformed' % GN_FILE)
+    g = m.group(1).strip()
+    items = g.split(',')
+    filenames = [ItemToFilename(item.strip()) for item in items
+                 if len(item) > 0]
+
+    error += check_common.CheckListedFilesSorted(GN_FILE, group, filenames)
+    listed_files.extend(map(os.path.normpath, filenames))
+
+  return error + check_common.CheckCommon(GN_FILE, listed_files)
+
+if __name__ == '__main__':
+  print GnCheck()
diff --git a/trace-viewer/trace_viewer/build/check_gyp.py b/trace-viewer/trace_viewer/build/check_gyp.py
new file mode 100644
index 0000000..2373543
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/check_gyp.py
@@ -0,0 +1,27 @@
+# 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.
+
+import os
+
+from trace_viewer.build import check_common
+
+GYP_FILE = "trace_viewer.gyp"
+
+def GypCheck():
+  f = open(GYP_FILE, 'r')
+  gyp = f.read()
+  f.close()
+
+  data = eval(gyp)
+  listed_files = []
+  error = ""
+  for group in check_common.FILE_GROUPS:
+    filenames = map(os.path.normpath, data["variables"][group])
+    error += check_common.CheckListedFilesSorted(GYP_FILE, group, filenames)
+    listed_files.extend(filenames)
+
+  return error + check_common.CheckCommon(GYP_FILE, listed_files)
+
+if __name__ == '__main__':
+  print GypCheck()
diff --git a/trace-viewer/trace_viewer/build/check_modules.py b/trace-viewer/trace_viewer/build/check_modules.py
new file mode 100644
index 0000000..673a781
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/check_modules.py
@@ -0,0 +1,18 @@
+# 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.
+
+import os
+
+from trace_viewer import trace_viewer_project
+
+def CheckModules():
+  p = trace_viewer_project.TraceViewerProject()
+  try:
+    p.CalcLoadSequenceForAllModules()
+  except Exception, ex:
+    return str(ex)
+  return []
+
+if __name__ == '__main__':
+  print GypCheck()
diff --git a/trace-viewer/trace_viewer/build/fixjsstyle b/trace-viewer/trace_viewer/build/fixjsstyle
new file mode 100755
index 0000000..294737a
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/fixjsstyle
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# 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.
+
+import os
+import sys
+
+if __name__ == '__main__':
+  top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+  sys.path.append(top_dir)
+  from trace_viewer.build import fixjsstyle
+  sys.exit(fixjsstyle.main())
diff --git a/trace-viewer/trace_viewer/build/fixjsstyle.py b/trace-viewer/trace_viewer/build/fixjsstyle.py
new file mode 100644
index 0000000..3e7a5bd
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/fixjsstyle.py
@@ -0,0 +1,23 @@
+# 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.
+import os
+import sys
+
+from trace_viewer import trace_viewer_project
+
+
+
+def main():
+  project = trace_viewer_project.TraceViewerProject()
+
+  sys.path.append(os.path.join(
+      project.trace_viewer_third_party_path, 'python_gflags'))
+  sys.path.append(os.path.join(
+      project.trace_viewer_third_party_path, 'closure_linter'))
+
+  from closure_linter import fixjsstyle
+
+  os.chdir(project.src_path)
+
+  fixjsstyle.main()
diff --git a/trace-viewer/trace_viewer/build/generate_about_tracing_contents b/trace-viewer/trace_viewer/build/generate_about_tracing_contents
new file mode 100755
index 0000000..2bac1a3
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/generate_about_tracing_contents
@@ -0,0 +1,13 @@
+#!/usr/bin/env python
+# 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.
+
+import os
+import sys
+
+if __name__ == '__main__':
+  top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+  sys.path.append(top_dir)
+  from trace_viewer.build import generate_about_tracing_contents
+  sys.exit(generate_about_tracing_contents.main(sys.argv))
diff --git a/trace-viewer/trace_viewer/build/generate_about_tracing_contents.py b/trace-viewer/trace_viewer/build/generate_about_tracing_contents.py
new file mode 100644
index 0000000..9c56105
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/generate_about_tracing_contents.py
@@ -0,0 +1,57 @@
+# 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.
+
+import codecs
+import optparse
+import os
+import sys
+
+import tvcm
+from trace_viewer import trace_viewer_project
+
+def main(args):
+  parser = optparse.OptionParser(usage="%prog --outdir=<directory>")
+  parser.add_option("--outdir", dest="out_dir",
+                    help="Where to place generated content")
+  options, args = parser.parse_args(args)
+
+  if not options.out_dir:
+    sys.stderr.write("ERROR: Must specify --outdir=<directory>")
+    parser.print_help()
+    return 1
+
+  filenames = ["extras/about_tracing/about_tracing.html"]
+  project = trace_viewer_project.TraceViewerProject()
+  load_sequence = project.CalcLoadSequenceForModuleFilenames(filenames)
+
+  olddir = os.getcwd()
+  try:
+    o = codecs.open(os.path.join(options.out_dir, "about_tracing.html"), 'w',
+                    encoding='utf-8')
+    try:
+      tvcm.GenerateStandaloneHTMLToFile(
+          o,
+          load_sequence,
+          title='chrome://tracing',
+          flattened_js_url='tracing.js')
+    except tvcm.module.DepsException, ex:
+      sys.stderr.write("Error: %s\n\n" % str(ex))
+      return 255
+    o.close()
+
+
+    o = codecs.open(os.path.join(options.out_dir, "about_tracing.js"), 'w',
+                    encoding='utf-8')
+    assert o.encoding == 'utf-8'
+    tvcm.GenerateJSToFile(
+        o,
+      load_sequence,
+      use_include_tags_for_scripts=True,
+      dir_for_include_tag_root=options.out_dir)
+    o.close()
+
+  finally:
+    os.chdir(olddir)
+
+  return 0
diff --git a/trace-viewer/trace_viewer/build/generate_about_tracing_contents_unittest.py b/trace-viewer/trace_viewer/build/generate_about_tracing_contents_unittest.py
new file mode 100644
index 0000000..fd03472
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/generate_about_tracing_contents_unittest.py
@@ -0,0 +1,18 @@
+# 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.
+import base64
+import unittest
+import tempfile
+import shutil
+
+from trace_viewer.build import generate_about_tracing_contents
+
+class GenerateAboutTracingContentsUnittTest(unittest.TestCase):
+  def test_smokeTest(self):
+    try:
+      tmpdir = tempfile.mkdtemp()
+      res = generate_about_tracing_contents.main(['--outdir', tmpdir])
+      assert res == 0
+    finally:
+      shutil.rmtree(tmpdir)
diff --git a/trace-viewer/trace_viewer/build/gjslint b/trace-viewer/trace_viewer/build/gjslint
new file mode 100755
index 0000000..14257ab
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/gjslint
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# 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.
+
+import os
+import sys
+
+
+src_dir = os.path.join(os.path.dirname(__file__), '..')
+
+if __name__ == '__main__':
+  top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+  sys.path.append(top_dir)
+  import trace_viewer
+  from hooks import gjslint
+  sys.exit(gjslint.Main([
+    os.path.join(top_dir, 'trace_viewer'),
+  ]))
diff --git a/trace-viewer/trace_viewer/build/trace2html.html b/trace-viewer/trace_viewer/build/trace2html.html
new file mode 100644
index 0000000..0889da9
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/trace2html.html
@@ -0,0 +1,66 @@
+<!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">
+<script>
+'use strict';
+
+var g_timelineViewEl;
+
+(function() {
+  var styleEl = document.createElement('style');
+  var lines = [
+    'html, body {',
+    '  box-sizing: border-box;',
+    '  overflow: hidden;',
+    '  margin: 0px;',
+    '  padding: 0;',
+    '  width: 100%;',
+    '  height: 100%;',
+    '}',
+    '#timeline-view {',
+    '  width: 100%;',
+    '  height: 100%;',
+    '}'
+  ];
+  styleEl.textContent = lines.join('\n');
+  document.head.appendChild(styleEl);
+})();
+
+document.addEventListener('DOMContentLoaded', function() {
+  g_timelineViewEl = new tv.TraceViewer();
+  g_timelineViewEl.id = 'timeline-view';
+  document.body.appendChild(g_timelineViewEl);
+
+  var traces = [];
+  var viewerDataScripts = document.querySelectorAll('#viewer-data');
+  for (var i = 0; i < viewerDataScripts.length; i++) {
+    var text = viewerDataScripts[i].textContent;
+    // Trim leading newlines off the text. They happen during writing.
+    while (text[0] == '\n')
+      text = text.substring(1);
+    traces.push(atob(text));
+  }
+
+  var m = new tv.c.TraceModel();
+  var p = m.importTracesWithProgressDialog(traces, true);
+  p.then(
+      function() {
+        g_timelineViewEl.model = m;
+        g_timelineViewEl.updateDocumentFavicon();
+        g_timelineViewEl.tabIndex = 1;
+        g_timelineViewEl.viewTitle = document.title;
+        if (g_timelineViewEl.timeline)
+          g_timelineViewEl.timeline.focusElement = g_timelineViewEl;
+      },
+      function(err) {
+        var overlay = new tv.b.ui.Overlay();
+        overlay.textContent = tv.b.normalizeException(err).message;
+        overlay.title = 'Import error';
+        overlay.visible = true;
+      });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/build/trace2html.py b/trace-viewer/trace_viewer/build/trace2html.py
new file mode 100644
index 0000000..0607ddc
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/trace2html.py
@@ -0,0 +1,116 @@
+# 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.
+
+import codecs
+import base64
+import gzip
+import json
+import optparse
+import shutil
+import os
+import StringIO
+import sys
+import tempfile
+
+from trace_viewer import trace_viewer_project
+from tvcm import generate
+
+
+def Main(args):
+  parser = optparse.OptionParser(
+    usage="%prog <options> trace_file1 [trace_file2 ...]",
+    epilog="""Takes the provided trace file and produces a standalone html
+file that contains both the trace and the trace viewer.""")
+
+  project = trace_viewer_project.TraceViewerProject()
+  project.AddConfigNameOptionToParser(parser)
+
+  parser.add_option(
+      "--output", dest="output",
+      help='Where to put the generated result. If not ' +
+           'given, the trace filename is used, with an html suffix.')
+  parser.add_option(
+      "--quiet", action='store_true',
+      help='Dont print the output file name')
+  options, args = parser.parse_args(args)
+  if len(args) == 0:
+    parser.error('At least one trace file required')
+
+  if options.output:
+    output_filename = options.output
+  elif len(args) > 1:
+    parser.error('Must specify --output if >1 trace file')
+  else:
+    namepart = os.path.splitext(args[0])[0]
+    output_filename = namepart + '.html'
+
+  with codecs.open(output_filename, mode='w', encoding='utf-8') as f:
+    WriteHTMLForTracesToFile(args, f, config_name=options.config_name)
+
+  if not options.quiet:
+    print output_filename
+  return 0
+
+
+class ViewerDataScript(generate.ExtraScript):
+  def __init__(self, trace_data_string, mime_type):
+    super(ViewerDataScript, self).__init__()
+    self._trace_data_string = trace_data_string
+    self._mime_type = mime_type
+
+  def WriteToFile(self, output_file):
+    output_file.write('<script id="viewer-data" type="%s">\n' % self._mime_type)
+    compressed_trace = StringIO.StringIO()
+    with gzip.GzipFile(fileobj=compressed_trace, mode='w') as f:
+      f.write(self._trace_data_string)
+    b64_content = base64.b64encode(compressed_trace.getvalue())
+    output_file.write(b64_content)
+    output_file.write('\n</script>\n')
+
+
+def WriteHTMLForTraceDataToFile(trace_data_list,
+                                title, output_file,
+                                config_name=None):
+  project = trace_viewer_project.TraceViewerProject()
+
+  if config_name == None:
+    config_name = project.GetDefaultConfigName()
+
+  modules = [
+    'build.trace2html',
+    'extras.importer.gzip_importer', # Must have this regardless of config.
+    project.GetModuleNameForConfigName(config_name)
+  ]
+
+  load_sequence = project.CalcLoadSequenceForModuleNames(modules)
+
+  scripts = []
+  for trace_data in trace_data_list:
+    # If the object was previously decoded from valid JSON data (e.g., in
+    # WriteHTMLForTracesToFile), it will be a JSON object at this point and we
+    # should re-serialize it into a string. Other types of data will be already
+    # be strings.
+    if not isinstance(trace_data, basestring):
+      trace_data = json.dumps(trace_data)
+      mime_type = 'application/json'
+    else:
+      mime_type = 'text/plain'
+    scripts.append(ViewerDataScript(trace_data, mime_type))
+  generate.GenerateStandaloneHTMLToFile(
+    output_file, load_sequence, title, extra_scripts=scripts)
+
+
+def WriteHTMLForTracesToFile(trace_filenames, output_file, config_name=None):
+  trace_data_list = []
+  for filename in trace_filenames:
+    with open(filename, 'r') as f:
+      trace_data = f.read()
+      try:
+        trace_data = json.loads(trace_data)
+      except ValueError:
+        pass
+      trace_data_list.append(trace_data)
+
+  title = "Trace from %s" % ','.join(trace_filenames)
+  WriteHTMLForTraceDataToFile(trace_data_list, title, output_file, config_name)
diff --git a/trace-viewer/trace_viewer/build/trace2html_unittest.py b/trace-viewer/trace_viewer/build/trace2html_unittest.py
new file mode 100644
index 0000000..20bb501
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/trace2html_unittest.py
@@ -0,0 +1,25 @@
+# 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.
+import codecs
+import unittest
+import tempfile
+import os
+
+from trace_viewer.build import trace2html
+
+class Trace2HTMLTests(unittest.TestCase):
+  def test_writeHTMLForTracesToFile(self):
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.html') as raw_tmpfile:
+      with codecs.open(raw_tmpfile.name, 'w', encoding='utf-8') as tmpfile:
+        simple_trace_path = os.path.join(
+            os.path.dirname(__file__),
+            '..', '..', 'test_data', 'simple_trace.json')
+        big_trace_path = os.path.join(
+            os.path.dirname(__file__),
+            '..', '..', 'test_data', 'big_trace.json')
+        non_json_trace_path = os.path.join(
+            os.path.dirname(__file__),
+            '..', '..', 'test_data', 'android_systrace.txt')
+        res = trace2html.WriteHTMLForTracesToFile(
+            [big_trace_path, simple_trace_path, non_json_trace_path], tmpfile)
diff --git a/trace-viewer/trace_viewer/build/trace_viewer_dev_server.py b/trace-viewer/trace_viewer/build/trace_viewer_dev_server.py
new file mode 100644
index 0000000..1b1e8fa
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/trace_viewer_dev_server.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# 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.
+import json
+import os
+import sys
+
+from trace_viewer import trace_viewer_project
+import tvcm
+
+_ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+
+def getFilesIn(basedir):
+  data_files = []
+  for dirpath, dirnames, filenames in os.walk(basedir, followlinks=True):
+    new_dirnames = [d for d in dirnames if not d.startswith('.')]
+    del dirnames[:]
+    dirnames += new_dirnames
+
+    for f in filenames:
+      if f.startswith('.'):
+        continue
+      full_f = os.path.join(dirpath, f)
+      rel_f = os.path.relpath(full_f, basedir)
+      data_files.append(rel_f)
+
+  data_files.sort()
+  return data_files
+
+
+def do_GET_json_examples(request):
+  test_data_path = os.path.abspath(os.path.join(_ROOT_PATH, 'test_data'))
+  data_files = getFilesIn(test_data_path)
+  files_as_json = json.dumps(data_files)
+
+  request.send_response(200)
+  request.send_header('Content-Type', 'application/json')
+  request.send_header('Content-Length', len(files_as_json))
+  request.end_headers()
+  request.wfile.write(files_as_json)
+
+def do_GET_json_examples_skp(request):
+  skp_data_path = os.path.abspath(os.path.join(_ROOT_PATH, 'skp_data'))
+  data_files = getFilesIn(skp_data_path)
+  files_as_json = json.dumps(data_files)
+
+  request.send_response(200)
+  request.send_header('Content-Type', 'application/json')
+  request.send_header('Content-Length', len(files_as_json))
+  request.end_headers()
+  request.wfile.write(files_as_json)
+
+def do_GET_json_tests(self):
+  test_module_resources = self.server.project.FindAllTestModuleResources()
+
+  test_module_names = [x.name for x in test_module_resources]
+
+  tests = {'test_module_names': test_module_names,
+           'test_links': self.server.test_links}
+  tests_as_json = json.dumps(tests);
+
+  self.send_response(200)
+  self.send_header('Content-Type', 'application/json')
+  self.send_header('Content-Length', len(tests_as_json))
+  self.end_headers()
+  self.wfile.write(tests_as_json)
+
+def do_POST_report_test_results(request):
+  request.send_response(200)
+  request.send_header('Content-Length', '0')
+  request.end_headers()
+  msg = request.rfile.read()
+  ostream = sys.stdout if 'PASSED' in msg else sys.stderr
+  ostream.write(msg + '\n')
+
+def do_POST_report_test_completion(request):
+  request.send_response(200)
+  request.send_header('Content-Length', '0')
+  request.end_headers()
+  msg = request.rfile.read()
+  sys.stdout.write(msg + '\n')
+  request.server.RequestShutdown(exit_code=(0 if 'ALL_PASSED' in msg else 1))
+
+def Main(args):
+  port = 8003
+  project = trace_viewer_project.TraceViewerProject()
+
+  server = tvcm.DevServer(port=port, project=project)
+  server.AddPathHandler('/json/examples', do_GET_json_examples)
+  server.AddPathHandler('/tv/json/tests', do_GET_json_tests)
+  server.AddPathHandler('/json/examples/skp', do_GET_json_examples_skp)
+
+  server.AddSourcePathMapping(project.trace_viewer_path)
+  server.AddTestLink('/examples/skia_debugger.html', 'Skia Debugger')
+  server.AddTestLink('/examples/trace_viewer.html', 'Trace File Viewer')
+
+  server.AddPathHandler('/test_automation/notify_test_result',
+                        do_POST_report_test_results, supports_post=True)
+  server.AddPathHandler('/test_automation/notify_completion',
+                        do_POST_report_test_completion, supports_post=True)
+
+
+  server.serve_forever()
diff --git a/trace-viewer/trace_viewer/build/update_gyp_and_gn b/trace-viewer/trace_viewer/build/update_gyp_and_gn
new file mode 100755
index 0000000..a7b094c
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/update_gyp_and_gn
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# 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.
+
+import os
+import sys
+
+if __name__ == '__main__':
+  top_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
+  sys.path.append(top_dir)
+  from trace_viewer import trace_viewer_project
+  from trace_viewer.build import update_gyp_and_gn
+  sys.exit(update_gyp_and_gn.Update())
diff --git a/trace-viewer/trace_viewer/build/update_gyp_and_gn.py b/trace-viewer/trace_viewer/build/update_gyp_and_gn.py
new file mode 100644
index 0000000..38c4c7a
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/update_gyp_and_gn.py
@@ -0,0 +1,155 @@
+# 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.
+
+import collections
+import os
+import re
+
+from trace_viewer.build import check_common
+from trace_viewer import trace_viewer_project
+
+class _Token(object):
+  def __init__(self, data, id=None):
+    self.data = data
+    if id:
+      self.id = id
+    else:
+      self.id = 'plain'
+
+
+class BuildFile(object):
+  def __init__(self, text, file_groups):
+    self._file_groups = file_groups
+    self._tokens = [token for token in self._Tokenize(text)]
+
+  def _Tokenize(self, text):
+    rest = text
+    token_regex = self._TokenRegex()
+    while len(rest):
+      m = token_regex.search(rest)
+      if not m:
+        # In `rest', we couldn't find a match.
+        # So, lump the entire `rest' into a token
+        # and stop producing any more tokens.
+        yield _Token(rest)
+        return
+      min_index, end_index, matched_token = self._ProcessMatch(m)
+
+      if min_index > 0:
+        yield _Token(rest[:min_index])
+
+      yield matched_token
+      rest = rest[end_index:]
+
+  def Update(self, files_by_group):
+    for token in self._tokens:
+      if token.id in files_by_group:
+        token.data = self._GetReplacementListAsString(
+          token.data,
+          files_by_group[token.id]
+        )
+
+  def Write(self, f):
+    for token in self._tokens:
+      f.write(token.data)
+
+  def _ProcessMatch(self, match):
+    raise Exception("Not implemented.")
+
+  def _TokenRegex(self):
+    raise Exception("Not implemented.")
+
+  def _GetReplacementListAsString(self, existing_list_as_string, filelist):
+    raise Exception("Not implemented.")
+
+
+class GnFile(BuildFile):
+  def _ProcessMatch(self, match):
+    min_index = match.start(2)
+    end_index = match.end(2)
+    token = _Token(match.string[min_index:end_index],
+                        id=match.groups()[0])
+    return min_index, end_index, token
+
+  def _TokenRegex(self):
+    # regexp to match the following:
+    # file_group_name = [
+    #   "path/to/one/file.extension",
+    #   "another/file.ex",
+    # ]
+    # In the match,
+    # group 1 is : 'file_group_name'
+    # group 2 is : """  "path/to/one/file.extension",\n  "another/file.ex",\n"""
+    regexp_str = '(%s) = \[\n(.+?)\]\n' % '|'.join(self._file_groups)
+    return re.compile(regexp_str, re.MULTILINE | re.DOTALL)
+
+  def _GetReplacementListAsString(self, existing_list_as_string, filelist):
+    list_entry = existing_list_as_string.splitlines()[0]
+    prefix, entry, suffix = list_entry.split('"')
+    return "".join(['"'.join([prefix, filename, suffix + '\n'])
+                    for filename in filelist])
+
+
+class GypFile(BuildFile):
+  def _ProcessMatch(self, match):
+    min_index = match.start(2)
+    end_index = match.end(2)
+    token = _Token(match.string[min_index:end_index],
+                        id=match.groups()[0])
+    return min_index, end_index, token
+
+  def _TokenRegex(self):
+    # regexp to match the following:
+    #   'file_group_name': [
+    #     'path/to/one/file.extension',
+    #     'another/file.ex',
+    #   ]
+    # In the match,
+    # group 1 is : 'file_group_name'
+    # group 2 is : """  'path/to/one/file.extension',\n  'another/file.ex',\n"""
+    regexp_str = "'(%s)': \[\n(.+?) +\],?\n" % "|".join(self._file_groups)
+    return re.compile(regexp_str, re.MULTILINE | re.DOTALL)
+
+  def _GetReplacementListAsString(self, existing_list_as_string, filelist):
+    list_entry = existing_list_as_string.splitlines()[0]
+    prefix, entry, suffix = list_entry.split("'")
+    return "".join(["'".join([prefix, filename, suffix + '\n'])
+                    for filename in filelist])
+
+
+def _GroupFiles(fileNameToGroupNameFunc, filenames):
+  file_groups = collections.defaultdict(lambda: [])
+  for filename in filenames:
+    file_groups[fileNameToGroupNameFunc(filename)].append(filename)
+  for group in file_groups:
+    file_groups[group].sort()
+  return file_groups
+
+
+def _UpdateBuildFile(filename, build_file_class):
+  updated_content = None
+  with open(filename, 'r') as f:
+    build_file = build_file_class(f.read(), check_common.FILE_GROUPS)
+  files_by_group = _GroupFiles(check_common.GetFileGroupFromFileName,
+                               check_common.GetKnownFiles())
+  build_file.Update(files_by_group)
+  with open(filename, 'w') as f:
+    build_file.Write(f)
+
+
+def UpdateGn():
+  tvp = trace_viewer_project.TraceViewerProject()
+  _UpdateBuildFile(
+      os.path.join(tvp.trace_viewer_path, 'BUILD.gn'), GnFile)
+
+
+def UpdateGyp():
+  tvp = trace_viewer_project.TraceViewerProject()
+  _UpdateBuildFile(
+      os.path.join(tvp.trace_viewer_path, 'trace_viewer.gyp'), GypFile)
+
+
+def Update():
+  UpdateGyp()
+  UpdateGn()
diff --git a/trace-viewer/trace_viewer/build/update_gyp_and_gn_unittest.py b/trace-viewer/trace_viewer/build/update_gyp_and_gn_unittest.py
new file mode 100644
index 0000000..25023aa
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/update_gyp_and_gn_unittest.py
@@ -0,0 +1,87 @@
+# 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.
+import unittest
+import StringIO
+
+from trace_viewer.build.update_gyp_and_gn import BuildFile, GnFile, GypFile
+
+class UpdateGypAndGnTests(unittest.TestCase):
+  def setUp(self):
+    self.file_groups = ['group1', 'group2']
+
+  def test_GnTokenizer(self):
+    content = """useless data
+group1 = [
+"/a/file",
+ <there can be useless things here too>
+"/another/file",
+]
+More useless things"""
+    gn_file = GnFile(content, self.file_groups)
+    try:
+      assert len(gn_files._tokens) == 3
+      assert gn_files._tokens[0].id == 'plain'
+      assert gn_files._tokens[0].data == """useless data
+group1 = [
+"""
+      assert gn_files._tokens[1].id == 'group1'
+      assert gn_files._tokens[1].data == """"/a/file",
+ <there can be useless things here too>
+"/another/file",
+"""
+      assert gn_files._tokens[2].id == 'plain'
+      assert gn_files._tokens[2].data == """]
+More useless things"""
+    except:
+      pass
+
+  def test_GypTokenizer(self):
+    content = """useless data
+'group1': [
+    <file list goes here>
+    ]
+Note the four spaces bofer the ] above"""
+    gyp_file = GypFile(content, self.file_groups)
+    try:
+      assert len(gyp_files._tokens) == 3
+      assert gyp_files._tokens[0].id == 'plain'
+      assert gyp_files._tokens[0].data == """useless data
+'group1': [
+"""
+      assert gyp_files._tokens[1].id == 'group1'
+      assert gyp_files._tokens[1].data == """    <file list goes here>
+"""
+      assert gyp_files._tokens[2].id == 'plain'
+      assert gyp_files._tokens[2].data == """    ]
+Note the four spaces before the ] above """
+    except:
+      pass
+
+  def test_GnFileListBuilder(self):
+    gn_file = GnFile("", self.file_groups)
+    existing_list_as_string = """    "/four/spaces/indent",
+    "/four/spaces/again",
+"""
+    new_list = ['item1', 'item2', 'item3']
+    try:
+      assert (gn_file._GetReplacementListAsString(existing_list_as_string,
+                                                 new_list)
+              ==
+      """    "item1",\n    "item2",\n    "item3",\n""")
+    except:
+      pass
+
+  def test_GypFileListBuilder(self):
+    gyp_file = GypFile("", self.file_groups)
+    existing_list_as_string = """    '/four/spaces/indent',
+     '/five/spaces/but/only/first/line/matters',
+"""
+    new_list = ['item1', 'item2', 'item3']
+    try:
+      assert (gyp_file._GetReplacementListAsString(existing_list_as_string,
+                                                 new_list)
+              ==
+      """    'item1',\n    'item2',\n    'item3',\n""")
+    except:
+      pass
diff --git a/trace-viewer/trace_viewer/build/vulcanize_trace_viewer.py b/trace-viewer/trace_viewer/build/vulcanize_trace_viewer.py
new file mode 100644
index 0000000..dbb0c00
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/vulcanize_trace_viewer.py
@@ -0,0 +1,89 @@
+# 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.
+
+import codecs
+import optparse
+import os
+import StringIO
+import sys
+import tempfile
+
+from trace_viewer import trace_viewer_project
+from tvcm import generate
+
+
+def Main(args):
+
+  parser = optparse.OptionParser(
+    usage="%prog <options>",
+    epilog="""Produces a standalone html import that contains the
+trace viewer.""")
+
+  project = trace_viewer_project.TraceViewerProject()
+  project.AddConfigNameOptionToParser(parser)
+
+  parser.add_option('--no-min', dest='no_min', default=False,
+                    action='store_true',
+                    help='skip minification')
+  parser.add_option('--report-sizes', dest='report_sizes', default=False,
+                    action='store_true',
+                    help='Explain what makes trace_viewer big.')
+  parser.add_option('--report-deps', dest='report_deps', default=False,
+                    action='store_true',
+                    help='Print a dot-formatted deps graph.')
+  parser.add_option(
+      "--output", dest="output",
+      help='Where to put the generated result. If not ' +
+           'given, $TRACE_VIEWER/bin/trace_viewer.html is used.')
+
+  options, args = parser.parse_args(args)
+  if len(args) != 0:
+    parser.error('No arguments needed.')
+
+  trace_viewer_dir = os.path.relpath(os.path.join(os.path.dirname(__file__), '..', '..'))
+  if options.output:
+    output_filename = options.output
+  else:
+    output_filename = os.path.join(
+        trace_viewer_dir, 'bin/trace_viewer_%s.html' % options.config_name)
+
+  with codecs.open(output_filename, 'w', encoding='utf-8') as f:
+    WriteTraceViewer(
+        f,
+        config_name=options.config_name,
+        minify=not options.no_min,
+        report_sizes=options.report_sizes,
+        report_deps=options.report_deps)
+
+  return 0
+
+
+def WriteTraceViewer(output_file,
+                     config_name=None,
+                     minify=False,
+                     report_sizes=False,
+                     report_deps=False,
+                     output_html_head_and_body=True,
+                     extra_search_paths=None,
+                     extra_module_names_to_load=None):
+  project = trace_viewer_project.TraceViewerProject()
+  if extra_search_paths:
+    for p in extra_search_paths:
+      project.source_paths.append(p)
+  if config_name == None:
+    config_name = project.GetDefaultConfigName()
+
+  module_names = ['trace_viewer', project.GetModuleNameForConfigName(config_name)]
+  if extra_module_names_to_load:
+    module_names += extra_module_names_to_load
+  load_sequence = project.CalcLoadSequenceForModuleNames(
+    module_names)
+
+  if report_deps:
+    sys.stdout.write(project.GetDepsGraphFromModuleNames(module_names))
+
+  generate.GenerateStandaloneHTMLToFile(
+    output_file, load_sequence,
+    minify=minify, report_sizes=report_sizes,
+    output_html_head_and_body=output_html_head_and_body)
diff --git a/trace-viewer/trace_viewer/build/vulcanize_trace_viewer_unittest.py b/trace-viewer/trace_viewer/build/vulcanize_trace_viewer_unittest.py
new file mode 100644
index 0000000..625a621
--- /dev/null
+++ b/trace-viewer/trace_viewer/build/vulcanize_trace_viewer_unittest.py
@@ -0,0 +1,18 @@
+# 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.
+import codecs
+import unittest
+import tempfile
+import os
+
+from trace_viewer.build import vulcanize_trace_viewer
+from tvcm import generate
+
+class Trace2HTMLTests(unittest.TestCase):
+  def test_writeHTMLForTracesToFile(self):
+    can_minify=generate.CanMinify()
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.html') as raw_tmpfile:
+      with codecs.open(raw_tmpfile.name, 'w', encoding='utf-8') as tmpfile:
+        res = vulcanize_trace_viewer.WriteTraceViewer(tmpfile,
+                                                      minify=can_minify)
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>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/about_tracing.html b/trace-viewer/trace_viewer/extras/about_tracing/about_tracing.html
new file mode 100644
index 0000000..f6b6c8a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/about_tracing.html
@@ -0,0 +1,23 @@
+<!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="/extras/about_tracing/common.css">
+<link rel="import" href="/extras/about_tracing/profiling_view.html">
+<link rel="import" href="/extras/full_config.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.about_tracing', function() {
+  window.profilingView = undefined;  // Made global for debugging purposes only.
+
+  document.addEventListener('DOMContentLoaded', function() {
+    window.profilingView = new tv.e.about_tracing.ProfilingView();
+    document.body.appendChild(profilingView);
+  });
+
+  return {};
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/common.css b/trace-viewer/trace_viewer/extras/about_tracing/common.css
new file mode 100644
index 0000000..e8d6990
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/common.css
@@ -0,0 +1,20 @@
+/* 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.
+ */
+html,
+body {
+  height: 100%;
+}
+
+body {
+  -webkit-flex-direction: column;
+  display: -webkit-flex;
+  margin: 0;
+  padding: 0;
+}
+
+body > x-profiling-view {
+  -webkit-flex: 1 1 auto;
+}
+
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/inspector_connection.html b/trace-viewer/trace_viewer/extras/about_tracing/inspector_connection.html
new file mode 100644
index 0000000..df37614
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/inspector_connection.html
@@ -0,0 +1,123 @@
+<!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';
+
+/**
+ * Contains connection code that inspector's embedding framework calls on
+ * tracing, and that tracing can use to talk to inspector.
+ */
+tv.exportTo('tv.e.about_tracing', function() {
+  // Interface used by inspector when it hands data to us from the backend.
+  window.DevToolsAPI = {
+    setToolbarColors: function() { },
+    addExtensions: function() { },
+    setInspectedPageId: function() { },
+
+    dispatchMessage: function(payload) {
+      throw new Error('Should have been patched by InspectorConnection');
+    }
+  };
+  // Temporary until inspector backend switches to DevToolsAPI.
+  window.InspectorFrontendAPI = window.DevToolsAPI;
+
+  /**
+   * @constructor
+   */
+  function InspectorConnection() {
+    if (InspectorConnection.instance)
+      throw new Error('Singleton');
+
+    this.nextRequestId_ = 1;
+    this.pendingRequestResolversId_ = {};
+
+    this.notificationListenersByMethodName_ = {};
+    DevToolsAPI.dispatchMessage = this.dispatchMessage_.bind(this);
+  }
+
+  InspectorConnection.prototype = {
+    req: function(method, params) {
+      var id = this.nextRequestId_++;
+      var msg = JSON.stringify({
+        id: id,
+        method: method,
+        params: params
+      });
+      DevToolsHost.sendMessageToBackend(msg);
+
+      return new Promise(function(resolve, reject) {
+        this.pendingRequestResolversId_[id] = {
+          resolve: resolve,
+          reject: reject
+        };
+      }.bind(this));
+    },
+
+    setNotificationListener: function(method, listener) {
+      this.notificationListenersByMethodName_[method] = listener;
+    },
+
+    dispatchMessage_: function(payload) {
+      var isStringPayload = typeof payload === 'string';
+      // Special handling for Tracing.dataCollected because it is high
+      // bandwidth.
+      var isDataCollectedMessage = isStringPayload ?
+          payload.indexOf('"method": "Tracing.dataCollected"') !== -1 :
+          payload.method === 'Tracing.dataCollected';
+      if (isDataCollectedMessage) {
+        var listener = this.notificationListenersByMethodName_[
+            'Tracing.dataCollected'];
+        if (listener) {
+          // FIXME(loislo): trace viewer should be able to process
+          // raw message object because string based version a few times
+          // slower on the browser side.
+          // see https://codereview.chromium.org/784513002.
+          listener(isStringPayload ? payload : JSON.stringify(payload));
+          return;
+        }
+      }
+
+      var message = isStringPayload ? JSON.parse(payload) : payload;
+      if (message.id) {
+        var resolver = this.pendingRequestResolversId_[message.id];
+        if (resolver === undefined) {
+          console.log('Unrecognized ack', message);
+          return;
+        }
+        if (message.error) {
+          resolver.reject(message.error);
+          return;
+        }
+        resolver.resolve(message.result);
+        return;
+      }
+
+      if (message['method']) {
+        var listener = this.notificationListenersByMethodName_[message.method];
+        if (listener === undefined) {
+          console.log('Unhandled ', message.method);
+          return;
+        }
+        listener(message.params);
+        return;
+      }
+      console.log('Unknown dispatchMessage: ', payload);
+    }
+  };
+
+  if (window.DevToolsHost)
+    InspectorConnection.instance = new InspectorConnection();
+  else
+    InspectorConnection.instance = undefined;
+
+  return {
+    InspectorConnection: InspectorConnection
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/inspector_tracing_controller_client.html b/trace-viewer/trace_viewer/extras/about_tracing/inspector_tracing_controller_client.html
new file mode 100644
index 0000000..e9b0185
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/inspector_tracing_controller_client.html
@@ -0,0 +1,182 @@
+<!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/about_tracing/tracing_controller_client.html">
+<link rel="import" href="/extras/about_tracing/inspector_connection.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.about_tracing', function() {
+  function createResolvedPromise(data) {
+    var promise = new Promise(function(resolve, reject) {
+      if (data)
+        resolve(data);
+      else
+        resolve();
+    });
+    return promise;
+  }
+
+  function appendTraceChunksTo(chunks, messageString) {
+    if (typeof messageString !== 'string')
+      throw new Error('Invalid data');
+    var re = /"params":\s*\{\s*"value":\s*\[([^]+)\]\s*\}\s*\}/;
+    var m = re.exec(messageString);
+    if (!m)
+      throw new Error('Malformed response');
+
+    if (chunks.length > 1)
+      chunks.push(',');
+    chunks.push(m[1]);
+  }
+
+  /**
+   * Controls tracing using the inspector's FrontendAgentHost APIs.
+   *
+   * @constructor
+   */
+  function InspectorTracingControllerClient() {
+    this.recording_ = false;
+    this.bufferUsage_ = 0;
+    this.conn_ = tv.e.about_tracing.InspectorConnection.instance;
+    this.currentTraceTextChunks_ = undefined;
+  }
+
+  InspectorTracingControllerClient.prototype = {
+    __proto__: tv.e.about_tracing.TracingControllerClient.prototype,
+
+    beginMonitoring: function(monitoringOptions) {
+      throw new Error('Not implemented');
+    },
+
+    endMonitoring: function() {
+      throw new Error('Not implemented');
+    },
+
+    captureMonitoring: function() {
+      throw new Error('Not implemented');
+    },
+
+    getMonitoringStatus: function() {
+      return createResolvedPromise({
+        isMonitoring: false,
+        categoryFilter: '',
+        useSystemTracing: false,
+        useContinuousTracing: false,
+        useSampling: false
+      });
+    },
+
+    getCategories: function() {
+      var res = this.conn_.req('Tracing.getCategories', {});
+      return res.then(function(result) {
+        return result.categories;
+      }, function(err) {
+        return [];
+      });
+    },
+
+    beginRecording: function(recordingOptions) {
+      if (this.recording_)
+        throw new Error('Already recording');
+      this.recording_ = 'starting';
+      var res = this.conn_.req(
+          'Tracing.start',
+          {
+            categories: recordingOptions.categoryFilter,
+            options:
+                [recordingOptions.tracingRecordMode,
+                  (recordingOptions.useSampling ? 'enable-sampling' : '')
+                ].join(','),
+            bufferUsageReportingInterval: 1000
+          });
+      res = res.then(
+          function ok() {
+            this.conn_.setNotificationListener(
+                'Tracing.bufferUsage',
+                this.onBufferUsageUpdateFromInspector_.bind(this));
+            this.recording_ = true;
+          }.bind(this),
+          function error() {
+            this.recording_ = false;
+          }.bind(this));
+      return res;
+    },
+
+    onBufferUsageUpdateFromInspector_: function(params) {
+      this.bufferUsage_ = params.value || params.percentFull;
+    },
+
+    beginGetBufferPercentFull: function() {
+      var that = this;
+      return new Promise(function(resolve, reject) {
+        setTimeout(function() {
+          resolve(that.bufferUsage_);
+        }, 100);
+      });
+    },
+
+    onDataCollected_: function(messageString) {
+      appendTraceChunksTo(this.currentTraceTextChunks_, messageString);
+    },
+
+    endRecording: function() {
+      if (this.recording_ === false)
+        return createResolvedPromise();
+
+      if (this.recording_ !== true)
+        throw new Error('Cannot end');
+
+      this.currentTraceTextChunks_ = ['['];
+      this.conn_.setNotificationListener(
+          'Tracing.dataCollected', this.onDataCollected_.bind(this));
+
+      var clearListeners = function() {
+        this.conn_.setNotificationListener(
+            'Tracing.bufferUsage', undefined);
+        this.conn_.setNotificationListener(
+            'Tracing.tracingComplete', undefined);
+        this.conn_.setNotificationListener(
+            'Tracing.dataCollected', undefined);
+      }.bind(this);
+
+      this.recording_ = 'stopping';
+      return new Promise(function(resolve, reject) {
+        function tracingComplete() {
+          clearListeners();
+          this.recording_ = false;
+          this.currentTraceTextChunks_.push(']');
+          var traceText = this.currentTraceTextChunks_.join('');
+          this.currentTraceTextChunks_ = undefined;
+          resolve(traceText);
+        }
+
+        function tracingFailed(err) {
+          clearListeners();
+          this.recording_ = false;
+          reject(err);
+        }
+
+        this.conn_.setNotificationListener(
+            'Tracing.tracingComplete', tracingComplete.bind(this));
+        this.conn_.req('Tracing.end', {}).then(
+            function end() {
+              // Nothing happens here. We're really waiting for
+              // Tracing.tracingComplete.
+            }.bind(this),
+            tracingFailed.bind(this));
+      }.bind(this));
+    }
+  };
+
+  return {
+    InspectorTracingControllerClient: InspectorTracingControllerClient,
+    appendTraceChunksTo: appendTraceChunksTo
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/inspector_tracing_controller_client_test.html b/trace-viewer/trace_viewer/extras/about_tracing/inspector_tracing_controller_client_test.html
new file mode 100644
index 0000000..d655c73
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/inspector_tracing_controller_client_test.html
@@ -0,0 +1,81 @@
+<!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/about_tracing/inspector_tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('beginRecording_sendCategoriesAndOptions', function() {
+    var controller = new tv.e.about_tracing.InspectorTracingControllerClient();
+    controller.conn_ = new (function() {
+      this.req = function(method, params) {
+        var msg = JSON.stringify({
+          id: 1,
+          method: method,
+          params: params
+        });
+        return new (function() {
+          this.msg = msg;
+          this.then = function(m1, m2) {
+            return this;
+          };
+        })();
+      };
+      this.setNotificationListener = function(method, listener) {
+      };
+    })();
+
+    var recordingOptions = {
+      categoryFilter: JSON.stringify(['a', 'b', 'c']),
+      useSystemTracing: false,
+      tracingRecordMode: 'test-mode',
+      useSampling: true
+    };
+
+    var result = JSON.parse(controller.beginRecording(recordingOptions).msg);
+    assert.equal(result.params.categories, JSON.stringify(['a', 'b', 'c']));
+    var options = result.params.options.split(',');
+    var tracingRecordTestMode = false;
+    var sampleFlag = false;
+    for (var s in options) {
+      if (options[s] === 'test-mode') tracingRecordTestMode = true;
+      else if (options[s] === 'enable-sampling') sampleFlag = true;
+      else assert.equal(options[s], '');
+    }
+    assert.isTrue(tracingRecordTestMode);
+    assert.isTrue(sampleFlag);
+  });
+
+  test('oldFormat', function() {
+    var chunks = [];
+    tv.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [ {"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}} ] } }"'); // @suppress longLineCheck
+    assert.equal(chunks.length, 1);
+    JSON.parse('[' + chunks.join('') + ']');
+  });
+
+  test('newFormat', function() {
+    var chunks = [];
+    tv.e.about_tracing.appendTraceChunksTo(chunks, '"{ "method": "Tracing.dataCollected", "params": { "value": [{"cat":"__metadata","pid":28871,"tid":0,"ts":0,"ph":"M","name":"num_cpus","args":{"number":4}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_sort_index","args":{"sort_index":-5}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_name","args":{"name":"Renderer"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"process_labels","args":{"labels":"JS Bin"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_sort_index","args":{"sort_index":-1}},{"cat":"__metadata","pid":28871,"tid":28917,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Compositor"}},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"thread_name","args":{"name":"Chrome_ChildIOThread"}},{"cat":"__metadata","pid":28871,"tid":28919,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CompositorRasterWorker1/28919"}},{"cat":"__metadata","pid":28871,"tid":28908,"ts":0,"ph":"M","name":"thread_name","args":{"name":"CrRendererMain"}},{"cat":"ipc,toplevel","pid":28871,"tid":28911,"ts":22000084746,"ph":"X","name":"ChannelReader::DispatchInputData","args":{"class":64,"line":25},"tdur":0,"tts":1853064},{"cat":"__metadata","pid":28871,"tid":28911,"ts":0,"ph":"M","name":"overhead","args":{"average_overhead":0.015}}] } }"'); // @suppress longLineCheck
+    assert.equal(chunks.length, 1);
+    JSON.parse('[' + chunks.join('') + ']');
+  });
+
+  test('stringAndObjectPayload', function() {
+    var connection = new tv.e.about_tracing.InspectorConnection();
+    connection.setNotificationListener('Tracing.dataCollected',
+        function(message) {
+          assert.typeOf(message, 'string');
+          JSON.parse(message);
+        }
+    );
+    connection.dispatchMessage_('{ "method": "Tracing.dataCollected", "params": { "value": [] } }'); // @suppress longLineCheck
+    connection.dispatchMessage_({'method': 'Tracing.dataCollected', 'params': {'value': [] } }); // @suppress longLineCheck
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/mock_tracing_controller_client.html b/trace-viewer/trace_viewer/extras/about_tracing/mock_tracing_controller_client.html
new file mode 100644
index 0000000..85eb8ef
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/mock_tracing_controller_client.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="/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.about_tracing', function() {
+  function MockTracingControllerClient() {
+    this.requests = [];
+    this.nextRequestIndex = 0;
+    this.allowLooping = false;
+  }
+
+  MockTracingControllerClient.prototype = {
+    __proto__: tv.e.about_tracing.TracingControllerClient.prototype,
+
+    expectRequest: function(method, generateResponse) {
+      var generateResponseCb;
+      if (typeof generateResponse === 'function') {
+        generateResponseCb = generateResponse;
+      } else {
+        generateResponseCb = function() {
+          return generateResponse;
+        };
+      }
+
+      this.requests.push({
+        method: method,
+        generateResponseCb: generateResponseCb});
+    },
+
+    _request: function(method, args) {
+      return new Promise(function(resolve) {
+        var requestIndex = this.nextRequestIndex;
+        if (requestIndex >= this.requests.length)
+          throw new Error('Unhandled request');
+        if (!this.allowLooping) {
+          this.nextRequestIndex++;
+        } else {
+          this.nextRequestIndex = (this.nextRequestIndex + 1) %
+              this.requests.length;
+        }
+
+        var req = this.requests[requestIndex];
+        assertEquals(req.method, method);
+        var resp = req.generateResponseCb(args);
+        resolve(resp);
+      }.bind(this));
+    },
+
+    assertAllRequestsHandled: function() {
+      if (this.allowLooping)
+        throw new Error('Incompatible with allowLooping');
+      assertTrue(this.nextRequestIndex == this.requests.length);
+    },
+
+    beginMonitoring: function(monitoringOptions) {
+      return this._request('beginMonitoring', monitoringOptions);
+    },
+
+    endMonitoring: function() {
+      return this._request('endMonitoring');
+    },
+
+    captureMonitoring: function() {
+      return this._request('captureMonitoring');
+    },
+
+    getMonitoringStatus: function() {
+      return this._request('getMonitoringStatus');
+    },
+
+    getCategories: function() {
+      return this._request('getCategories');
+    },
+
+    beginRecording: function(recordingOptions) {
+      return this._request('beginRecording', recordingOptions);
+    },
+
+    beginGetBufferPercentFull: function() {
+      return this._request('beginGetBufferPercentFull');
+    },
+
+    endRecording: function() {
+      return this._request('endRecording');
+    }
+  };
+
+  return {
+    MockTracingControllerClient: MockTracingControllerClient
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/profiling_view.html b/trace-viewer/trace_viewer/extras/about_tracing/profiling_view.html
new file mode 100644
index 0000000..cf89a6f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/profiling_view.html
@@ -0,0 +1,594 @@
+<!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/about_tracing/record_and_capture_controller.html">
+<link rel="import" href="/extras/about_tracing/inspector_tracing_controller_client.html">
+<link rel="import" href="/extras/about_tracing/xhr_based_tracing_controller_client.html">
+<link rel="import" href="/base/key_event_manager.html">
+<link rel="import" href="/base/ui/info_bar_group.html">
+<link rel="import" href="/base/ui/overlay.html">
+<link rel="import" href="/trace_viewer.html">
+
+<style>
+x-profiling-view {
+  -webkit-flex-direction: column;
+  display: -webkit-flex;
+  padding: 0;
+}
+
+x-profiling-view .controls #save-button {
+  margin-left: 64px !important;
+}
+
+x-profiling-view .controls #upload-button {
+  display: none;
+}
+
+x-profiling-view > x-timeline-view {
+  -webkit-flex: 1 1 auto;
+}
+
+.report-id-message {
+  -webkit-user-select: text;
+}
+
+x-timeline-view-buttons,
+x-timeline-view-buttons > #monitoring-elements {
+  display: flex;
+}
+</style>
+
+<template id="profiling-view-template">
+  <tv-b-ui-info-bar-group></tv-b-ui-info-bar-group>
+  <x-timeline-view>
+    <x-timeline-view-buttons>
+      <button id="record-button">Record</button>
+      <span id="monitoring-elements">
+        <input id="monitor-checkbox" type="checkbox">
+        <label for="monitor-checkbox">Monitoring</label></input>
+        <button id="capture-button">Capture Monitoring Snapshot</button>
+      </span>
+      <button id="save-button">Save</button>
+      <button id="upload-button">Upload</button>
+      <button id="load-button">Load</button>
+    </x-timeline-view-buttons>
+  </x-timeline-view>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview ProfilingView glues the View control to
+ * TracingController.
+ */
+tv.exportTo('tv.e.about_tracing', function() {
+  function readFile(file) {
+    return new Promise(function(resolve, reject) {
+      var reader = new FileReader();
+      var filename = file.name;
+      reader.onload = function(data) {
+        resolve(data.target.result);
+      };
+      reader.onerror = function(err) {
+        reject(err);
+      }
+
+      var is_binary = /[.]gz$/.test(filename) || /[.]zip$/.test(filename);
+      if (is_binary)
+        reader.readAsArrayBuffer(file);
+      else
+        reader.readAsText(file);
+    });
+  }
+
+  /**
+   * ProfilingView
+   * @constructor
+   * @extends {HTMLUnknownElement}
+   */
+  var ProfilingView = tv.b.ui.define('x-profiling-view');
+  var THIS_DOC = document.currentScript.ownerDocument;
+  var REPORT_UPLOAD_URL = 'http://crash-staging/';
+
+  ProfilingView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function(tracingControllerClient) {
+      this.appendChild(tv.b.instantiateTemplate('#profiling-view-template',
+          THIS_DOC));
+
+      this.timelineView_ = this.querySelector('x-timeline-view');
+      tv.b.ui.decorate(this.timelineView_, tv.c.TimelineView);
+
+      this.infoBarGroup_ = this.querySelector('tv-b-ui-info-bar-group');
+
+      // Detach the buttons. We will reattach them to the timeline view.
+      // TODO(nduca): Make timeline-view have a content select="x-buttons"
+      // that pulls in any buttons.
+      var buttons = this.querySelector('x-timeline-view-buttons');
+      buttons.parentElement.removeChild(buttons);
+      this.timelineView_.leftControls.appendChild(buttons);
+      this.initButtons_(buttons);
+
+      tv.b.KeyEventManager.instance.addListener(
+          'keypress', this.onKeypress_, this);
+
+      this.initDragAndDrop_();
+
+      if (tracingControllerClient) {
+        this.tracingControllerClient_ = tracingControllerClient;
+      } else if (window.DevToolsHost !== undefined) {
+        this.tracingControllerClient_ =
+            new tv.e.about_tracing.InspectorTracingControllerClient();
+      } else {
+        this.tracingControllerClient_ =
+            new tv.e.about_tracing.XhrBasedTracingControllerClient();
+      }
+
+      this.isRecording_ = false;
+      this.isMonitoring_ = false;
+      this.activeTrace_ = undefined;
+
+      window.onMonitoringStateChanged = function(is_monitoring) {
+        this.onMonitoringStateChanged_(is_monitoring);
+      }.bind(this);
+
+      window.onUploadError = function(error_message) {
+        this.setUploadOverlayText_(['Trace upload failed: ' + error_message]);
+      }.bind(this);
+      window.onUploadProgress = function(percent, currentAsString,
+                                         totalAsString) {
+        this.setUploadOverlayText_(
+            ['Upload progress: ' + percent + '% (' + currentAsString + ' of ' +
+            currentAsString + ' bytes)']);
+      }.bind(this);
+      window.onUploadComplete = function(reportId) {
+        var messageDiv = document.createElement('div');
+        var textNode = document.createTextNode(
+            'Trace uploaded successfully. Report id: ');
+        messageDiv.appendChild(textNode);
+        var reportLink = document.createElement('a');
+        messageDiv.appendChild(reportLink);
+        reportLink.href = REPORT_UPLOAD_URL + reportId;
+        reportLink.text = reportId;
+        reportLink.className = 'report-id-message';
+        reportLink.target = '_blank';
+        this.setUploadOverlayContent_(messageDiv);
+      }.bind(this);
+
+      this.getMonitoringStatus();
+      this.updateTracingControllerSpecificState_();
+    },
+
+    // Detach all document event listeners. Without this the tests can get
+    // confused as the element may still be listening when the next test runs.
+    detach_: function() {
+      this.detachDragAndDrop_();
+    },
+
+    get isRecording() {
+      return this.isRecording_;
+    },
+
+    get isMonitoring() {
+      return this.isMonitoring_;
+    },
+
+    set tracingControllerClient(tracingControllerClient) {
+      this.tracingControllerClient_ = tracingControllerClient;
+      this.updateTracingControllerSpecificState_();
+    },
+
+    updateTracingControllerSpecificState_: function() {
+      var isInspector = this.tracingControllerClient_ instanceof
+          tv.e.about_tracing.InspectorTracingControllerClient;
+
+      if (isInspector) {
+        this.infoBarGroup_.addMessage(
+            'This about:tracing is connected to a remote device...',
+            [{buttonText: 'Wow!', onClick: function() {}}]);
+      }
+
+      var monitoringElementsEl = this.querySelector('#monitoring-elements');
+      if (isInspector)
+        monitoringElementsEl.style.display = 'none';
+      else
+        monitoringElementsEl.style.display = '';
+    },
+
+    beginRecording: function() {
+      if (this.isRecording_)
+        throw new Error('Already recording');
+      if (this.isMonitoring_)
+        throw new Error('Already monitoring');
+      this.isRecording_ = true;
+      var buttons = this.querySelector('x-timeline-view-buttons');
+      buttons.querySelector('#monitor-checkbox').disabled = true;
+      buttons.querySelector('#monitor-checkbox').checked = false;
+      var resultPromise = tv.e.about_tracing.beginRecording(
+          this.tracingControllerClient_);
+      resultPromise.then(
+          function(data) {
+            this.isRecording_ = false;
+            buttons.querySelector('#monitor-checkbox').disabled = false;
+            this.setActiveTrace('trace.json', data, false);
+          }.bind(this),
+          function(err) {
+            this.isRecording_ = false;
+            buttons.querySelector('#monitor-checkbox').disabled = false;
+            if (err instanceof tv.e.about_tracing.UserCancelledError)
+              return;
+            tv.b.ui.Overlay.showError('Error while recording', err);
+          }.bind(this));
+      return resultPromise;
+    },
+
+    beginMonitoring: function() {
+      if (this.isRecording_)
+        throw new Error('Already recording');
+      if (this.isMonitoring_)
+        throw new Error('Already monitoring');
+      var buttons = this.querySelector('x-timeline-view-buttons');
+      var resultPromise =
+          tv.e.about_tracing.beginMonitoring(this.tracingControllerClient_);
+      resultPromise.then(
+          function() {
+          }.bind(this),
+          function(err) {
+            if (err instanceof tv.e.about_tracing.UserCancelledError)
+              return;
+            tv.b.ui.Overlay.showError('Error while monitoring', err);
+          }.bind(this));
+      return resultPromise;
+    },
+
+    endMonitoring: function() {
+      if (this.isRecording_)
+        throw new Error('Already recording');
+      if (!this.isMonitoring_)
+        throw new Error('Monitoring is disabled');
+      var buttons = this.querySelector('x-timeline-view-buttons');
+      var resultPromise =
+          tv.e.about_tracing.endMonitoring(this.tracingControllerClient_);
+      resultPromise.then(
+          function() {
+          }.bind(this),
+          function(err) {
+            if (err instanceof tv.e.about_tracing.UserCancelledError)
+              return;
+            tv.b.ui.Overlay.showError('Error while monitoring', err);
+          }.bind(this));
+      return resultPromise;
+    },
+
+    captureMonitoring: function() {
+      if (!this.isMonitoring_)
+        throw new Error('Monitoring is disabled');
+      var resultPromise =
+          tv.e.about_tracing.captureMonitoring(this.tracingControllerClient_);
+      resultPromise.then(
+          function(data) {
+            this.setActiveTrace('trace.json', data, true);
+          }.bind(this),
+          function(err) {
+            if (err instanceof tv.e.about_tracing.UserCancelledError)
+              return;
+            tv.b.ui.Overlay.showError('Error while monitoring', err);
+          }.bind(this));
+      return resultPromise;
+    },
+
+    getMonitoringStatus: function() {
+      var resultPromise =
+          tv.e.about_tracing.getMonitoringStatus(this.tracingControllerClient_);
+      resultPromise.then(
+          function(status) {
+            this.onMonitoringStateChanged_(status.isMonitoring);
+          }.bind(this),
+          function(err) {
+            if (err instanceof tv.e.about_tracing.UserCancelledError)
+              return;
+            tv.b.ui.Overlay.showError('Error while updating tracing states',
+                                      err);
+          }.bind(this));
+      return resultPromise;
+    },
+
+    onMonitoringStateChanged_: function(is_monitoring) {
+      this.isMonitoring_ = is_monitoring;
+      var buttons = this.querySelector('x-timeline-view-buttons');
+      buttons.querySelector('#record-button').disabled = is_monitoring;
+      buttons.querySelector('#capture-button').disabled = !is_monitoring;
+      buttons.querySelector('#monitor-checkbox').checked = is_monitoring;
+    },
+
+    onKeypress_: function(event) {
+      if (document.activeElement.nodeName === 'INPUT')
+        return;
+
+      if (!this.isRecording &&
+          event.keyCode === 'r'.charCodeAt(0)) {
+        this.beginRecording();
+        event.preventDefault();
+        event.stopPropagation();
+        return true;
+      }
+    },
+
+    get timelineView() {
+      return this.timelineView_;
+    },
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    clearActiveTrace: function() {
+      this.saveButton_.disabled = true;
+      this.uploadButton_.disabled = true;
+      this.activeTrace_ = undefined;
+    },
+
+    setActiveTrace: function(filename, data) {
+      this.activeTrace_ = {
+        filename: filename,
+        data: data
+      };
+
+      this.infoBarGroup_.clearMessages();
+      this.updateTracingControllerSpecificState_();
+      this.saveButton_.disabled = false;
+      this.uploadButton_.disabled = false;
+      this.timelineView_.viewTitle = filename;
+
+      var m = new tv.c.TraceModel();
+      var p = m.importTracesWithProgressDialog([data], true);
+      p.then(
+          function() {
+            this.timelineView_.model = m;
+            this.timelineView_.updateDocumentFavicon();
+          }.bind(this),
+          function(err) {
+            tv.b.ui.Overlay.showError('While importing: ', err);
+          }.bind(this));
+    },
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    initButtons_: function(buttons) {
+      buttons.querySelector('#record-button').addEventListener(
+          'click', function(event) {
+            event.stopPropagation();
+            this.beginRecording();
+          }.bind(this));
+
+      buttons.querySelector('#monitor-checkbox').addEventListener(
+          'click', function(event) {
+            event.stopPropagation();
+            if (this.isMonitoring_)
+              this.endMonitoring();
+            else
+              this.beginMonitoring();
+          }.bind(this));
+
+      buttons.querySelector('#capture-button').addEventListener(
+          'click', function(event) {
+            event.stopPropagation();
+            this.captureMonitoring();
+          }.bind(this));
+      buttons.querySelector('#capture-button').disabled = true;
+
+      buttons.querySelector('#load-button').addEventListener(
+          'click', function(event) {
+            event.stopPropagation();
+            this.onLoadClicked_();
+          }.bind(this));
+
+      this.saveButton_ = buttons.querySelector('#save-button');
+      this.saveButton_.addEventListener('click',
+                                        this.onSaveClicked_.bind(this));
+      this.saveButton_.disabled = true;
+
+      this.uploadButton_ = buttons.querySelector('#upload-button');
+      this.uploadButton_.addEventListener('click',
+                                          this.onUploadClicked_.bind(this));
+      if (typeof(chrome.send) === 'function') {
+        this.uploadButton_.style.display = 'inline-block';
+      }
+      this.uploadButton_.disabled = true;
+      this.uploadOverlay_ = null;
+    },
+
+    requestFilename_: function() {
+
+      // unsafe filename patterns:
+      var illegalRe = /[\/\?<>\\:\*\|":]/g;
+      var controlRe = /[\x00-\x1f\x80-\x9f]/g;
+      var reservedRe = /^\.+$/;
+
+      var filename;
+      var defaultName = this.activeTrace_.filename;
+      var custom = prompt('Filename? (.json appended) Or leave blank:');
+      if (custom === null)
+        return undefined;
+
+      if (custom) {
+        filename = defaultName.replace(/\.json$/, ' ' + custom) + '.json';
+      } else {
+        var date = new Date();
+        var dateText = ' ' + date.toDateString() +
+                       ' ' + date.toLocaleTimeString();
+        filename = defaultName.replace(/\.json$/, dateText + '.json');
+      }
+
+      return filename
+              .replace(illegalRe, '.')
+              .replace(controlRe, '•')
+              .replace(reservedRe, '')
+              .replace(/\s+/g, '_');
+    },
+
+    onSaveClicked_: function() {
+      // Create a blob URL from the binary array.
+      var blob = new Blob([this.activeTrace_.data],
+                          {type: 'application/octet-binary'});
+      var blobUrl = window.webkitURL.createObjectURL(blob);
+
+      // Create a link and click on it. BEST API EVAR!
+      var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+      link.href = blobUrl;
+      var filename = this.requestFilename_();
+      if (filename) {
+        link.download = filename;
+        link.click();
+      }
+    },
+
+    onUploadClicked_: function() {
+      if (this.uploadOverlay_) {
+        throw new Error('Already uploading');
+      }
+      this.initUploadStatusOverlay_();
+    },
+
+    initUploadStatusOverlay_: function() {
+      this.uploadOverlay_ = tv.b.ui.Overlay();
+      this.uploadOverlay_.title = 'Uploading trace...';
+      this.uploadOverlay_.userCanClose = false;
+      this.uploadOverlay_.visible = true;
+
+      this.setUploadOverlayText_([
+        'You are about to upload trace data to Google server.',
+        'Would you like to proceed?'
+      ]);
+      var okButton = document.createElement('button');
+      okButton.textContent = 'Ok';
+      okButton.addEventListener('click', this.doTraceUpload_.bind(this));
+      this.uploadOverlay_.buttons.appendChild(okButton);
+
+      var cancelButton = document.createElement('button');
+      cancelButton.textContent = 'Cancel';
+      cancelButton.addEventListener('click',
+                                    this.hideUploadOverlay_.bind(this));
+      this.uploadOverlay_.buttons.appendChild(cancelButton);
+    },
+
+    setUploadOverlayContent_: function(content) {
+      if (!this.uploadOverlay_)
+        throw new Error('Not uploading');
+
+      this.uploadOverlay_.textContent = '';
+      this.uploadOverlay_.appendChild(content);
+    },
+
+    setUploadOverlayText_: function(messages) {
+      var contentDiv = document.createElement('div');
+
+      for (var i = 0; i < messages.length; ++i) {
+        var messageDiv = document.createElement('div');
+        messageDiv.textContent = messages[i];
+        contentDiv.appendChild(messageDiv);
+      }
+      this.setUploadOverlayContent_(contentDiv);
+    },
+
+    doTraceUpload_: function() {
+      this.setUploadOverlayText_(['Uploading trace data...']);
+      this.uploadOverlay_.buttons.removeChild(
+          this.uploadOverlay_.buttons.firstChild);
+      this.uploadOverlay_.buttons.firstChild.textContent = 'Close';
+      chrome.send('doUpload', [this.activeTrace_.data]);
+    },
+
+    hideUploadOverlay_: function() {
+      if (!this.uploadOverlay_)
+        throw new Error('Not uploading');
+
+      this.uploadOverlay_.visible = false;
+      this.uploadOverlay_ = null;
+    },
+
+    onLoadClicked_: function() {
+      var inputElement = document.createElement('input');
+      inputElement.type = 'file';
+      inputElement.multiple = false;
+
+      var changeFired = false;
+      inputElement.addEventListener(
+          'change',
+          function(e) {
+            if (changeFired)
+              return;
+            changeFired = true;
+
+            var file = inputElement.files[0];
+            readFile(file).then(
+                function(data) {
+                  this.setActiveTrace(file.name, data);
+                }.bind(this),
+                function(err) {
+                  tv.b.ui.Overlay.showError('Error while loading file: ' + err);
+                });
+          }.bind(this), false);
+      inputElement.click();
+    },
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    initDragAndDrop_: function() {
+      this.dropHandler_ = this.dropHandler_.bind(this);
+      this.ignoreDragEvent_ = this.ignoreDragEvent_.bind(this);
+      document.addEventListener('dragstart', this.ignoreDragEvent_, false);
+      document.addEventListener('dragend', this.ignoreDragEvent_, false);
+      document.addEventListener('dragenter', this.ignoreDragEvent_, false);
+      document.addEventListener('dragleave', this.ignoreDragEvent_, false);
+      document.addEventListener('dragover', this.ignoreDragEvent_, false);
+      document.addEventListener('drop', this.dropHandler_, false);
+    },
+
+    detachDragAndDrop_: function() {
+      document.removeEventListener('dragstart', this.ignoreDragEvent_);
+      document.removeEventListener('dragend', this.ignoreDragEvent_);
+      document.removeEventListener('dragenter', this.ignoreDragEvent_);
+      document.removeEventListener('dragleave', this.ignoreDragEvent_);
+      document.removeEventListener('dragover', this.ignoreDragEvent_);
+      document.removeEventListener('drop', this.dropHandler_);
+    },
+
+    ignoreDragEvent_: function(e) {
+      e.preventDefault();
+      return false;
+    },
+
+    dropHandler_: function(e) {
+      if (this.isAnyDialogUp_)
+        return;
+
+      e.stopPropagation();
+      e.preventDefault();
+
+      var files = e.dataTransfer.files;
+      if (files.length !== 1) {
+        tv.b.ui.Overlay.showError('1 file supported at a time.');
+        return;
+      }
+
+      readFile(files[0]).then(
+          function(data) {
+            this.setActiveTrace(files[0].name, data);
+          }.bind(this),
+          function(err) {
+            tv.b.ui.Overlay.showError('Error while loading file: ' + err);
+          });
+      return false;
+    }
+  };
+
+  return {
+    ProfilingView: ProfilingView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/profiling_view_test.html b/trace-viewer/trace_viewer/extras/about_tracing/profiling_view_test.html
new file mode 100644
index 0000000..32b40ee
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/profiling_view_test.html
@@ -0,0 +1,190 @@
+<!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/about_tracing/mock_tracing_controller_client.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/extras/about_tracing/profiling_view.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var testData = [
+    {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'},
+    {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'},
+    {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'},
+    {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'}
+  ];
+
+  var monitoringOptions = {
+    isMonitoring: false,
+    categoryFilter: '*',
+    useSystemTracing: false,
+    useContinuousTracing: false,
+    useSampling: false
+  };
+
+  var ProfilingView = tv.e.about_tracing.ProfilingView;
+
+  test('recording', function() {
+    var mock = new tv.e.about_tracing.MockTracingControllerClient();
+    mock.allowLooping = true;
+    mock.expectRequest('getMonitoringStatus', function() {
+      return btoa(JSON.stringify(monitoringOptions));
+    });
+    mock.expectRequest('endRecording', function() {
+      return '';
+    });
+    mock.expectRequest('getCategories', function() {
+      return JSON.stringify(['a', 'b', 'c']);
+    });
+    mock.expectRequest('beginRecording', function(data) {
+      return '';
+    });
+    mock.expectRequest('endRecording', function(data) {
+      return JSON.stringify(testData);
+    });
+
+    var view = new ProfilingView(mock);
+    view.style.height = '400px';
+    view.style.border = '1px solid black';
+    this.addHTMLOutput(view);
+
+    return new Promise(function(resolve, reject) {
+      var recordingPromise = view.beginRecording();
+      function pressRecord() {
+        recordingPromise.selectionDlg.clickRecordButton();
+        setTimeout(pressStop, 60);
+      }
+      function pressStop() {
+        recordingPromise.progressDlg.clickStopButton();
+      }
+      setTimeout(pressRecord, 60);
+      recordingPromise.then(
+          function() {
+            resolve();
+          },
+          function() {
+            reject();
+          });
+    });
+  });
+
+  test('monitoring', function() {
+    var mock = new tv.e.about_tracing.MockTracingControllerClient();
+    mock.allowLooping = true;
+    mock.expectRequest('getMonitoringStatus', function() {
+      return btoa(JSON.stringify(monitoringOptions));
+    });
+    mock.expectRequest('beginMonitoring', function(data) {
+      return '';
+    });
+    mock.expectRequest('captureMonitoring', function(data) {
+      return JSON.stringify(testData);
+    });
+    mock.expectRequest('endMonitoring', function(data) {
+      return '';
+    });
+
+    var view = new ProfilingView(mock);
+    view.style.height = '400px';
+    view.style.border = '1px solid black';
+    this.addHTMLOutput(view);
+
+    return new Promise(function(resolve, reject) {
+      var buttons = view.querySelector('x-timeline-view-buttons');
+      assert.isFalse(buttons.querySelector('#monitor-checkbox').checked);
+
+      function beginMonitoring() {
+        // Since we don't fall back to TracingController when testing,
+        // we cannot rely on TracingController to invoke a callback to change
+        // view.isMonitoring_. Thus we change view.isMonitoring_ manually.
+        view.onMonitoringStateChanged_(true);
+        assert.isTrue(buttons.querySelector('#monitor-checkbox').checked);
+        setTimeout(captureMonitoring, 60);
+      }
+
+      function captureMonitoring() {
+        assert.isTrue(buttons.querySelector('#monitor-checkbox').checked);
+        buttons.querySelector('#capture-button').click();
+        setTimeout(endMonitoring, 60);
+      }
+      function endMonitoring() {
+        assert.isTrue(buttons.querySelector('#monitor-checkbox').checked);
+        buttons.querySelector('#monitor-checkbox').click();
+        assert.isFalse(buttons.querySelector('#monitor-checkbox').checked);
+      }
+
+      var monitoringPromise = view.beginMonitoring();
+      setTimeout(beginMonitoring, 60);
+
+      monitoringPromise.then(
+          resolve,
+          reject);
+    });
+  });
+
+  test('upload', function() {
+    var mock = new tv.e.about_tracing.MockTracingControllerClient();
+    mock.allowLooping = true;
+    mock.expectRequest('getMonitoringStatus', function() {
+      return btoa(JSON.stringify(monitoringOptions));
+    });
+    var view = new ProfilingView(mock);
+    this.addHTMLOutput(view);
+    var buttons = view.querySelector('x-timeline-view-buttons');
+    var uploadButton = buttons.querySelector('#upload-button');
+    assert.isNotNull(uploadButton);
+    assert.isTrue(uploadButton.disabled);
+    assert.isNull(view.uploadOverlay_);
+
+    view.setActiveTrace('testFile', []);
+    view.activeTrace_.data = ['t', 'e', 's', 't'];
+    assert.isFalse(uploadButton.disabled);
+
+    var overlay = null;
+    var clickUploadAndVerify = function() {
+      view.onUploadClicked_();
+      assert.isNotNull(view.uploadOverlay_);
+      assert.isTrue(view.uploadOverlay_.visible);
+      overlay = view.uploadOverlay_;
+      assert.notEqual(view.uploadOverlay_.buttons.style.display, 'none');
+      assert.equal(view.uploadOverlay_.buttons.childNodes.length, 2);
+    };
+    clickUploadAndVerify();
+
+    var cancelButton = view.uploadOverlay_.buttons.lastChild;
+    assert.equal(cancelButton.textContent, 'Cancel');
+    cancelButton.click();
+    assert.isNull(view.uploadOverlay_);
+    assert.isFalse(overlay.visible);
+
+    clickUploadAndVerify();
+    var okButton = view.uploadOverlay_.buttons.firstChild;
+    assert.equal(okButton.textContent, 'Ok');
+    var commandSent = null;
+    var dataSent = null;
+    chrome.send = function(command, data) {
+      commandSent = command;
+      dataSent = data;
+    };
+    okButton.click();
+    assert.equal(commandSent, 'doUpload');
+    assert.equal(dataSent[0], view.activeTrace_.data);
+
+    assert.isTrue(view.uploadOverlay_.visible);
+    overlay = view.uploadOverlay_;
+    assert.equal(view.uploadOverlay_.buttons.childNodes.length, 1);
+    var closeButton = view.uploadOverlay_.buttons.childNodes[0];
+    assert.equal(closeButton.textContent, 'Close');
+    closeButton.click();
+    assert.isNull(view.uploadOverlay_);
+    assert.isFalse(overlay.visible);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/record_and_capture_controller.html b/trace-viewer/trace_viewer/extras/about_tracing/record_and_capture_controller.html
new file mode 100644
index 0000000..ba6f0d0
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/record_and_capture_controller.html
@@ -0,0 +1,240 @@
+<!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/about_tracing/record_selection_dialog.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.about_tracing', function() {
+  function beginMonitoring(tracingControllerClient) {
+    var finalPromiseResolver;
+    var finalPromise = new Promise(function(resolve, reject) {
+      finalPromiseResolver = {
+        resolve: resolve,
+        reject: reject
+      };
+    });
+
+    // TODO(haraken): Implement a configure dialog to set these options.
+    var monitoringOptions = {
+      categoryFilter: '*',
+      useSystemTracing: false,
+      tracingRecordMode: 'record-until-full',
+      useSampling: true
+    };
+
+
+    var beginMonitoringPromise = tracingControllerClient.beginMonitoring(
+        monitoringOptions);
+
+    beginMonitoringPromise.then(
+        function() {
+          finalPromiseResolver.resolve();
+        },
+        function(err) {
+          finalPromiseResolver.reject(err);
+        });
+
+    return finalPromise;
+  }
+
+  function endMonitoring(tracingControllerClient) {
+    var endMonitoringPromise = tracingControllerClient.endMonitoring();
+    return endMonitoringPromise.then(
+        function() {
+        },
+        function(err) {
+        });
+  }
+
+  function captureMonitoring(tracingControllerClient) {
+    var captureMonitoringPromise =
+        tracingControllerClient.captureMonitoring();
+    return captureMonitoringPromise;
+  }
+
+  function getMonitoringStatus(tracingControllerClient) {
+    var getMonitoringStatusPromise =
+        tracingControllerClient.getMonitoringStatus();
+    return getMonitoringStatusPromise;
+  }
+
+  function beginRecording(tracingControllerClient) {
+    var defaultTitle = document.title;
+    var toggleRecordingIndicator = false;
+    var finalPromiseResolver;
+    var finalPromise = new Promise(function(resolve, reject) {
+      finalPromiseResolver = {
+        resolve: resolve,
+        reject: reject
+      };
+    });
+    finalPromise.selectionDlg = undefined;
+    finalPromise.progressDlg = undefined;
+
+    function beginRecordingError(err) {
+      finalPromiseResolver.reject(err);
+    }
+
+    // Step 0: End recording. This is necessary when the user reloads the
+    // about:tracing page when we are recording. Window.onbeforeunload is not
+    // reliable to end recording on reload.
+    endRecording(tracingControllerClient).then(
+        getCategories,
+        getCategories);  // Ignore error.
+
+    // But just in case, bind onbeforeunload anyway.
+    window.onbeforeunload = function(e) {
+      endRecording(tracingControllerClient);
+    }
+
+    // Step 1: Get categories.
+    function getCategories() {
+      tracingControllerClient.getCategories().then(
+          showTracingDialog,
+          beginRecordingError);
+    }
+
+    // Step 2: Show tracing dialog.
+    var selectionDlg;
+    function showTracingDialog(categories) {
+      selectionDlg = new tv.e.about_tracing.RecordSelectionDialog();
+      selectionDlg.categories = categories;
+      selectionDlg.settings_key = 'tv.e.about_tracing.record_selection_dialog';
+      selectionDlg.addEventListener('recordclick', startTracing);
+      selectionDlg.addEventListener('closeclick', cancelRecording);
+      selectionDlg.visible = true;
+
+      finalPromise.selectionDlg = selectionDlg;
+    }
+
+    function cancelRecording() {
+      finalPromise.selectionDlg = undefined;
+      finalPromiseResolver.reject(new UserCancelledError());
+    }
+
+    // Step 2: Do the actual tracing dialog.
+    var progressDlg;
+    var bufferPercentFullDiv;
+    function startTracing() {
+      progressDlg = new tv.b.ui.Overlay();
+      progressDlg.textContent = 'Recording...';
+      progressDlg.userCanClose = false;
+
+      bufferPercentFullDiv = document.createElement('div');
+      progressDlg.appendChild(bufferPercentFullDiv);
+
+      var stopButton = document.createElement('button');
+      stopButton.textContent = 'Stop';
+      progressDlg.clickStopButton = function() {
+        stopButton.click();
+      };
+      progressDlg.appendChild(stopButton);
+
+      var recordingOptions = {
+        categoryFilter: selectionDlg.categoryFilter(),
+        useSystemTracing: selectionDlg.useSystemTracing,
+        tracingRecordMode: selectionDlg.tracingRecordMode,
+        useSampling: selectionDlg.useSampling
+      };
+
+
+      var requestPromise = tracingControllerClient.beginRecording(
+          recordingOptions);
+      requestPromise.then(
+          function() {
+            progressDlg.visible = true;
+            stopButton.focus();
+            updateBufferPercentFull('0');
+          },
+          recordFailed);
+
+      stopButton.addEventListener('click', function() {
+        // TODO(chrishenry): Currently, this only dismiss the progress
+        // dialog when tracingComplete event is received. When performing
+        // remote debugging, the tracingComplete event may be delayed
+        // considerable. We should indicate to user that we are waiting
+        // for tracingComplete event instead of being unresponsive. (For
+        // now, I disable the "stop" button, since clicking on the button
+        // again now cause exception.)
+        var recordingPromise = endRecording(tracingControllerClient);
+        recordingPromise.then(
+            recordFinished,
+            recordFailed);
+        stopButton.disabled = true;
+        bufferPercentFullDiv = undefined;
+        document.title = defaultTitle;
+      });
+      finalPromise.progressDlg = progressDlg;
+    }
+
+    function recordFinished(tracedData) {
+      progressDlg.visible = false;
+      finalPromise.progressDlg = undefined;
+      finalPromiseResolver.resolve(tracedData);
+    }
+
+    function recordFailed(err) {
+      progressDlg.visible = false;
+      finalPromise.progressDlg = undefined;
+      finalPromiseResolver.reject(err);
+    }
+
+    function getBufferPercentFull() {
+      if (!bufferPercentFullDiv)
+        return;
+
+      tracingControllerClient.beginGetBufferPercentFull().then(
+          updateBufferPercentFull);
+    }
+
+    function updateBufferPercentFull(percent_full) {
+      if (!bufferPercentFullDiv)
+        return;
+
+      percent_full = Math.round(100 * parseFloat(percent_full));
+      var newText = 'Buffer usage: ' + percent_full + '%';
+      if (bufferPercentFullDiv.textContent != newText)
+        bufferPercentFullDiv.textContent = newText;
+
+      document.title = 'tracing: ' + percent_full + '%';
+      if (toggleRecordingIndicator)
+        document.title = document.title + ' \u25AA';
+      if (percent_full < 100)
+          toggleRecordingIndicator = !toggleRecordingIndicator;
+      else
+          toggleRecordingIndicator = false;
+
+      window.setTimeout(getBufferPercentFull, 500);
+    }
+
+    // Thats it! We're done.
+    return finalPromise;
+  };
+
+  function endRecording(tracingControllerClient) {
+    return tracingControllerClient.endRecording();
+  }
+
+  function UserCancelledError() {
+    Error.apply(this, arguments);
+  }
+  UserCancelledError.prototype = {
+    __proto__: Error.prototype
+  };
+
+  return {
+    beginRecording: beginRecording,
+    beginMonitoring: beginMonitoring,
+    endMonitoring: endMonitoring,
+    captureMonitoring: captureMonitoring,
+    getMonitoringStatus: getMonitoringStatus,
+    UserCancelledError: UserCancelledError
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/record_and_capture_controller_test.html b/trace-viewer/trace_viewer/extras/about_tracing/record_and_capture_controller_test.html
new file mode 100644
index 0000000..3449230
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/record_and_capture_controller_test.html
@@ -0,0 +1,108 @@
+<!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/about_tracing/mock_tracing_controller_client.html">
+<link rel="import" href="/extras/about_tracing/record_and_capture_controller.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var testData = [
+    {name: 'a', args: {}, pid: 52, ts: 15000, cat: 'foo', tid: 53, ph: 'B'},
+    {name: 'a', args: {}, pid: 52, ts: 19000, cat: 'foo', tid: 53, ph: 'E'},
+    {name: 'b', args: {}, pid: 52, ts: 32000, cat: 'foo', tid: 53, ph: 'B'},
+    {name: 'b', args: {}, pid: 52, ts: 54000, cat: 'foo', tid: 53, ph: 'E'}
+  ];
+
+  test('fullRecording', function() {
+    return new Promise(function(resolve, reject) {
+      var mock = new tv.e.about_tracing.MockTracingControllerClient();
+      mock.expectRequest('endRecording', function() {
+        return '';
+      });
+      mock.expectRequest('getCategories', function() {
+        setTimeout(function() {
+          recordingPromise.selectionDlg.clickRecordButton();
+        }, 20);
+        return JSON.stringify(['a', 'b', 'c']);
+      });
+      mock.expectRequest('beginRecording', function(recordingOptions) {
+        assert.typeOf(recordingOptions.categoryFilter, 'string');
+        assert.typeOf(recordingOptions.useSystemTracing, 'boolean');
+        assert.typeOf(recordingOptions.useSampling, 'boolean');
+        assert.typeOf(recordingOptions.tracingRecordMode, 'string');
+        setTimeout(function() {
+          recordingPromise.progressDlg.clickStopButton();
+        }, 10);
+        return '';
+      });
+      mock.expectRequest('endRecording', function(data) {
+        return JSON.stringify(testData);
+      });
+
+      var recordingPromise = tv.e.about_tracing.beginRecording(mock);
+
+      return recordingPromise.then(
+          function(data) {
+            mock.assertAllRequestsHandled();
+            var testDataString = JSON.stringify(testData);
+            assert.equal(data, testDataString);
+            resolve();
+          },
+          function(error) {
+            reject('This should never be reached');
+          });
+    });
+  });
+
+  test('monitoring', function() {
+    return new Promise(function(resolve, reject) {
+      var mock = new tv.e.about_tracing.MockTracingControllerClient();
+
+      mock.expectRequest('beginMonitoring', function(monitoringOptions) {
+        assert.typeOf(monitoringOptions.categoryFilter, 'string');
+        assert.typeOf(monitoringOptions.useSystemTracing, 'boolean');
+        assert.typeOf(monitoringOptions.useSampling, 'boolean');
+        assert.typeOf(monitoringOptions.tracingRecordMode, 'string');
+        setTimeout(function() {
+          var capturePromise = tv.e.about_tracing.captureMonitoring(mock);
+          capturePromise.then(
+              function(data) {
+                var testDataString = JSON.stringify(testData);
+                assert.equal(data, testDataString);
+              },
+              function(error) {
+                reject();
+              });
+        }, 10);
+        return '';
+      });
+
+      mock.expectRequest('captureMonitoring', function(data) {
+        setTimeout(function() {
+          var endPromise = tv.e.about_tracing.endMonitoring(mock);
+          endPromise.then(
+              function(data) {
+                mock.assertAllRequestsHandled();
+                resolve();
+              },
+              function(error) {
+                reject();
+              });
+        }, 10);
+        return JSON.stringify(testData);
+      });
+
+      mock.expectRequest('endMonitoring', function(data) {
+      });
+
+      tv.e.about_tracing.beginMonitoring(mock);
+    });
+  });
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/record_selection_dialog.html b/trace-viewer/trace_viewer/extras/about_tracing/record_selection_dialog.html
new file mode 100644
index 0000000..8b801ef
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/record_selection_dialog.html
@@ -0,0 +1,644 @@
+<!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="/base/utils.html">
+<link rel="import" href="/base/ui/overlay.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/info_bar.html">
+
+<template id="record-selection-dialog-template">
+  <style>
+  .categories-column-view {
+    display: -webkit-flex;
+    -webkit-flex-direction: column;
+    font-family: sans-serif;
+    max-width: 640px;
+    min-height: 0;
+    min-width: 0;
+    opacity: 1;
+    transition: max-height 1s ease, max-width 1s ease, opacity 1s ease;
+    will-change: opacity;
+  }
+
+  .categories-column-view-hidden {
+    max-height: 0;
+    max-width: 0;
+    opacity: 0;
+    overflow: hidden;
+  }
+
+  .categories-selection {
+    display: -webkit-flex;
+    -webkit-flex-direction: row;
+  }
+
+  .category-presets {
+    padding: 4px;
+  }
+
+  .category-description {
+    color: #aaa;
+    font-size: small;
+    max-height: 1em;
+    opacity: 1;
+    padding-left: 4px;
+    padding-right: 4px;
+    text-align: right;
+    transition: max-height 1s ease, opacity 1s ease;
+    will-change: opacity;
+  }
+
+  .category-description-hidden {
+    max-height: 0;
+    opacity: 0;
+  }
+
+  .default-enabled-categories,
+  .default-disabled-categories {
+    -webkit-flex: 1 1 auto;
+    display: -webkit-flex;
+    -webkit-flex-direction: column;
+    padding: 4px;
+    width: 300px;
+  }
+
+  .default-enabled-categories > div,
+  .default-disabled-categories > div {
+    padding: 4px;
+  }
+
+  .tracing-modes {
+    -webkit-flex: 1 0 auto;
+    display: -webkit-flex;
+    -webkit-flex-direction: reverse;
+    padding: 4px;
+    border-bottom: 2px solid #ddd;
+    border-top: 2px solid #ddd;
+  }
+
+  .default-disabled-categories {
+    border-left: 2px solid #ddd;
+  }
+
+  .warning-default-disabled-categories {
+    display: inline-block;
+    font-weight: bold;
+    text-align: center;
+    color: #BD2E2E;
+    width: 2.0ex;
+    height: 2.0ex;
+    border-radius: 2.0ex;
+    border: 1px solid #BD2E2E;
+  }
+
+  .categories {
+    font-size: 80%;
+    padding: 10px;
+    -webkit-flex: 1 1 auto;
+  }
+
+  .group-selectors {
+    font-size: 80%;
+    border-bottom: 1px solid #ddd;
+    padding-bottom: 6px;
+    -webkit-flex: 0 0 auto;
+  }
+
+  .group-selectors button {
+    padding: 1px;
+  }
+  </style>
+
+  <div class="record-selection-dialog">
+    <tv-b-ui-info-bar-group></tv-b-ui-info-bar-group>
+    <div class="category-presets">
+    </div>
+    <div class="category-description"></div>
+    <div class="categories-column-view">
+      <div class="tracing-modes"></div>
+      <div class="categories-selection">
+        <div class="default-enabled-categories">
+          <div>Record&nbsp;Categories</div>
+          <div class="group-selectors">
+            Select
+            <button class="all-btn">All</button>
+            <button class="none-btn">None</button>
+          </div>
+          <div class="categories"></div>
+        </div>
+        <div class="default-disabled-categories">
+          <div>Disabled&nbsp;by&nbsp;Default&nbsp;Categories
+            <a class="warning-default-disabled-categories">!</a>
+          </div>
+          <div class="group-selectors">
+            Select
+            <button class="all-btn">All</button>
+            <button class="none-btn">None</button>
+          </div>
+          <div class="categories"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview RecordSelectionDialog presents the available categories
+ * to be enabled/disabled during tv.c.
+ */
+tv.exportTo('tv.e.about_tracing', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+  var RecordSelectionDialog = tv.b.ui.define('div');
+
+  var DEFAULT_PRESETS = [
+    {title: 'Web developer',
+      categoryFilter: ['blink', 'cc', 'netlog', 'renderer.scheduler',
+        'toplevel', 'v8']},
+    {title: 'Input latency',
+      categoryFilter: ['benchmark', 'input', 'renderer.scheduler', 'toplevel']},
+    {title: 'Rendering',
+      categoryFilter: ['blink', 'cc', 'gpu', 'toplevel']},
+    {title: 'Javascript and rendering',
+      categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', 'v8',
+        'toplevel']},
+    {title: 'Frame Viewer',
+      categoryFilter: ['blink', 'cc', 'gpu', 'renderer.scheduler', 'v8',
+        'toplevel',
+        'disabled-by-default-cc.debug',
+        'disabled-by-default-cc.debug.picture']},
+    {title: 'Manually select settings',
+      categoryFilter: []}
+  ];
+  var RECORDING_MODES = [
+      {'label': 'Record until full',
+        'value': 'record-until-full'},
+      {'label': 'Record continuously',
+        'value': 'record-continuously'},
+      {'label': 'Record as much as possible',
+        'value': 'record-as-much-as-possible'}];
+  var DEFAULT_RECORD_MODE = 'record-as-much-as-possible';
+  var DEFAULT_CONTINUOUS_TRACING = true;
+  var DEFAULT_SYSTEM_TRACING = false;
+  var DEFAULT_SAMPLING_TRACING = false;
+
+  RecordSelectionDialog.prototype = {
+    __proto__: tv.b.ui.Overlay.prototype,
+
+    decorate: function() {
+      tv.b.ui.Overlay.prototype.decorate.call(this);
+      this.title = 'Record a new trace...';
+
+      this.classList.add('record-dialog-overlay');
+
+      var node = tv.b.instantiateTemplate('#record-selection-dialog-template',
+          THIS_DOC);
+      this.appendChild(node);
+
+      this.recordButtonEl_ = document.createElement('button');
+      this.recordButtonEl_.textContent = 'Record';
+      this.recordButtonEl_.addEventListener(
+          'click',
+          this.onRecordButtonClicked_.bind(this));
+      this.recordButtonEl_.style.fontSize = '110%';
+      this.buttons.appendChild(this.recordButtonEl_);
+
+      this.categoriesView_ = this.querySelector('.categories-column-view');
+      this.presetsEl_ = this.querySelector('.category-presets');
+      this.presetsEl_.appendChild(tv.b.ui.createOptionGroup(
+          this, 'currentlyChosenPreset',
+          'about_tracing.record_selection_dialog_preset',
+          DEFAULT_PRESETS[0].categoryFilter,
+          DEFAULT_PRESETS.map(function(p) {
+            return { label: p.title, value: p.categoryFilter };
+          })));
+
+      this.tracingRecordModeSltr_ = tv.b.ui.createSelector(
+          this, 'tracingRecordMode',
+          'recordSelectionDialog.tracingRecordMode',
+          DEFAULT_RECORD_MODE, RECORDING_MODES);
+
+      this.systemTracingBn_ = tv.b.ui.createCheckBox(
+          undefined, undefined,
+          'recordSelectionDialog.useSystemTracing', true,
+          'System tracing');
+      this.samplingTracingBn_ = tv.b.ui.createCheckBox(
+          undefined, undefined,
+          'recordSelectionDialog.useSampling', false,
+          'State sampling');
+      this.tracingModesContainerEl_ = this.querySelector('.tracing-modes');
+      this.tracingModesContainerEl_.appendChild(this.tracingRecordModeSltr_);
+      this.tracingModesContainerEl_.appendChild(this.systemTracingBn_);
+      this.tracingModesContainerEl_.appendChild(this.samplingTracingBn_);
+
+
+      this.enabledCategoriesContainerEl_ =
+          this.querySelector('.default-enabled-categories .categories');
+
+      this.disabledCategoriesContainerEl_ =
+          this.querySelector('.default-disabled-categories .categories');
+
+      this.createGroupSelectButtons_(
+          this.querySelector('.default-enabled-categories'));
+      this.createGroupSelectButtons_(
+          this.querySelector('.default-disabled-categories'));
+      this.createDefaultDisabledWarningDialog_(
+          this.querySelector('.warning-default-disabled-categories'));
+      this.editCategoriesOpened_ = false;
+
+      // TODO(chrishenry): When used with tv.b.ui.Overlay (such as in
+      // chrome://tracing, this does not yet look quite right due to
+      // the 10px overlay content padding (but it's good enough).
+      this.infoBarGroup_ = this.querySelector('tv-b-ui-info-bar-group');
+
+      this.addEventListener('visibleChange', this.onVisibleChange_.bind(this));
+    },
+
+    set supportsSystemTracing(s) {
+      if (s) {
+        this.systemTracingBn_.style.display = undefined;
+      } else {
+        this.systemTracingBn_.style.display = 'none';
+        this.useSystemTracing = false;
+      }
+    },
+
+    get tracingRecordMode() {
+      if (this.usingPreset_())
+        return DEFAULT_RECORD_MODE;
+      return this.tracingRecordModeSltr_.selectedValue;
+    },
+    set tracingRecordMode(value) {
+      this.tracingRecordMode_ = value;
+    },
+
+    get useSystemTracing() {
+      if (this.usingPreset_())
+        return DEFAULT_SYSTEM_TRACING;
+      return this.systemTracingBn_.checked;
+    },
+    set useSystemTracing(value) {
+      this.systemTracingBn_.checked = !!value;
+    },
+
+    get useSampling() {
+      if (this.usingPreset_())
+        return DEFAULT_SAMPLING_TRACING;
+      return this.samplingTracingBn_.checked;
+    },
+    set useSampling(value) {
+      this.samplingTracingBn_.checked = !!value;
+    },
+
+    set categories(c) {
+      this.categories_ = c;
+
+      for (var i = 0; i < this.categories_.length; i++) {
+        var split = this.categories_[i].split(',');
+        this.categories_[i] = split.shift();
+        if (split.length > 0)
+          this.categories_ = this.categories_.concat(split);
+      }
+    },
+
+    set settings_key(k) {
+      this.settings_key_ = k;
+    },
+
+    set settings(s) {
+      throw new Error('Dont use this!');
+    },
+
+    usingPreset_: function() {
+      return this.currentlyChosenPreset_.length > 0 ||
+             this.isPresetSelected_;
+    },
+
+    get currentlyChosenPreset() {
+      return this.currentlyChosenPreset_;
+    },
+
+    set currentlyChosenPreset(preset) {
+      if (!(preset instanceof Array))
+        throw new Error('RecordSelectionDialog.currentlyChosenPreset:' +
+            ' preset must be an array.');
+      this.currentlyChosenPreset_ = preset;
+      this.isPresetSelected_ = false;
+
+      if (this.currentlyChosenPreset_.length) {
+        this.isPresetSelected_ = true;
+        this.changeEditCategoriesState_(false);
+      } else {
+        this.updateCategoryColumnView_(true);
+        this.changeEditCategoriesState_(true);
+      }
+      this.updateManualSelectionView_();
+      this.updatePresetDescription_();
+    },
+
+    updateManualSelectionView_: function() {
+      var classList = this.categoriesView_.classList;
+      if (!this.usingPreset_()) {
+        classList.remove('categories-column-view-hidden');
+      } else {
+        if (this.editCategoriesOpened_)
+          classList.remove('categories-column-view-hidden');
+        else
+          classList.add('categories-column-view-hidden');
+      }
+    },
+
+    updateCategoryColumnView_: function(shouldReadFromSettings) {
+      var categorySet = this.querySelectorAll('.categories');
+      for (var i = 0; i < categorySet.length; ++i) {
+        var categoryGroup = categorySet[i].children;
+        for (var j = 0; j < categoryGroup.length; ++j) {
+          var categoryEl = categoryGroup[j].children[0];
+          categoryEl.checked = shouldReadFromSettings ?
+              tv.b.Settings.get(categoryEl.value, false, this.settings_key_) :
+              false;
+        }
+      }
+    },
+
+    onClickEditCategories: function() {
+      if (!this.usingPreset_())
+        return;
+
+      if (!this.editCategoriesOpened_) {
+        this.updateCategoryColumnView_(false);
+        for (var i = 0; i < this.currentlyChosenPreset_.length; ++i) {
+          var categoryEl = document.getElementById(
+              this.currentlyChosenPreset_[i]);
+          categoryEl.checked = true;
+        }
+      }
+
+      this.changeEditCategoriesState_(!this.editCategoriesOpened_);
+      this.updateManualSelectionView_();
+      this.recordButtonEl_.focus();
+    },
+
+    changeEditCategoriesState_: function(editCategoriesState) {
+      var presetOptionsGroup = this.querySelector('.labeled-option-group');
+      if (!presetOptionsGroup)
+        return;
+
+      this.editCategoriesOpened_ = editCategoriesState;
+      if (this.editCategoriesOpened_)
+          presetOptionsGroup.classList.add('categories-expanded');
+      else
+          presetOptionsGroup.classList.remove('categories-expanded');
+    },
+
+    updatePresetDescription_: function() {
+      var description = this.querySelector('.category-description');
+      if (this.usingPreset_()) {
+        description.innerText = this.currentlyChosenPreset_;
+        description.classList.remove('category-description-hidden');
+      } else {
+        description.innerText = '';
+        if (!description.classList.contains('category-description-hidden'))
+          description.classList.add('category-description-hidden');
+      }
+    },
+
+    categoryFilter: function() {
+      if (this.usingPreset_()) {
+        var categories = [];
+        var allCategories = this.allCategories_();
+        for (var category in allCategories) {
+          var disabled = category.indexOf('disabled-by-default-') == 0;
+          if (this.currentlyChosenPreset_.indexOf(category) >= 0) {
+            if (disabled)
+              categories.push(category);
+          } else {
+            if (!disabled)
+              categories.push('-' + category);
+          }
+        }
+        return categories.join(',');
+      }
+
+      var categories = this.unselectedCategories_();
+      var categories_length = categories.length;
+      var negated_categories = [];
+      for (var i = 0; i < categories_length; ++i) {
+        // Skip any category with a , as it will cause issues when we negate.
+        // Both sides should have been added as separate categories, these can
+        // only come from settings.
+        if (categories[i].match(/,/))
+          continue;
+        negated_categories.push('-' + categories[i]);
+      }
+      categories = negated_categories.join(',');
+
+      var disabledCategories = this.enabledDisabledByDefaultCategories_();
+      disabledCategories = disabledCategories.join(',');
+
+      var results = [];
+      if (categories !== '')
+        results.push(categories);
+      if (disabledCategories !== '')
+        results.push(disabledCategories);
+      return results.join(',');
+    },
+
+    clickRecordButton: function() {
+      this.recordButtonEl_.click();
+    },
+
+    onRecordButtonClicked_: function() {
+      this.visible = false;
+      tv.b.dispatchSimpleEvent(this, 'recordclick');
+      return false;
+    },
+
+    collectInputs_: function(inputs, isChecked) {
+      var inputs_length = inputs.length;
+      var categories = [];
+      for (var i = 0; i < inputs_length; ++i) {
+        var input = inputs[i];
+        if (input.checked === isChecked)
+          categories.push(input.value);
+      }
+      return categories;
+    },
+
+    unselectedCategories_: function() {
+      var inputs =
+          this.enabledCategoriesContainerEl_.querySelectorAll('input');
+      return this.collectInputs_(inputs, false);
+    },
+
+    enabledDisabledByDefaultCategories_: function() {
+      var inputs =
+          this.disabledCategoriesContainerEl_.querySelectorAll('input');
+      return this.collectInputs_(inputs, true);
+    },
+
+    onVisibleChange_: function() {
+      if (this.visible)
+        this.updateForm_();
+    },
+
+    buildInputs_: function(inputs, checkedDefault, parent) {
+      var inputs_length = inputs.length;
+      for (var i = 0; i < inputs_length; i++) {
+        var category = inputs[i];
+
+        var inputEl = document.createElement('input');
+        inputEl.type = 'checkbox';
+        inputEl.id = category;
+        inputEl.value = category;
+
+        inputEl.checked = tv.b.Settings.get(
+            category, checkedDefault, this.settings_key_);
+        inputEl.onclick = this.updateSetting_.bind(this);
+
+        var labelEl = document.createElement('label');
+        labelEl.textContent = category.replace('disabled-by-default-', '');
+        labelEl.setAttribute('for', category);
+
+        var divEl = document.createElement('div');
+        divEl.appendChild(inputEl);
+        divEl.appendChild(labelEl);
+
+        parent.appendChild(divEl);
+      }
+    },
+
+    allCategories_: function() {
+      // Dedup the categories. We may have things in settings that are also
+      // returned when we query the category list.
+      var categorySet = {};
+      var allCategories =
+          this.categories_.concat(tv.b.Settings.keys(this.settings_key_));
+      var allCategoriesLength = allCategories.length;
+      for (var i = 0; i < allCategoriesLength; ++i)
+        categorySet[allCategories[i]] = true;
+      return categorySet;
+    },
+
+    updateForm_: function() {
+      this.enabledCategoriesContainerEl_.innerHTML = ''; // Clear old categories
+      this.disabledCategoriesContainerEl_.innerHTML = '';
+
+      this.recordButtonEl_.focus();
+
+      var allCategories = this.allCategories_();
+      var categories = [];
+      var disabledCategories = [];
+      for (var category in allCategories) {
+        if (category.indexOf('disabled-by-default-') == 0)
+          disabledCategories.push(category);
+        else
+          categories.push(category);
+      }
+      disabledCategories = disabledCategories.sort();
+      categories = categories.sort();
+
+      if (this.categories_.length == 0) {
+        this.infoBarGroup_.addMessage(
+            'No categories found; recording will use default categories.');
+      }
+
+      this.buildInputs_(categories, true, this.enabledCategoriesContainerEl_);
+
+      if (disabledCategories.length > 0) {
+        this.disabledCategoriesContainerEl_.hidden = false;
+        this.buildInputs_(disabledCategories, false,
+            this.disabledCategoriesContainerEl_);
+      }
+    },
+
+    updateSetting_: function(e) {
+      var checkbox = e.target;
+      tv.b.Settings.set(checkbox.value, checkbox.checked, this.settings_key_);
+
+      // Change the current record mode to 'Manually select settings' from
+      // preset mode if and only if currently user is in preset record mode
+      // and user selects/deselects any category in 'Edit Categories' mode.
+      if (this.usingPreset_()) {
+        if (checkbox.checked) {
+          this.currentlyChosenPreset_.push(checkbox.value);
+        } else {
+          var pos = this.currentlyChosenPreset_.lastIndexOf(checkbox.value);
+          this.currentlyChosenPreset_.splice(pos, 1);
+        }
+        var categoryEl = document.getElementById(
+            'category-preset-Manually-select-settings');
+        categoryEl.checked = true;
+        var description = this.querySelector('.category-description');
+        description.innerText = '';
+        description.classList.add('category-description-hidden');
+      }
+    },
+
+    createGroupSelectButtons_: function(parent) {
+      var flipInputs = function(dir) {
+        var inputs = parent.querySelectorAll('input');
+        for (var i = 0; i < inputs.length; i++) {
+          if (inputs[i].checked === dir)
+            continue;
+          // click() is used so the settings will be correclty stored. Setting
+          // checked does not trigger the onclick (or onchange) callback.
+          inputs[i].click();
+        }
+      };
+
+      var allBtn = parent.querySelector('.all-btn');
+      allBtn.onclick = function(evt) {
+        flipInputs(true);
+        evt.preventDefault();
+      };
+
+      var noneBtn = parent.querySelector('.none-btn');
+      noneBtn.onclick = function(evt) {
+        flipInputs(false);
+        evt.preventDefault();
+      };
+    },
+
+    setWarningDialogOverlayText_: function(messages) {
+      var contentDiv = document.createElement('div');
+
+      for (var i = 0; i < messages.length; ++i) {
+        var messageDiv = document.createElement('div');
+        messageDiv.textContent = messages[i];
+        contentDiv.appendChild(messageDiv);
+      }
+      this.warningOverlay_.textContent = '';
+      this.warningOverlay_.appendChild(contentDiv);
+    },
+
+    createDefaultDisabledWarningDialog_: function(warningLink) {
+      function onClickHandler(evt) {
+        this.warningOverlay_ = tv.b.ui.Overlay();
+        this.warningOverlay_.parentEl_ = this;
+        this.warningOverlay_.title = 'Warning...';
+        this.warningOverlay_.userCanClose = true;
+        this.warningOverlay_.visible = true;
+
+        this.setWarningDialogOverlayText_([
+          'Enabling the default disabled categories may have',
+          'performance and memory impact while tv.c.'
+        ]);
+
+        evt.preventDefault();
+      }
+      warningLink.onclick = onClickHandler.bind(this);
+    }
+  };
+
+  return {
+    RecordSelectionDialog: RecordSelectionDialog
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/record_selection_dialog_test.html b/trace-viewer/trace_viewer/extras/about_tracing/record_selection_dialog_test.html
new file mode 100644
index 0000000..5fdbd1e
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/record_selection_dialog_test.html
@@ -0,0 +1,332 @@
+<!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/about_tracing/record_selection_dialog.html">
+<link rel="import" href="/base/settings.html">
+<link rel="import" href="/core/test_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('instantitate', function() {
+    var categories = [];
+    for (var i = 0; i < 30; i++)
+      categories.push('cat-' + i);
+    for (var i = 0; i < 20; i++)
+      categories.push('disabled-by-default-cat-' + i);
+    categories.push('really-really-really-really-really-really-very-loong-cat');
+    categories.push('first,second,third');
+    categories.push('cc,disabled-by-default-cc.debug');
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = categories;
+    dlg.settings_key = 'key';
+    dlg.currentlyChosenPreset = [];
+
+    var showButton = document.createElement('button');
+    showButton.textContent = 'Show record selection dialog';
+    showButton.addEventListener('click', function(e) {
+      dlg.visible = true;
+      e.stopPropagation();
+    });
+    this.addHTMLOutput(showButton);
+  });
+
+  test('recordSelectionDialog_splitCategories', function() {
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories =
+        ['cc,disabled-by-default-one,cc.debug', 'two,three', 'three'];
+    dlg.settings_key = 'key';
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+
+    var expected =
+        ['"cc"', '"cc.debug"', '"disabled-by-default-one"', '"three"', '"two"'];
+
+    var labels = dlg.querySelectorAll('.categories input');
+    var results = [];
+    for (var i = 0; i < labels.length; i++) {
+      results.push('"' + labels[i].value + '"');
+    }
+    results = results.sort();
+
+    assert.deepEqual(results, expected);
+  });
+
+  test('recordSelectionDialog_UpdateForm_NoSettings', function() {
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one', 'two', 'three'];
+    dlg.settings_key = 'key';
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+
+    var checkboxes = dlg.querySelectorAll('.categories input');
+    assert.equal(checkboxes.length, 3);
+    assert.equal(checkboxes[0].id, 'three');
+    assert.equal(checkboxes[0].value, 'three');
+    assert.isTrue(checkboxes[0].checked);
+    assert.equal(checkboxes[1].id, 'two');
+    assert.equal(checkboxes[1].value, 'two');
+    assert.isTrue(checkboxes[1].checked);
+    assert.equal(checkboxes[2].id, 'disabled-by-default-one');
+    assert.equal(checkboxes[2].value, 'disabled-by-default-one');
+    assert.isFalse(checkboxes[2].checked);
+
+    assert.equal(dlg.categoryFilter(), '');
+
+    var labels = dlg.querySelectorAll('.categories label');
+    assert.equal(labels.length, 3);
+    assert.equal(labels[0].textContent, 'three');
+    assert.equal(labels[1].textContent, 'two');
+    assert.equal(labels[2].textContent, 'one');
+  });
+
+  test('recordSelectionDialog_UpdateForm_Settings', function() {
+    tv.b.Settings.set('two', true, 'categories');
+    tv.b.Settings.set('three', false, 'categories');
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one'];
+    dlg.settings_key = 'categories';
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+
+    var checkboxes = dlg.querySelectorAll('.categories input');
+    assert.equal(checkboxes.length, 3);
+    assert.equal(checkboxes[0].id, 'three');
+    assert.equal(checkboxes[0].value, 'three');
+    assert.isFalse(checkboxes[0].checked);
+    assert.equal(checkboxes[1].id, 'two');
+    assert.equal(checkboxes[1].value, 'two');
+    assert.isTrue(checkboxes[1].checked);
+    assert.equal(checkboxes[2].id, 'disabled-by-default-one');
+    assert.equal(checkboxes[2].value, 'disabled-by-default-one');
+    assert.isFalse(checkboxes[2].checked);
+
+    assert.equal(dlg.categoryFilter(), '-three');
+
+    var labels = dlg.querySelectorAll('.categories label');
+    assert.equal(labels.length, 3);
+    assert.equal(labels[0].textContent, 'three');
+    assert.equal(labels[1].textContent, 'two');
+    assert.equal(labels[2].textContent, 'one');
+  });
+
+  test('recordSelectionDialog_UpdateForm_DisabledByDefault', function() {
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-bar', 'baz'];
+    dlg.settings_key = 'categories';
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+
+    assert.equal(dlg.categoryFilter(), '');
+
+    var inputs =
+        dlg.querySelector('input#disabled-by-default-bar').click();
+
+    assert.equal(dlg.categoryFilter(), 'disabled-by-default-bar');
+
+    assert.isFalse(
+        tv.b.Settings.get('disabled-by-default-foo', false, 'categories'));
+  });
+
+  test('selectAll', function() {
+    tv.b.Settings.set('two', true, 'categories');
+    tv.b.Settings.set('three', false, 'categories');
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one'];
+    dlg.settings_key = 'categories';
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+  });
+
+  test('selectNone', function() {
+    tv.b.Settings.set('two', true, 'categories');
+    tv.b.Settings.set('three', false, 'categories');
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one'];
+    dlg.settings_key = 'categories';
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+
+    // Enables the three option, two already enabled.
+    dlg.querySelector('.default-enabled-categories .all-btn').click();
+    assert.equal(dlg.categoryFilter(), '');
+    assert.isTrue(tv.b.Settings.get('three', false, 'categories'));
+
+    // Disables three and two.
+    dlg.querySelector('.default-enabled-categories .none-btn').click();
+    assert.equal(dlg.categoryFilter(), '-three,-two');
+    assert.isFalse(tv.b.Settings.get('two', false, 'categories'));
+    assert.isFalse(tv.b.Settings.get('three', false, 'categories'));
+
+    // Turn categories back on so they can be ignored.
+    dlg.querySelector('.default-enabled-categories .all-btn').click();
+
+    // Enables disabled category.
+    dlg.querySelector('.default-disabled-categories .all-btn').click();
+    assert.equal(dlg.categoryFilter(), 'disabled-by-default-one');
+    assert.isTrue(
+        tv.b.Settings.get('disabled-by-default-one', false, 'categories'));
+
+    // Turn disabled by default back off.
+    dlg.querySelector('.default-disabled-categories .none-btn').click();
+    assert.equal(dlg.categoryFilter(), '');
+    assert.isFalse(
+        tv.b.Settings.get('disabled-by-default-one', false, 'categories'));
+  });
+
+  test('recordSelectionDialog_noPreset', function() {
+    tv.b.Settings.set('about_tracing.record_selection_dialog_preset', []);
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    assert.isFalse(dlg.usingPreset_());
+  });
+
+  test('recordSelectionDialog_defaultPreset', function() {
+    tv.b.Settings.set('two', true, 'categories');
+    tv.b.Settings.set('three', false, 'categories');
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one'];
+    dlg.settings_key = 'categories';
+    // Note: currentlyChosenPreset is not set here, so the default is used.
+    dlg.updateForm_();
+
+    // Make sure the default filter is returned.
+    assert.equal(dlg.categoryFilter(), '-three,-two');
+
+    // Make sure the default tracing types are returned.
+    assert.equal(dlg.tracingRecordMode, 'record-as-much-as-possible');
+    assert.isFalse(dlg.useSystemTracing);
+    assert.isFalse(dlg.useSampling);
+
+    // Make sure the manual settings are not visible.
+    var classList = dlg.categoriesView_.classList;
+    assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+    // Verify manual settings do not modify the checkboxes.
+    var checkboxes = dlg.querySelectorAll('.categories input');
+    assert.equal(checkboxes.length, 3);
+    assert.equal(checkboxes[0].id, 'three');
+    assert.equal(checkboxes[0].value, 'three');
+    assert.isFalse(checkboxes[0].checked);
+    assert.equal(checkboxes[1].id, 'two');
+    assert.equal(checkboxes[1].value, 'two');
+    assert.isTrue(checkboxes[1].checked);
+    assert.equal(checkboxes[2].id, 'disabled-by-default-one');
+    assert.equal(checkboxes[2].value, 'disabled-by-default-one');
+    assert.isFalse(checkboxes[2].checked);
+  });
+
+  test('recordSelectionDialog_changePresets', function() {
+    tv.b.Settings.set('two', true, 'categories');
+    tv.b.Settings.set('three', false, 'categories');
+    tv.b.Settings.set('disabled-by-default-cc.debug', true, 'categories');
+    tv.b.Settings.set('recordSelectionDialog.tracingRecordMode',
+        'record-until-full');
+    tv.b.Settings.set('recordSelectionDialog.useSystemTracing', true);
+    tv.b.Settings.set('recordSelectionDialog.useSampling', false);
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one'];
+    dlg.settings_key = 'categories';
+    // Note: currentlyChosenPreset is not set here, so the default is used.
+    dlg.updateForm_();
+
+    // Make sure the default filter is returned.
+    assert.equal(dlg.categoryFilter(),
+        '-three,-two');
+
+    // Make sure the default tracing types are returned.
+    assert.equal(dlg.tracingRecordMode, 'record-as-much-as-possible');
+    assert.isFalse(dlg.useSystemTracing);
+    assert.isFalse(dlg.useSampling);
+
+    // Make sure the manual settings are not visible.
+    var classList = dlg.categoriesView_.classList;
+    assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+    // Switch to manual settings and verify the default values are not returned.
+    dlg.currentlyChosenPreset = [];
+    assert.equal(dlg.categoryFilter(), '-three,disabled-by-default-cc.debug');
+    assert.equal(dlg.tracingRecordMode, 'record-until-full');
+    assert.isTrue(dlg.useSystemTracing);
+    assert.isFalse(dlg.useSampling);
+    assert.isFalse(classList.contains('categories-column-view-hidden'));
+
+    // Switch to the graphics, rendering, and rasterization preset.
+    dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer',
+      'disabled-by-default-cc.debug'];
+    assert.equal(dlg.categoryFilter(),
+        'disabled-by-default-cc.debug,-three,-two');
+  });
+
+  test('recordSelectionDialog_savedPreset', function() {
+    tv.b.Settings.set('two', true, 'categories');
+    tv.b.Settings.set('three', false, 'categories');
+    tv.b.Settings.set('recordSelectionDialog.tracingRecordMode',
+        'record-continuously');
+    tv.b.Settings.set('recordSelectionDialog.useSystemTracing', true);
+    tv.b.Settings.set('recordSelectionDialog.useSampling', true);
+    tv.b.Settings.set('tv.e.about_tracing.record_selection_dialog_preset',
+        ['blink', 'cc', 'renderer', 'cc.debug']);
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.categories = ['disabled-by-default-one'];
+    dlg.settings_key = 'categories';
+    dlg.updateForm_();
+
+    // Make sure the correct filter is returned.
+    assert.equal(dlg.categoryFilter(), '-three,-two');
+
+    // Make sure the correct tracing types are returned.
+    assert.equal(dlg.tracingRecordMode, 'record-as-much-as-possible');
+    assert.isFalse(dlg.useSystemTracing);
+    assert.isFalse(dlg.useSampling);
+
+    // Make sure the manual settings are not visible.
+    var classList = dlg.categoriesView_.classList;
+    assert.isTrue(classList.contains('categories-column-view-hidden'));
+
+    // Switch to manual settings and verify the default values are not returned.
+    dlg.currentlyChosenPreset = [];
+    assert.equal(dlg.categoryFilter(), '-three');
+    assert.equal(dlg.tracingRecordMode, 'record-continuously');
+    assert.isTrue(dlg.useSystemTracing);
+    assert.isTrue(dlg.useSampling);
+    assert.isFalse(classList.contains('categories-column-view-hidden'));
+  });
+
+  test('recordSelectionDialog_categoryFilters', function() {
+    tv.b.Settings.set('default1', true, 'categories');
+    tv.b.Settings.set('disabled1', false, 'categories');
+    tv.b.Settings.set('disabled-by-default-cc.disabled2', false, 'categories');
+    tv.b.Settings.set('input', true, 'categories');
+    tv.b.Settings.set('blink', true, 'categories');
+    tv.b.Settings.set('cc', false, 'categories');
+    tv.b.Settings.set('disabled-by-default-cc.debug', true, 'categories');
+
+    var dlg = new tv.e.about_tracing.RecordSelectionDialog();
+    dlg.settings_key = 'categories';
+    dlg.categories = [];
+    dlg.currentlyChosenPreset = [];
+    dlg.updateForm_();
+
+    assert.equal(dlg.categoryFilter(),
+        '-cc,-disabled1,disabled-by-default-cc.debug');
+
+    // Switch to the graphics, rendering, and rasterization preset.
+    dlg.currentlyChosenPreset = ['blink', 'cc', 'renderer',
+      'disabled-by-default-cc.debug'];
+    assert.equal(dlg.categoryFilter(),
+        '-default1,disabled-by-default-cc.debug,-disabled1,-input');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/tracing_controller_client.html b/trace-viewer/trace_viewer/extras/about_tracing/tracing_controller_client.html
new file mode 100644
index 0000000..8114e59
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/tracing_controller_client.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="/base/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.about_tracing', function() {
+  /**
+   * Communicates with content/browser/tracing_controller_impl.cc
+   *
+   * @constructor
+   */
+  function TracingControllerClient() { }
+
+  TracingControllerClient.prototype = {
+    beginMonitoring: function(monitoringOptions) { },
+    endMonitoring: function() { },
+    captureMonitoring: function() { },
+    getMonitoringStatus: function() { },
+    getCategories: function() { },
+    beginRecording: function(recordingOptions) { },
+    beginGetBufferPercentFull: function() { },
+    endRecording: function() { }
+  };
+
+  return {
+    TracingControllerClient: TracingControllerClient
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/about_tracing/xhr_based_tracing_controller_client.html b/trace-viewer/trace_viewer/extras/about_tracing/xhr_based_tracing_controller_client.html
new file mode 100644
index 0000000..12beaa6
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/about_tracing/xhr_based_tracing_controller_client.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="/extras/about_tracing/tracing_controller_client.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.about_tracing', function() {
+  function beginXhr(method, path, data) {
+    if (data === undefined)
+      data = null;
+    return new Promise(function(resolve, reject) {
+      var req = new XMLHttpRequest();
+      if (method != 'POST' && data !== null)
+        throw new Error('Non-POST should have data==null');
+      req.open(method, path, true);
+      req.onreadystatechange = function(e) {
+        if (req.readyState == 4) {
+          window.setTimeout(function() {
+            if (req.status == 200 && req.responseText != '##ERROR##') {
+              resolve(req.responseText);
+            } else {
+              reject(new Error('Error occured at ' + path));
+            }
+          }, 0);
+        }
+      };
+      req.send(data);
+    });
+  }
+
+  /**
+   * @constructor
+   */
+  function XhrBasedTracingControllerClient() { }
+
+  XhrBasedTracingControllerClient.prototype = {
+    __proto__: tv.e.about_tracing.TracingControllerClient.prototype,
+
+    beginMonitoring: function(monitoringOptions) {
+      var monitoringOptionsB64 = btoa(JSON.stringify(monitoringOptions));
+      return beginXhr('GET', '/json/begin_monitoring?' + monitoringOptionsB64);
+    },
+
+    endMonitoring: function() {
+      return beginXhr('GET', '/json/end_monitoring');
+    },
+
+    captureMonitoring: function() {
+      return beginXhr('GET', '/json/capture_monitoring');
+    },
+
+    getMonitoringStatus: function() {
+      return beginXhr('GET', '/json/get_monitoring_status').then(
+          function(monitoringOptionsB64) {
+            return JSON.parse(atob(monitoringOptionsB64));
+          });
+    },
+
+    getCategories: function() {
+      return beginXhr('GET', '/json/categories').then(
+          function(json) {
+            return JSON.parse(json);
+          });
+    },
+
+    beginRecording: function(recordingOptions) {
+      var recordingOptionsB64 = btoa(JSON.stringify(recordingOptions));
+      return beginXhr('GET', '/json/begin_recording?' +
+                      recordingOptionsB64);
+    },
+
+    beginGetBufferPercentFull: function() {
+      return beginXhr('GET', '/json/get_buffer_percent_full');
+    },
+
+    endRecording: function() {
+      return beginXhr('GET', '/json/end_recording');
+    }
+  };
+
+  return {
+    XhrBasedTracingControllerClient: XhrBasedTracingControllerClient
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/analysis/sampling_summary.html b/trace-viewer/trace_viewer/extras/analysis/sampling_summary.html
new file mode 100644
index 0000000..aa8e08d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/analysis/sampling_summary.html
@@ -0,0 +1,565 @@
+<!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">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/base/statistics.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/pie_chart.html">
+<link rel="import" href="/base/ui/sortable_table.html">
+<link rel="import" href="/base/ui/sunburst_chart.html">
+
+<template id="x-sample-summary-panel-template">
+  <style>
+    .x-sample-summary-panel {
+      display: block;
+      overflow: auto;
+      padding: 5px;
+    }
+    .x-sample-summary-panel > x-toolbar {
+      display: block;
+      border-bottom: 1px solid black;
+    }
+
+    .x-sample-summary-panel > x-left-panel {
+      position: relative;
+      width: 610px;
+      left: 0;
+      top: 0;
+      height: 610px;
+      padding: 5px;
+    }
+
+    .x-sample-summary-panel > x-left-panel > result-area {
+      display: block;
+      width: 610px;
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 610px;
+    }
+
+    .x-sample-summary-panel > x-left-panel > x-explanation {
+      display: block;
+      position: absolute;
+      top: 300px;
+      left: 250px;
+      width: 100px;
+      height: 100px;
+      text-align: center;
+      vertical-align:middle;
+      color: #666;
+      font-size: 12px;
+    }
+
+    .x-sample-summary-panel > x-right-panel {
+      display: block;
+      min-height: 610px;
+      margin-left: 610px;
+      padding: 5px;
+    }
+
+    .x-sample-summary-panel > x-right-panel td {
+      color: #fff;
+      padding: 1px 5px;
+      font-size: 1.0em;
+      white-space: nowrap;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-sequence {
+      display: block;
+      overflow: auto;
+      width: 600px;
+      height: 400px;
+      font-size: 14px;
+      margin: 10px;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-sequence table {
+      min-width: 600px;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-callees {
+      display: block;
+      position: relative;
+      width: 600px;
+      height: auto;
+      overflow: auto;
+      font-size: 14px;
+      margin: 10px;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-callees .x-col-numeric {
+      width: 30px;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-callees .x-td-numeric {
+      text-align: right;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-callees table {
+      min-width: 600px;
+      max-width: 600px;
+    }
+
+    .x-sample-summary-panel > x-right-panel > x-callees {
+      display: block;
+      overflow: auto;
+      height: 300px;
+    }
+
+  </style>
+
+  <x-toolbar></x-toolbar>
+  <x-left-panel>
+    <result-area></result-area>
+    <x-explanation></x-explanation>
+  </x-left-panel>
+  <x-right-panel>
+    <x-sequence></x-sequence>
+    <x-callees></x-callees>
+  </x-right-panel>
+</template>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.analysis', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  var RequestSelectionChangeEvent = tv.c.RequestSelectionChangeEvent;
+  var getColorOfKey = tv.b.ui.getColorOfKey;
+
+  /**
+    * @constructor
+    */
+  var CallTreeNode = function(name, category) {
+    this.parent = undefined;
+    this.name = name;
+    this.category = category;
+    this.selfTime = 0.0;
+    this.children = [];
+
+    // Defined only for leaf nodes, leaf_node_id corresponds to
+    // the id of the leaf stack frame.
+    this.leaf_node_id = undefined;
+  };
+
+  CallTreeNode.prototype = {
+    addChild: function(node) {
+      this.children.push(node);
+      node.parent = this;
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  var Thread = function(thread) {
+    this.thread = thread;
+    this.rootNode = new CallTreeNode('root', 'root');
+    this.rootCategories = {};
+    this.sfToNode = {};
+    this.sfToNode['__root'] = self.rootNode;
+  };
+
+  Thread.prototype = {
+    getCallTreeNode: function(stackFrame) {
+      if (stackFrame.id in this.sfToNode)
+        return this.sfToNode[stackFrame.id];
+
+      // Create & save the node.
+      var newNode = new CallTreeNode(stackFrame.title, stackFrame.category);
+      this.sfToNode[stackFrame.id] = newNode;
+
+      // Add node to parent tree node.
+      if (stackFrame.parentFrame) {
+        var parentNode = this.getCallTreeNode(stackFrame.parentFrame);
+
+        parentNode.addChild(newNode);
+      } else {
+        // Creating a root category node for each category helps group samples
+        // that may be missing call stacks.
+        var rootCategory = this.rootCategories[stackFrame.category];
+        if (!rootCategory) {
+          rootCategory =
+              new CallTreeNode(stackFrame.category, stackFrame.category);
+          this.rootNode.addChild(rootCategory);
+          this.rootCategories[stackFrame.category] = rootCategory;
+        }
+        rootCategory.addChild(newNode);
+      }
+      return newNode;
+    },
+
+    addSample: function(sample) {
+      var leaf_node = this.getCallTreeNode(sample.leafStackFrame);
+      leaf_node.leaf_node_id = sample.leafStackFrame.id;
+      leaf_node.selfTime += sample.weight;
+    }
+  };
+
+  function genCallTree(node, isRoot) {
+    var ret = {
+      category: node.category,
+      name: node.name,
+      leaf_node_id: node.leaf_node_id
+    };
+
+    if (isRoot || node.children.length > 0) {
+      ret.children = [];
+      for (var c = 0; c < node.children.length; c++)
+        ret.children.push(genCallTree(node.children[c], false));
+      if (node.selfTime > 0.0) {
+        // say, caller (r) calls callee (e) which calls callee2 (2)
+        // and following are the samples
+        // r r r r r r r
+        //   e e e
+        //   2 2 2
+        // In this case, r has a non-zero self time (4 samples to be precise)
+        // The <self> node makes the representation resemble the following
+        // where s denotes the selftime.
+        // r r r r r r r
+        // s e e e s s s
+        //   2 2 2
+        //
+        // Among the obvious visualization benefit, this also creates the
+        // invariance that a node can not simultaneously have samples and
+        // children.
+        ret.children.push({
+          name: '<self>',
+          category: ret.category,
+          size: node.selfTime,
+          leaf_node_id: node.leaf_node_id
+        });
+        delete ret.leaf_node_id;  // ret is not a leaf node anymore.
+      }
+    }
+    else {
+      ret.size = node.selfTime;
+    }
+    if (isRoot)
+      return ret.children;
+    return ret;
+  }
+
+  function getSampleTypes(selection) {
+    var sampleDict = {};
+    var samples = selection.getEventsOrganizedByBaseType().sample;
+    for (var i = 0; i < samples.length; i++) {
+      sampleDict[samples[i].title] = null;
+    }
+    return Object.keys(sampleDict);
+  }
+
+  // Create sunburst data from the selection.
+  function createSunburstData(selection, sampleType) {
+    var threads = {};
+    function getOrCreateThread(thread) {
+      var ret = undefined;
+      if (thread.tid in threads) {
+        ret = threads[thread.tid];
+      } else {
+        ret = new Thread(thread);
+        threads[thread.tid] = ret;
+      }
+      return ret;
+    }
+
+    // Process samples.
+    var samples = selection.getEventsOrganizedByBaseType().sample;
+    for (var i = 0; i < samples.length; i++) {
+      var sample = samples[i];
+      if (sample.title == sampleType)
+        getOrCreateThread(sample.thread).addSample(sample);
+    }
+
+    // Generate sunburst data.
+    var sunburstData = {
+      name: '<All Threads>',
+      category: 'root',
+      children: []
+    };
+    for (var t in threads) {
+      if (!threads.hasOwnProperty(t)) continue;
+      var thread = threads[t];
+      var threadData = {
+        name: 'Thread ' + thread.thread.tid + ': ' + thread.thread.name,
+        category: 'Thread',
+        children: genCallTree(thread.rootNode, true)
+      };
+      sunburstData.children.push(threadData);
+    }
+    return sunburstData;
+  }
+
+  /**
+   * @constructor
+   */
+  var SamplingSummaryPanel =
+      tv.b.ui.define('x-sample-summary-panel');
+  SamplingSummaryPanel.textLabel = 'Sampling Summary';
+  SamplingSummaryPanel.supportsModel = function(m) {
+    if (m == undefined) {
+      return {
+        supported: false,
+        reason: 'Unknown tracing model'
+      };
+    }
+
+    if (m.samples.length == 0) {
+      return {
+        supported: false,
+        reason: 'No sampling data in trace'
+      };
+    }
+
+    return {
+      supported: true
+    };
+  };
+
+  // Return a dict with keys as stack-frame ids
+  // and values as selection objects which contain all the
+  // samples whose leaf stack-frame id matches the key.
+  function divideSamplesBasedOnLeafStackFrame(selection) {
+    var stackFrameIdToSamples = {};
+    for (var i = 0; i < selection.length; ++i) {
+      var sample = selection[i];
+      var id = sample.leafStackFrame.id;
+      if (!stackFrameIdToSamples[id])
+        stackFrameIdToSamples[id] = new tv.c.Selection();
+      stackFrameIdToSamples[id].push(sample);
+    }
+    return stackFrameIdToSamples;
+  }
+
+  SamplingSummaryPanel.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.classList.add('x-sample-summary-panel');
+      this.appendChild(tv.b.instantiateTemplate(
+          '#x-sample-summary-panel-template', THIS_DOC));
+
+      this.sampleType_ = undefined;
+      this.sampleTypeSelector_ = undefined;
+      this.chart_ = undefined;
+      this.selection_ = undefined;
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      this.selection_ = selection;
+      this.stackFrameIdToSamples_ = divideSamplesBasedOnLeafStackFrame(
+          selection);
+      this.updateContents_();
+    },
+
+    get sampleType() {
+      return this.sampleType_;
+    },
+
+    set sampleType(type) {
+      this.sampleType_ = type;
+      if (this.sampleTypeSelector_)
+        this.sampleTypeSelector_.selectedValue = type;
+      this.updateResultArea_();
+    },
+
+    updateCallees_: function(d) {
+      // Update callee table.
+      var that = this;
+      var table = document.createElement('table');
+
+      // Add column styles.
+      var col0 = document.createElement('col');
+      var col1 = document.createElement('col');
+      col0.className += 'x-col-numeric';
+      col1.className += 'x-col-numeric';
+      table.appendChild(col0);
+      table.appendChild(col1);
+
+      // Add headers.
+      var thead = table.createTHead();
+      var headerRow = thead.insertRow(0);
+      headerRow.style.backgroundColor = '#888';
+      headerRow.insertCell(0).appendChild(document.createTextNode('Samples'));
+      headerRow.insertCell(1).appendChild(document.createTextNode('Percent'));
+      headerRow.insertCell(2).appendChild(document.createTextNode('Symbol'));
+
+      // Add body.
+      var tbody = table.createTBody();
+      if (d.children) {
+        for (var i = 0; i < d.children.length; i++) {
+          var c = d.children[i];
+          var row = tbody.insertRow(i);
+          var bgColor = getColorOfKey(c.category);
+          if (bgColor == undefined)
+            bgColor = '#444444';
+          row.style.backgroundColor = bgColor;
+          var cell0 = row.insertCell(0);
+          var cell1 = row.insertCell(1);
+          var cell2 = row.insertCell(2);
+          cell0.className += 'x-td-numeric';
+          cell1.className += 'x-td-numeric';
+          cell0.appendChild(document.createTextNode(c.value.toString()));
+          cell1.appendChild(document.createTextNode(
+              (100 * c.value / d.value).toFixed(2) + '%'));
+          cell2.appendChild(document.createTextNode(c.name));
+        }
+      }
+
+      // Make it sortable.
+      tv.b.ui.SortableTable.decorate(table);
+
+      var calleeArea = that.querySelector('x-callees');
+      calleeArea.textContent = '';
+      calleeArea.appendChild(table);
+    },
+
+    updateHighlight_: function(d) {
+      var that = this;
+
+      // Update explanation.
+      var percent = 100.0;
+      if (that.chart_.selectedNode != null)
+        percent = 100.0 * d.value / that.chart_.selectedNode.value;
+      that.querySelector('x-explanation').innerHTML =
+          d.value + '<br>' + percent.toFixed(2) + '%';
+
+      // Update call stack table.
+      var table = document.createElement('table');
+      var thead = table.createTHead();
+      var tbody = table.createTBody();
+      var headerRow = thead.insertRow(0);
+      headerRow.style.backgroundColor = '#888';
+      headerRow.insertCell(0).appendChild(
+          document.createTextNode('Call Stack'));
+
+      var callStack = [];
+      var frame = d;
+      while (frame && frame.id) {
+        callStack.push(frame);
+        frame = frame.parent;
+      }
+
+      for (var i = 0; i < callStack.length; i++) {
+        var row = tbody.insertRow(i);
+        var bgColor = getColorOfKey(callStack[i].category);
+        if (bgColor == undefined)
+          bgColor = '#444444';
+        row.style.backgroundColor = bgColor;
+        if (i == 0)
+          row.style.fontWeight = 'bold';
+        row.insertCell(0).appendChild(
+            document.createTextNode(callStack[i].name));
+      }
+
+      var sequenceArea = that.querySelector('x-sequence');
+      sequenceArea.textContent = '';
+      sequenceArea.appendChild(table);
+    },
+
+    getSamplesFromNode_: function(node) {
+      // A node has samples associated with it, if it's a leaf node.
+      var selection = new tv.c.Selection();
+      if (node.leaf_node_id !== undefined) {
+        selection.addSelection(this.stackFrameIdToSamples_[node.leaf_node_id]);
+      }
+      else if (node.children === undefined ||
+               node.children.length === 0) {
+        throw new Error('A node should either have samples, or children');
+      }
+      else {
+        for (var i = 0; i < node.children.length; ++i)
+          selection.addSelection(this.getSamplesFromNode_(node.children[i]));
+      }
+      return selection;
+    },
+
+    updateResultArea_: function() {
+      if (this.selection_ === undefined)
+        return;
+
+      var resultArea = this.querySelector('result-area');
+      this.chart_ = undefined;
+      resultArea.textContent = '';
+
+      var sunburstData =
+          createSunburstData(this.selection_, this.sampleType_);
+      this.chart_ = new tv.b.ui.SunburstChart();
+      this.chart_.width = 600;
+      this.chart_.height = 600;
+      this.chart_.chartTitle = 'Sampling Summary';
+
+      this.chart_.addEventListener('node-selected', (function(e) {
+        this.updateCallees_(e.node);
+      }).bind(this));
+
+      this.chart_.addEventListener('node-clicked', (function(e) {
+        var event = new RequestSelectionChangeEvent();
+        var depth = e.node.depth;
+        if (e.node.name === '<self>')
+          depth--;
+        event.selection = this.getSamplesFromNode_(e.node);
+        event.selection.sunburst_zoom_level = depth;
+        this.dispatchEvent(event);
+      }).bind(this));
+
+      this.chart_.addEventListener('node-highlighted', (function(e) {
+        this.updateHighlight_(e.node);
+      }).bind(this));
+
+      this.chart_.data = {
+        nodes: sunburstData
+      };
+
+      resultArea.appendChild(this.chart_);
+      this.chart_.setSize(this.chart_.getMinSize());
+
+      if (this.selection_.sunburst_zoom_level !== undefined) {
+        this.chart_.zoomToDepth(this.selection_.sunburst_zoom_level);
+      }
+    },
+
+    updateContents_: function() {
+      if (this.selection_ === undefined || this.selection_.length == 0)
+        return;
+
+      // Get available sample types in range.
+      var sampleTypes = getSampleTypes(this.selection_);
+      if (sampleTypes.indexOf(this.sampleType_) == -1)
+        this.sampleType_ = sampleTypes[0];
+
+      // Create sample type dropdown.
+      var sampleTypeOptions = [];
+      for (var i = 0; i < sampleTypes.length; i++)
+        sampleTypeOptions.push({label: sampleTypes[i], value: sampleTypes[i]});
+
+      var toolbarEl = this.querySelector('x-toolbar');
+      this.sampleTypeSelector_ = tv.b.ui.createSelector(
+          this,
+          'sampleType',
+          'samplingSummaryPanel.sampleType',
+          this.sampleType_,
+          sampleTypeOptions);
+      toolbarEl.textContent = 'Sample Type: ';
+      toolbarEl.appendChild(this.sampleTypeSelector_);
+    }
+  };
+
+  return {
+    SamplingSummaryPanel: SamplingSummaryPanel,
+    createSunburstData: createSunburstData
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/analysis/sampling_summary_test.html b/trace-viewer/trace_viewer/extras/analysis/sampling_summary_test.html
new file mode 100644
index 0000000..2db0fbd
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/analysis/sampling_summary_test.html
@@ -0,0 +1,214 @@
+<!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="/extras/analysis/sampling_summary.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 StackFrame = tv.c.trace_model.StackFrame;
+  var Sample = tv.c.trace_model.Sample;
+
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  function createSelection() {
+    var selection = new tv.c.Selection();
+    var model = new tv.c.TraceModel();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    thread.name = 'The Thread';
+
+    var fA = new StackFrame(undefined, 0, 'Chrome', 'a', 7);
+    var fAB = new StackFrame(fA, 1, 'Chrome', 'b', 7);
+    var fABC = new StackFrame(fAB, 2, 'Chrome', 'c', 7);
+    var fAD = new StackFrame(fA, 3, 'GPU Driver', 'd', 7);
+
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              10, fABC, 10));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              20, fAB, 10));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              25, fAB, 10));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              30, fAB, 10));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              35, fAD, 10));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              35, fAD, 5));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              40, fAD, 5));
+    selection.push(new Sample(undefined, thread, 'page_misses',
+                              35, fAD, 7));
+    selection.push(new Sample(undefined, thread, 'page_misses',
+                              40, fAD, 9));
+    return selection;
+  }
+
+  test('createSunburstDataBasic', function() {
+    var s = createSelection();
+
+    var expect = {
+      name: '<All Threads>',
+      category: 'root',
+      children: [
+        {
+          name: 'Thread 2: The Thread',
+          category: 'Thread',
+          children: [
+            {
+              category: 'Chrome',
+              name: 'Chrome',
+              children: [
+                {
+                  category: 'Chrome',
+                  name: 'a',
+                  children: [
+                    {
+                      category: 'Chrome',
+                      name: 'b',
+                      children: [
+                        {
+                          category: 'Chrome',
+                          name: 'c',
+                          leaf_node_id: 2,
+                          size: 10
+                        },
+                        {
+                          name: '<self>',
+                          category: 'Chrome',
+                          size: 30,
+                          leaf_node_id: 1
+                        }
+                      ]
+                    },
+                    {
+                      category: 'GPU Driver',
+                      name: 'd',
+                      leaf_node_id: 3,
+                      size: 20
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    };
+
+    var sunburstData = tv.e.analysis.createSunburstData(s, 'cycles');
+    assert.equal(JSON.stringify(sunburstData), JSON.stringify(expect));
+  });
+
+  test('processOnlySamples', function() {
+    var selection = new tv.c.Selection();
+    var model = new tv.c.TraceModel();
+    var thread = model.getOrCreateProcess(1).getOrCreateThread(2);
+    thread.name = 'The Thread';
+
+    var fA = new StackFrame(undefined, 1, 'Chrome', 'a', 7);
+    var fAB = new StackFrame(fA, 2, 'Chrome', 'b', 7);
+    var fABC = new StackFrame(fAB, 3, 'Chrome', 'c', 7);
+    var fAD = new StackFrame(fA, 4, 'GPU Driver', 'd', 7);
+
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              10, fABC, 10));
+    selection.push(new Sample(undefined, thread, 'cycles',
+                              20, fAB, 10));
+    selection.push(new Sample(undefined, thread, 'page_misses',
+                              40, fAD, 9));
+    var expect = {
+      name: '<All Threads>',
+      category: 'root',
+      children: [
+        {
+          name: 'Thread 2: The Thread',
+          category: 'Thread',
+          children: [
+            {
+              category: 'Chrome',
+              name: 'Chrome',
+              children: [
+                {
+                  category: 'Chrome',
+                  name: 'a',
+                  children: [
+                    {
+                      category: 'GPU Driver',
+                      name: 'd',
+                      leaf_node_id: 4,
+                      size: 9
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    };
+
+    // Along with the samples, push some slices too.
+    // The panel should completely ignore these.
+    selection.push(newSliceNamed('a', 1, 2));
+    selection.push(newSliceNamed('f', 9, 7));
+
+    var sunburstData = tv.e.analysis.createSunburstData(
+        selection, 'page_misses');
+    assert.equal(JSON.stringify(sunburstData), JSON.stringify(expect));
+  });
+
+  test('createSunburstDataSampleType', function() {
+    var s = createSelection();
+
+    var expect = {
+      name: '<All Threads>',
+      category: 'root',
+      children: [
+        {
+          name: 'Thread 2: The Thread',
+          category: 'Thread',
+          children: [
+            {
+              category: 'Chrome',
+              name: 'Chrome',
+              children: [
+                {
+                  category: 'Chrome',
+                  name: 'a',
+                  children: [
+                    {
+                      category: 'GPU Driver',
+                      name: 'd',
+                      leaf_node_id: 3,
+                      size: 16
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    };
+
+    var sunburstData = tv.e.analysis.createSunburstData(s, 'page_misses');
+    assert.equal(JSON.stringify(sunburstData), JSON.stringify(expect));
+  });
+
+  test('instantiate', function() {
+    var s = createSelection();
+
+    var panel = new tv.e.analysis.SamplingSummaryPanel();
+    this.addHTMLOutput(panel);
+    panel.style.border = '1px solid black';
+    panel.selection = s;
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/android_app.html b/trace-viewer/trace_viewer/extras/audits/android_app.html
new file mode 100644
index 0000000..48a543b
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/android_app.html
@@ -0,0 +1,255 @@
+<!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/sorted_array_utils.html">
+<link rel="import" href="/core/trace_model/frame.html">
+<link rel="import" href="/extras/audits/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for managing android-specific model meta data,
+ * such as rendering apps, and frames rendered.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var Frame = tv.c.trace_model.Frame;
+
+  var UI_THREAD_DRAW_NAME = 'performTraversals';
+  var RENDER_THREAD_DRAW_NAME = 'DrawFrame';
+  var RENDER_THREAD_INDEP_DRAW_NAME = 'doFrame';
+  var THREAD_SYNC_NAME = 'syncFrameState';
+
+  function findOverlappingDrawFrame(renderThread, time) {
+    if (!renderThread)
+      return undefined;
+
+    var slices = renderThread.sliceGroup.slices;
+    for (var i = 0; i < slices.length; i++) {
+      var slice = slices[i];
+      if (slice.title == RENDER_THREAD_DRAW_NAME &&
+          slice.start <= time &&
+          time <= slice.end) {
+        return slice;
+      }
+    }
+    return undefined;
+  }
+
+  /**
+   * Builds an array of {start, end} ranges grouping common work of a frame
+   * that occurs just before performTraversals().
+   */
+  function getPreTraversalWorkRanges(uiThread) {
+    if (!uiThread)
+      return [];
+
+    // gather all frame work that occurs outside of performTraversals
+    var preFrameEvents = [];
+    uiThread.sliceGroup.slices.forEach(function(slice) {
+      if (slice.title == 'obtainView' ||
+          slice.title == 'setupListItem' ||
+          slice.title == 'deliverInputEvent')
+        preFrameEvents.push(slice);
+    });
+    uiThread.asyncSliceGroup.slices.forEach(function(slice) {
+      if (slice.title == 'deliverInputEvent')
+        preFrameEvents.push(slice);
+    });
+
+    return tv.e.audits.mergeEvents(preFrameEvents, 3, function(events) {
+      return {
+        start: events[0].start,
+        end: events[events.length - 1].end
+      };
+    });
+  }
+
+  function getFrameStartTime(traversalStart, preTraversalWorkRanges) {
+    var preTraversalWorkRange = tv.b.findClosestIntervalInSortedIntervals(
+        preTraversalWorkRanges,
+        function(range) { return range.start },
+        function(range) { return range.end },
+        traversalStart,
+        3);
+
+    if (preTraversalWorkRange)
+      return preTraversalWorkRange.start;
+    return traversalStart;
+  }
+
+  function getUiThreadDrivenFrames(app) {
+    if (!app.uiThread)
+      return [];
+
+    var preTraversalWorkRanges = getPreTraversalWorkRanges(app.uiThread);
+
+    var frames = [];
+    app.uiThread.sliceGroup.getSlicesOfName(UI_THREAD_DRAW_NAME)
+        .forEach(function(uiDrawSlice) {
+      var threadTimeRanges = [];
+      var uiThreadTimeRange = {
+        thread: app.uiThread,
+        start: getFrameStartTime(uiDrawSlice.start, preTraversalWorkRanges),
+        end: uiDrawSlice.end
+      };
+      threadTimeRanges.push(uiThreadTimeRange);
+
+      // on SDK 21+ devices with RenderThread,
+      // account for time taken on RenderThread
+      var rtDrawSlice = findOverlappingDrawFrame(
+          app.renderThread, uiDrawSlice.end);
+      if (rtDrawSlice) {
+        var rtSyncSlice = rtDrawSlice.findDescendentSlice(THREAD_SYNC_NAME);
+        if (rtSyncSlice) {
+          // Generally, the UI thread is only on the critical path
+          // until the start of sync.
+          uiThreadTimeRange.end = Math.min(uiThreadTimeRange.end,
+                                           rtSyncSlice.start);
+        }
+
+        threadTimeRanges.push({
+          thread: app.renderThread,
+          start: rtDrawSlice.start,
+          end: rtDrawSlice.end
+        });
+      }
+      frames.push(new Frame(threadTimeRanges));
+    });
+    return frames;
+  }
+
+  function getRenderThreadDrivenFrames(app) {
+    if (!app.renderThread)
+      return [];
+
+    var frames = [];
+    app.renderThread.sliceGroup.getSlicesOfName(RENDER_THREAD_INDEP_DRAW_NAME)
+        .forEach(function(slice) {
+      var threadTimeRanges = [{
+        thread: app.renderThread,
+        start: slice.start,
+        end: slice.end
+      }];
+      frames.push(new Frame(threadTimeRanges));
+    });
+    return frames;
+  }
+
+  function hasUiDraw(uiThread) {
+    var slices = uiThread.sliceGroup.slices;
+    for (var i = 0; i < slices.length; i++) {
+      var slice = slices[i];
+      if (slice.title == UI_THREAD_DRAW_NAME) {
+        return uiThread;
+      }
+    }
+    return undefined;
+  }
+
+  function getInputSamples(process) {
+    var samples = undefined;
+    for (var counterName in process.counters) {
+          if (/^android\.aq\:pending/.test(counterName) &&
+        process.counters[counterName].numSeries == 1) {
+        samples = process.counters[counterName].series[0].samples;
+        break;
+      }
+    }
+
+    if (!samples)
+      return [];
+
+    // output rising edges only, since those are user inputs
+    var inputSamples = [];
+    var lastValue = 0;
+    samples.forEach(function(sample) {
+      if (sample.value > lastValue) {
+        inputSamples.push(sample);
+      }
+      lastValue = sample.value;
+    });
+    return inputSamples;
+  }
+
+  function getAnimationAsyncSlices(uiThread) {
+    if (!uiThread)
+      return [];
+
+    var slices = [];
+    uiThread.asyncSliceGroup.iterateAllEvents(function(slice) {
+      if (/^animator\:/.test(slice.title))
+        slices.push(slice);
+    });
+    return slices;
+  }
+
+  /**
+   * Model for Android App specific data.
+   * @constructor
+   */
+  function AndroidApp(process, uiThread, renderThread) {
+    this.process = process;
+    this.uiThread = uiThread;
+    this.renderThread = renderThread;
+
+    this.frames_ = undefined;
+    this.inputs_ = undefined;
+  };
+
+  AndroidApp.createForProcessIfPossible = function(process) {
+    var uiThread = process.getThread(process.pid);
+    if (uiThread && !hasUiDraw(uiThread)) {
+      uiThread = undefined;
+    }
+    var renderThreads = process.findAllThreadsNamed('RenderThread');
+    var renderThread = renderThreads.length == 1 ? renderThreads[0] : undefined;
+
+    if (uiThread || renderThread) {
+      return new AndroidApp(process, uiThread, renderThread);
+    }
+  }
+
+  AndroidApp.prototype = {
+  /**
+   * Returns a list of all frames in the trace for the app,
+   * constructed on first query.
+   */
+    getFrames: function() {
+      if (!this.frames_) {
+        var uiFrames = getUiThreadDrivenFrames(this);
+        var rtFrames = getRenderThreadDrivenFrames(this);
+        this.frames_ = uiFrames.concat(rtFrames);
+
+        // merge frames by sorting by end timestamp
+        this.frames_.sort(function(a, b) { a.end - b.end });
+      }
+      return this.frames_;
+    },
+
+    /**
+     * Returns list of CounterSamples for each input event enqueued to the app.
+     */
+    getInputSamples: function() {
+      if (!this.inputs_) {
+        this.inputs_ = getInputSamples(this.process);
+      }
+      return this.inputs_;
+    },
+
+    getAnimationAsyncSlices: function() {
+      if (!this.animations_) {
+        this.animations_ = getAnimationAsyncSlices(this.uiThread);
+      }
+      return this.animations_;
+    }
+  };
+
+  return {
+    AndroidApp: AndroidApp
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/android_auditor.html b/trace-viewer/trace_viewer/extras/audits/android_auditor.html
new file mode 100644
index 0000000..2c54137
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/android_auditor.html
@@ -0,0 +1,212 @@
+<!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/auditor.html">
+<link rel="import" href="/extras/audits/android_model_helper.html">
+<link rel="import" href="/extras/audits/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for Android-specific Auditing
+ */
+tv.exportTo('tv.e.audits', function() {
+  // TODO: extract from VSYNC, since not all devices have vsync near 60fps
+  var EXPECTED_FRAME_TIME_MS = 16.67;
+
+  var Auditor = tv.c.Auditor;
+  var AndroidModelHelper = tv.e.audits.AndroidModelHelper;
+
+  /**
+   * Auditor for Android-specific traces.
+   * @constructor
+   */
+  function AndroidAuditor(model) {
+    this.model = model;
+    var helper = new AndroidModelHelper(model);
+    if (helper.apps.length || helper.surfaceFlinger)
+      this.helper = helper;
+  };
+
+  AndroidAuditor.prototype = {
+    __proto__: Auditor.prototype,
+
+    renameAndSort_: function() {
+      // SurfaceFlinger first, other processes sorted by slice count
+      this.model.getAllProcesses().forEach(function(process) {
+        if (this.helper.surfaceFlinger &&
+            process == this.helper.surfaceFlinger.process) {
+          if (!process.name)
+            process.name = 'SurfaceFlinger';
+          process.sortIndex = Number.NEGATIVE_INFINITY;
+          return;
+        }
+
+        var uiThread = process.getThread(process.pid);
+        if (!process.name && uiThread && uiThread.name) {
+          if (/^ndroid\./.test(uiThread.name))
+            uiThread.name = 'a' + uiThread.name;
+          process.name = uiThread.name;
+        }
+
+        process.sortIndex = 0;
+        for (var tid in process.threads) {
+          process.sortIndex -= process.threads[tid].sliceGroup.slices.length;
+        }
+      }, this);
+
+      // ensure sequential, relative order for UI/Render/Worker threads
+      this.model.getAllThreads().forEach(function(thread) {
+        if (thread.tid == thread.parent.pid)
+          thread.sortIndex = -3;
+        if (thread.name == 'RenderThread')
+          thread.sortIndex = -2;
+        if (/^hwuiTask/.test(thread.name))
+          thread.sortIndex = -1;
+      });
+    },
+
+    pushFramesAndJudgeJank_: function() {
+      var badFramesObserved = 0;
+      var framesObserved = 0;
+      this.helper.apps.forEach(function(app) {
+        // override frame list
+        app.process.frames = app.getFrames();
+
+        app.process.frames.forEach(function(frame) {
+          if (frame.totalDuration > EXPECTED_FRAME_TIME_MS)
+            badFramesObserved++;
+        });
+        framesObserved += app.process.frames.length;
+      });
+
+      if (framesObserved) {
+        var portionBad = badFramesObserved / framesObserved;
+        if (portionBad > 0.3)
+          this.model.faviconHue = 'red';
+        else if (portionBad > 0.05)
+          this.model.faviconHue = 'yellow';
+        else
+          this.model.faviconHue = 'green';
+      }
+    },
+
+    runAnnotate: function() {
+      if (!this.helper)
+        return;
+
+      this.renameAndSort_();
+      this.pushFramesAndJudgeJank_();
+
+      this.helper.iterateImportantSlices(function(slice) {
+        slice.important = true;
+      });
+    },
+
+    runAudit: function() {
+      if (!this.helper)
+        return;
+
+      this.helper.apps.forEach(function(app) {
+        // per frame alerts
+        app.getFrames().forEach(this.getFrameTimingAlerts, this);
+
+        // general alerts
+        this.addSaveLayerAlerts(app.renderThread);
+      }, this);
+
+      this.addRenderingInteractionRecords();
+      this.addInputInteractionRecords();
+    },
+
+    getFrameTimingAlerts: function(frame) {
+      /*
+      * Currently, we create alerts for these, but really we should
+      * highlight frame-production-performance isolated per app,
+      * without alerts.
+      *
+      * Consider highlighting process background green when frames
+      * take < 16.6ms, yellow when double buffered but not dropping
+      * frames, red when actively janking.
+      */
+      if (frame.totalDuration > EXPECTED_FRAME_TIME_MS) {
+        var alertType = new tv.c.trace_model.AlertType(
+            'Long frame',
+            'Frames should take fewer than' + EXPECTED_FRAME_TIME_MS +
+            'ms to ensure smooth performance.',
+            tv.c.trace_model.ALERT_SEVERITY.WARNING);
+        var alert = new tv.c.trace_model.Alert(
+            alertType,
+            frame.start, { 'totalDuration' : frame.totalDuration });
+        this.model.alerts.push(alert);
+      }
+    },
+
+    addSaveLayerAlerts: function(renderThread) {
+      if (!renderThread)
+        return;
+      var saveLayerRegEx = /caused (unclipped )?saveLayer (\d+)x(\d+)/;
+      renderThread.sliceGroup.slices.forEach(function(slice) {
+        var match = saveLayerRegEx.exec(slice.title);
+        if (match) {
+          var alertType = new tv.c.trace_model.AlertType(
+              'Inefficient Alpha',
+              'http://developer.android.com/reference/android/view/View.html#setAlpha(float)', // @suppress longLineCheck
+              tv.c.trace_model.ALERT_SEVERITY.CRITICAL);
+          var args = { 'width' : parseInt(match[2]),
+                       'height' : parseInt(match[3]) };
+          var alert = new tv.c.trace_model.Alert(alertType, slice.start, args);
+          this.model.alerts.push(alert);
+        }
+        // TODO: also do something reasonable with standalone saveLayer commands
+      }, this);
+    },
+
+    addRenderingInteractionRecords: function() {
+      var events = [];
+      this.helper.apps.forEach(function(app) {
+        events.push.apply(events, app.getAnimationAsyncSlices());
+        events.push.apply(events, app.getFrames());
+      });
+
+      var mergerFunction = function(events) {
+        var ir = new tv.c.trace_model.InteractionRecord('Rendering',
+            tv.b.ui.getColorIdForGeneralPurposeString('mt_rendering'),
+            events[0].start,
+            events[events.length - 1].end - events[0].start);
+        this.model.addInteractionRecord(ir);
+      }.bind(this);
+      tv.e.audits.mergeEvents(events, 30, mergerFunction);
+    },
+
+    addInputInteractionRecords: function() {
+      var inputSamples = [];
+      this.helper.apps.forEach(function(app) {
+        inputSamples.push.apply(inputSamples, app.getInputSamples());
+      });
+
+      var mergerFunction = function(events) {
+        var ir = new tv.c.trace_model.InteractionRecord('Input',
+            tv.b.ui.getColorIdForGeneralPurposeString('mt_input'),
+            events[0].timestamp,
+            events[events.length - 1].timestamp - events[0].timestamp);
+        this.model.addInteractionRecord(ir);
+      }.bind(this);
+      var timestampFunction = function(x) { return x.timestamp; };
+      tv.e.audits.mergeEvents(inputSamples, 30, mergerFunction,
+                              timestampFunction, timestampFunction);
+    }
+  };
+
+  Auditor.register(AndroidAuditor);
+
+  return {
+    AndroidAuditor: AndroidAuditor
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/android_auditor_test.html b/trace-viewer/trace_viewer/extras/audits/android_auditor_test.html
new file mode 100644
index 0000000..c15123a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/android_auditor_test.html
@@ -0,0 +1,129 @@
+<!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="/core/test_utils.html">
+<link rel="import" href="/extras/audits/android_auditor.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  test('longFrameAlert', function() {
+    var model = tv.c.test_utils.newModelWithAuditor(function(model) {
+      var uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 0, 80));
+    }, tv.e.audits.AndroidAuditor);
+
+    assert.equal(model.alerts.length, 1);
+    var alert = model.alerts[0];
+    assert.closeTo(alert.args.totalDuration, 80, 1e-5);
+  });
+
+  test('saveLayerAlert', function() {
+    var model = tv.c.test_utils.newModelWithAuditor(function(model) {
+      var renderThread = model.getOrCreateProcess(1).getOrCreateThread(2);
+      renderThread.name = 'RenderThread';
+      renderThread.sliceGroup.pushSlice(newSliceNamed('DrawFrame', 200, 5));
+      renderThread.sliceGroup.pushSlice(newSliceNamed(
+          'BadAlphaView alpha caused saveLayer 480x320', 203, 1));
+    }, tv.e.audits.AndroidAuditor);
+
+    assert.equal(model.alerts.length, 1);
+
+    var alert = model.alerts[0];
+    assert.equal(alert.args.width, 480);
+    assert.equal(alert.args.height, 320);
+  });
+
+  test('addFrameToModel', function() {
+    var process;
+    var model = tv.c.test_utils.newModelWithAuditor(function(model) {
+      process = model.getOrCreateProcess(1);
+      var uiThread = process.getOrCreateThread(1);
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 0, 8));
+    }, tv.e.audits.AndroidAuditor);
+
+    assert.equal(process.frames.length, 1);
+    assert.closeTo(process.frames[0].totalDuration, 8, 1e-5);
+  });
+
+  test('processRenameAndSort', function() {
+    var appProcess;
+    var sfProcess;
+    var model = tv.c.test_utils.newModelWithAuditor(function(model) {
+      appProcess = model.getOrCreateProcess(1);
+      var uiThread = appProcess.getOrCreateThread(1);
+      uiThread.name = 'ndroid.systemui';
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 0, 8));
+
+      sfProcess = model.getOrCreateProcess(2);
+      var sfThread = sfProcess.getOrCreateThread(2);
+      sfThread.name = '/system/bin/surfaceflinger';
+      sfThread.sliceGroup.pushSlice(newSliceNamed('doComposition', 8, 2));
+
+    }, tv.e.audits.AndroidAuditor);
+
+    // both processes should be renamed
+    assert.equal(appProcess.name, 'android.systemui');
+    assert.equal(sfProcess.name, 'SurfaceFlinger');
+
+    assert.isTrue(sfProcess.sortIndex < appProcess.sortIndex);
+  });
+
+  test('drawingThreadPriorities', function() {
+    var uiThread;
+    var renderThread;
+    var workerThread;
+    var otherThread;
+    var model = tv.c.test_utils.newModelWithAuditor(function(model) {
+      var appProcess = model.getOrCreateProcess(1);
+
+      uiThread = appProcess.getOrCreateThread(1);
+      uiThread.name = 'ndroid.systemui';
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 0, 4));
+
+      renderThread = appProcess.getOrCreateThread(2);
+      renderThread.name = 'RenderThread';
+      renderThread.sliceGroup.pushSlice(newSliceNamed('DrawFrame', 3, 4));
+
+      workerThread = appProcess.getOrCreateThread(3);
+      workerThread.name = 'hwuiTask1';
+      workerThread.sliceGroup.pushSlice(newSliceNamed('work', 4, 1));
+
+      otherThread = appProcess.getOrCreateThread(4);
+      otherThread.name = 'other';
+      otherThread.sliceGroup.pushSlice(newSliceNamed('otherWork', 0, 2));
+    }, tv.e.audits.AndroidAuditor);
+
+    assert.isTrue(uiThread.sortIndex < renderThread.sortIndex);
+    assert.isTrue(renderThread.sortIndex < workerThread.sortIndex);
+    assert.isTrue(workerThread.sortIndex < otherThread.sortIndex);
+  });
+
+  test('favicon', function() {
+    var createTraceModelWithJank = function(percentageJank) {
+
+      return tv.c.test_utils.newModelWithAuditor(function(model) {
+        var uiThread = model.getOrCreateProcess(1).getOrCreateThread(1);
+        for (var i = 0; i < 100; i++) {
+          var slice = newSliceNamed('performTraversals',
+                                    30 * i,
+                                    i <= percentageJank ? 24 : 8);
+          uiThread.sliceGroup.pushSlice(slice);
+        }
+      }, tv.e.audits.AndroidAuditor);
+    };
+    assert.equal(createTraceModelWithJank(3).faviconHue, 'green');
+    assert.equal(createTraceModelWithJank(10).faviconHue, 'yellow');
+    assert.equal(createTraceModelWithJank(50).faviconHue, 'red');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/android_model_helper.html b/trace-viewer/trace_viewer/extras/audits/android_model_helper.html
new file mode 100644
index 0000000..6cae739
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/android_model_helper.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="/base/base.html">
+<link rel="import" href="/core/auditor.html">
+<link rel="import" href="/extras/audits/android_app.html">
+<link rel="import" href="/extras/audits/android_surface_flinger.html">
+<link rel="import" href="/extras/audits/utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for managing android-specific model meta data,
+ * such as rendering apps, frames rendered, and SurfaceFlinger.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var AndroidApp = tv.e.audits.AndroidApp;
+  var AndroidSurfaceFlinger = tv.e.audits.AndroidSurfaceFlinger;
+
+  var IMPORTANT_SURFACE_FLINGER_SLICES = {
+    'doComposition' : true,
+    'updateTexImage' : true,
+    'postFramebuffer' : true
+  };
+  var IMPORTANT_UI_THREAD_SLICES = {
+    'performTraversals' : true,
+    'deliverInputEvent' : true
+  };
+  var IMPORTANT_RENDER_THREAD_SLICES = {
+    'doFrame' : true
+  };
+
+  function iterateImportantThreadSlices(thread, important, callback) {
+    if (!thread)
+      return;
+
+    thread.sliceGroup.slices.forEach(function(slice) {
+      if (slice.title in important)
+        callback(slice);
+    });
+  }
+
+  /**
+   * Model for Android-specific data.
+   * @constructor
+   */
+  function AndroidModelHelper(model) {
+    this.model = model;
+    this.apps = [];
+    this.surfaceFlinger = undefined;
+
+    model.getAllProcesses().forEach(function(process) {
+      var app = AndroidApp.createForProcessIfPossible(process);
+      if (app) {
+        this.apps.push(app);
+        return;
+      }
+
+      var sf = AndroidSurfaceFlinger.createForProcessIfPossible(process);
+      if (sf) {
+        this.surfaceFlinger = sf;
+        return;
+      }
+
+      // TODO: classify other useful processes/threads
+    }, this);
+  };
+
+  AndroidModelHelper.prototype = {
+    iterateImportantSlices: function(callback) {
+      if (this.surfaceFlinger) {
+        iterateImportantThreadSlices(
+            this.surfaceFlinger.thread,
+            IMPORTANT_SURFACE_FLINGER_SLICES,
+            callback);
+      }
+
+      this.apps.forEach(function(app) {
+        iterateImportantThreadSlices(
+            app.uiThread,
+            IMPORTANT_UI_THREAD_SLICES,
+            callback);
+        iterateImportantThreadSlices(
+            app.renderThread,
+            IMPORTANT_RENDER_THREAD_SLICES,
+            callback);
+      });
+    }
+  };
+
+  return {
+    AndroidModelHelper: AndroidModelHelper
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/android_model_helper_test.html b/trace-viewer/trace_viewer/extras/audits/android_model_helper_test.html
new file mode 100644
index 0000000..30d1b59
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/android_model_helper_test.html
@@ -0,0 +1,172 @@
+<!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="/extras/audits/android_auditor.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var AndroidModelHelper = tv.e.audits.AndroidModelHelper;
+  var TraceModel = tv.c.TraceModel;
+  var newAsyncSliceNamed = tv.c.test_utils.newAsyncSliceNamed;
+  var newSliceNamed = tv.c.test_utils.newSliceNamed;
+  var newCounterNamed = tv.c.test_utils.newCounterNamed;
+  var newCounterSeries = tv.c.test_utils.newCounterSeries;
+  /*
+   * List of customizeModelCallbacks which produce different 80ms frames,
+   * each starting at 10ms, and with a single important slice
+   */
+  var SINGLE_FRAME_CUSTOM_MODELS = [
+    function(model) {
+      // UI thread only
+      var uiThread = model.getOrCreateProcess(120).getOrCreateThread(120);
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 10, 80));
+
+      model.uiThread = uiThread;
+    },
+
+    function(model) {
+      // RenderThread only
+      var renderThread = model.getOrCreateProcess(120).getOrCreateThread(200);
+      renderThread.name = 'RenderThread';
+      renderThread.sliceGroup.pushSlice(newSliceNamed('doFrame', 10, 80));
+
+      model.renderThread = renderThread;
+    },
+
+    function(model) {
+      var uiThread = model.getOrCreateProcess(120).getOrCreateThread(120);
+
+      // UI thread time - 19 (from 10 to 29)
+      uiThread.asyncSliceGroup.push(
+        newAsyncSliceNamed('deliverInputEvent', 10, 9, uiThread, uiThread));
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 20, 10));
+      uiThread.sliceGroup.pushSlice(newSliceNamed('draw', 20, 8));
+      uiThread.sliceGroup.pushSlice(newSliceNamed('Record View#draw()', 20, 8));
+
+      // RenderThread time - 61 (from 29 to 90)
+      var renderThread = model.getOrCreateProcess(120).getOrCreateThread(200);
+      renderThread.name = 'RenderThread';
+      renderThread.sliceGroup.pushSlice(newSliceNamed('DrawFrame', 29, 61));
+      renderThread.sliceGroup.pushSlice(newSliceNamed('syncFrameState', 29, 1));
+
+      model.uiThread = uiThread;
+      model.renderThread = renderThread;
+    }
+  ];
+
+  test('getThreads', function() {
+    SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) {
+      var model = tv.c.test_utils.newModel(customizeModelCallback);
+      var helper = new AndroidModelHelper(model);
+      assert.equal(helper.apps[0].uiThread, model.uiThread);
+      assert.equal(helper.apps[0].renderThread, model.renderThread);
+    });
+  });
+
+  test('getFrames', function() {
+    SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) {
+      var model = tv.c.test_utils.newModel(customizeModelCallback);
+      var helper = new AndroidModelHelper(model);
+      assert.equal(helper.apps.length, 1);
+
+      var frames = helper.apps[0].getFrames();
+      assert.equal(frames.length, 1);
+      assert.closeTo(frames[0].totalDuration, 80, 1e-5);
+
+      assert.closeTo(frames[0].start, 10, 1e-5);
+      assert.closeTo(frames[0].end, 90, 1e-5);
+    });
+  });
+
+  test('iterateImportantSlices', function() {
+    SINGLE_FRAME_CUSTOM_MODELS.forEach(function(customizeModelCallback) {
+      var model = tv.c.test_utils.newModel(customizeModelCallback);
+      var helper = new AndroidModelHelper(model);
+
+      var seen = 0;
+      helper.iterateImportantSlices(function(importantSlice) {
+        assert.isTrue(importantSlice instanceof tv.c.trace_model.Slice);
+        seen++;
+      });
+      assert.equal(seen, 1);
+    });
+  });
+
+  test('surfaceFlingerVsyncs', function() {
+    var model = tv.c.test_utils.newModel(function(model) {
+      var sfProcess = model.getOrCreateProcess(2);
+      var sfThread = sfProcess.getOrCreateThread(2);
+      sfThread.name = '/system/bin/surfaceflinger';
+      sfThread.sliceGroup.pushSlice(newSliceNamed('doComposition', 8, 2));
+
+      var counter = sfProcess.getOrCreateCounter('android', 'VSYNC');
+      var series = newCounterSeries();
+      for (var i = 0; i <= 10; i++) {
+        series.addCounterSample(i * 10, i % 2);
+      }
+      counter.addSeries(series);
+    });
+    var helper = new AndroidModelHelper(model);
+    assert.isTrue(helper.surfaceFlinger.hasVsyncs);
+
+    // test querying the vsyncs
+    assert.closeTo(helper.surfaceFlinger.getFrameKickoff(5), 0, 1e-5);
+    assert.closeTo(helper.surfaceFlinger.getFrameDeadline(95), 100, 1e-5);
+
+    // test undefined behavior outside of vsyncs.
+    assert.isUndefined(helper.surfaceFlinger.getFrameKickoff(-5));
+    assert.isUndefined(helper.surfaceFlinger.getFrameDeadline(105));
+  });
+
+  test('appInputs', function() {
+    var model = tv.c.test_utils.newModel(function(model) {
+      var process = model.getOrCreateProcess(120);
+      var uiThread = process.getOrCreateThread(120);
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 20, 4));
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 40, 4));
+
+      var counter = process.getOrCreateCounter('android', 'aq:pending:foo');
+      var series = newCounterSeries();
+      series.addCounterSample(10, 1);
+      series.addCounterSample(20, 0);
+      series.addCounterSample(30, 1);
+      series.addCounterSample(40, 2);
+      series.addCounterSample(50, 0);
+      counter.addSeries(series);
+    });
+    var helper = new AndroidModelHelper(model);
+    assert.equal(helper.apps.length, 1);
+
+    var inputSamples = helper.apps[0].getInputSamples();
+    assert.equal(inputSamples.length, 3);
+    assert.equal(inputSamples[0].timestamp, 10);
+    assert.equal(inputSamples[1].timestamp, 30);
+    assert.equal(inputSamples[2].timestamp, 40);
+  });
+
+  test('appAnimations', function() {
+    var model = tv.c.test_utils.newModel(function(model) {
+      var process = model.getOrCreateProcess(120);
+      var uiThread = process.getOrCreateThread(120);
+      uiThread.sliceGroup.pushSlice(newSliceNamed('performTraversals', 10, 10));
+      uiThread.asyncSliceGroup.push(newAsyncSliceNamed('animator:foo', 0, 10,
+                                                       uiThread, uiThread));
+    });
+    var helper = new AndroidModelHelper(model);
+    assert.equal(helper.apps.length, 1);
+
+    var animations = helper.apps[0].getAnimationAsyncSlices();
+    assert.equal(animations.length, 1);
+    assert.equal(animations[0].start, 0);
+    assert.equal(animations[0].end, 10);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/android_surface_flinger.html b/trace-viewer/trace_viewer/extras/audits/android_surface_flinger.html
new file mode 100644
index 0000000..82032e5
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/android_surface_flinger.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="/base/sorted_array_utils.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Class for representing SurfaceFlinger process and its Vsyncs.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var findLowIndexInSortedArray = tv.b.findLowIndexInSortedArray;
+
+  var VSYNC_SF_NAME = 'android.VSYNC-sf';
+  var VSYNC_APP_NAME = 'android.VSYNC-app';
+  var VSYNC_FALLBACK_NAME = 'android.VSYNC';
+
+  function getVsyncTimestamps(process, counterName) {
+
+    var vsync = process.counters[counterName];
+    if (!vsync)
+      vsync = process.counters[VSYNC_FALLBACK_NAME];
+
+    if (vsync && vsync.numSeries == 1 && vsync.numSamples > 1)
+      return vsync.series[0].timestamps;
+    return undefined;
+  }
+
+  /**
+   * Model for SurfaceFlinger specific data.
+   * @constructor
+   */
+  function AndroidSurfaceFlinger(process, thread) {
+    this.process = process;
+    this.thread = thread;
+
+    this.appVsync_ = undefined;
+    this.sfVsync_ = undefined;
+
+    this.appVsyncTimestamps_ = getVsyncTimestamps(process, VSYNC_APP_NAME);
+    this.sfVsyncTimestamps_ = getVsyncTimestamps(process, VSYNC_SF_NAME);
+  };
+
+  AndroidSurfaceFlinger.createForProcessIfPossible = function(process) {
+    var mainThread = process.getThread(process.pid);
+
+    // newer versions - main thread, lowercase name, preceeding forward slash
+    if (mainThread && mainThread.name &&
+        /surfaceflinger/.test(mainThread.name))
+      return new AndroidSurfaceFlinger(process, mainThread);
+
+    // older versions - another thread is named SurfaceFlinger
+    var primaryThreads = process.findAllThreadsNamed('SurfaceFlinger');
+    if (primaryThreads.length == 1)
+      return new AndroidSurfaceFlinger(process, primaryThreads[0]);
+    return undefined;
+  };
+
+  AndroidSurfaceFlinger.prototype = {
+    get hasVsyncs() {
+      return !!this.appVsyncTimestamps_ && !!this.sfVsyncTimestamps_;
+    },
+
+    getFrameKickoff: function(timestamp) {
+      if (!this.hasVsyncs)
+        throw new Error('cannot query vsync info without vsyncs');
+
+      var firstGreaterIndex =
+          findLowIndexInSortedArray(this.appVsyncTimestamps_,
+                                    function(x) { return x; },
+                                    timestamp);
+
+      if (firstGreaterIndex < 1)
+        return undefined;
+      return this.appVsyncTimestamps_[firstGreaterIndex - 1];
+    },
+
+    getFrameDeadline: function(timestamp) {
+      if (!this.hasVsyncs)
+        throw new Error('cannot query vsync info without vsyncs');
+
+      var firstGreaterIndex =
+          findLowIndexInSortedArray(this.sfVsyncTimestamps_,
+                                    function(x) { return x; },
+                                    timestamp);
+      if (firstGreaterIndex >= this.sfVsyncTimestamps_.length)
+        return undefined;
+      return this.sfVsyncTimestamps_[firstGreaterIndex];
+    }
+  };
+
+  return {
+    AndroidSurfaceFlinger: AndroidSurfaceFlinger
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_auditor.html b/trace-viewer/trace_viewer/extras/audits/chrome_auditor.html
new file mode 100644
index 0000000..6a08a0f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_auditor.html
@@ -0,0 +1,170 @@
+<!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/auditor.html">
+<link rel="import" href="/core/trace_model/alert_type.html">
+<link rel="import" href="/extras/audits/utils.html">
+<link rel="import" href="/extras/audits/chrome_model_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for trace data Auditors.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var Auditor = tv.c.Auditor;
+
+  /**
+   * Auditor for Chrome-specific traces.
+   * @constructor
+   */
+  function ChromeAuditor(model) {
+    this.model = model;
+    if (tv.e.audits.ChromeModelHelper.supportsModel(this.model)) {
+      var modelHelper = new tv.e.audits.ChromeModelHelper(this.model);
+
+      // Must be a browser in order to do audits.
+      if (modelHelper.browser === undefined)
+        this.modelHelper = undefined;
+      else
+        this.modelHelper = modelHelper;
+    } else {
+      this.modelHelper = undefined;
+    }
+  };
+
+  ChromeAuditor.prototype = {
+    __proto__: Auditor.prototype,
+
+    runAudit: function() {
+      if (!this.modelHelper)
+        return;
+
+      // ChromeAuditor is disabled when used directly inside about://tracing,
+      // for now.
+      if (window.profilingView)
+        return;
+
+      var allLoadAndNetEvents = this.getAllLoadAndNetEvents();
+      var allFrameEvents = this.getAllFrameEvents();
+      var allLatencyEvents = this.getAllLatencyEvents();
+      var allFrameAndLatencyEvents = [];
+      allFrameAndLatencyEvents.push.apply(allFrameAndLatencyEvents,
+                                          allFrameEvents);
+      allFrameAndLatencyEvents.push.apply(allFrameAndLatencyEvents,
+                                          allLatencyEvents);
+
+      function mergeEventsIntoIR(title, colorId, events) {
+        var e0 = events[0];
+        var eN = events[events.length - 1];
+        var ir = new tv.c.trace_model.InteractionRecord(
+            title, colorId,
+            e0.start, eN.end - e0.start);
+        ir.events = events;
+        return ir;
+      }
+
+      var addIRs = function(records) {
+        records.forEach(function(ir) {
+          this.model.addInteractionRecord(ir);
+        }, this);
+      }.bind(this);
+
+      var mergedLoadIRs = tv.e.audits.mergeEvents(
+          allLoadAndNetEvents, 300,
+          mergeEventsIntoIR.bind(
+              undefined, 'Loading/Net',
+              tv.b.ui.getColorIdForGeneralPurposeString('mt_loading')));
+      addIRs(mergedLoadIRs);
+
+      var mergedFrameIRs = tv.e.audits.mergeEvents(
+          allFrameEvents, 150,
+          mergeEventsIntoIR.bind(
+              undefined, 'Rendering',
+              tv.b.ui.getColorIdForGeneralPurposeString('mt_rendering')));
+      addIRs(mergedFrameIRs);
+
+      var mergedLatencyIRs = tv.e.audits.mergeEvents(
+          allLatencyEvents, 150,
+          mergeEventsIntoIR.bind(
+              undefined, 'Input',
+              tv.b.ui.getColorIdForGeneralPurposeString('mt_input')));
+      addIRs(mergedLatencyIRs);
+
+      this.addBigTaskAlerts();
+    },
+
+    addBigTaskAlerts: function() {
+      var model = this.model;
+      tv.b.iterItems(this.modelHelper.renderers, function(pid, renderer) {
+        var slices = renderer.mainThread.sliceGroup.slices;
+        slices.forEach(function(slice) {
+          if (slice.category != 'toplevel')
+            return;
+          if (slice.duration > 75.0) {
+            var alertType = new tv.c.trace_model.AlertType(
+                'Task too long',
+                'Tasks taking >= 75ms are bad, and should be broken up to' +
+                'ensure the main thread is responsive',
+                tv.c.trace_model.ALERT_SEVERITY.CRITICAL);
+            var center = slice.start + 0.5 * slice.duration;
+            var alert = new tv.c.trace_model.Alert(
+                alertType, center, {/* explanatory data goes here */});
+            model.alerts.push(alert);
+          }
+        });
+      });
+    },
+
+    getAllLoadAndNetEvents: function() {
+      var model = this.model;
+      if (this.modelHelper.browser === undefined)
+        return;
+      var browser = this.modelHelper.browser;
+
+      var events = [];
+      events.push.apply(
+          events, browser.getLoadingEventsInRange(model.bounds));
+      events.push.apply(
+          events, browser.getAllNetworkEventsInRange(model.bounds));
+      return events;
+    },
+
+    getAllFrameEvents: function() {
+      var model = this.model;
+      var frameEvents = [];
+      if (this.modelHelper.browser) {
+        var fe = this.modelHelper.browser.getFrameEventsInRange(
+            tv.e.audits.IMPL_FRAMETIME_TYPE,
+            model.bounds);
+        frameEvents.push.apply(frameEvents, fe);
+      }
+      tv.b.iterItems(this.modelHelper.renderers, function(pid, renderer) {
+        var fe = renderer.getFrameEventsInRange(tv.e.audits.IMPL_FRAMETIME_TYPE,
+                                       model.bounds);
+        frameEvents.push.apply(frameEvents, fe);
+      }, this);
+      frameEvents.sort(function(x, y) { return x.start - y.start; });
+      return frameEvents;
+    },
+
+    getAllLatencyEvents: function() {
+      if (!this.modelHelper.browser)
+        return [];
+      return this.modelHelper.browser.getLatencyEventsInRange(
+          this.model.bounds);
+    }
+  };
+
+  Auditor.register(ChromeAuditor);
+
+  return {
+    ChromeAuditor: ChromeAuditor
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_auditor_test.html b/trace-viewer/trace_viewer/extras/audits/chrome_auditor_test.html
new file mode 100644
index 0000000..fc20ec8
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_auditor_test.html
@@ -0,0 +1,75 @@
+<!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="/extras/audits/chrome_auditor.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/test_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function createModelWithChromeAuditor(customizeModelCallback) {
+    return tv.c.test_utils.newModelWithAuditor(function(m) {
+      m.browserProcess = m.getOrCreateProcess(1);
+      m.browserMain = m.browserProcess.getOrCreateThread(2);
+      m.browserMain.name = 'CrBrowserMain';
+
+      m.renderer1 = m.getOrCreateProcess(3);
+      m.renderer1Main = m.renderer1.getOrCreateThread(4);
+      m.renderer1Main.name = 'CrRendererMain';
+
+      m.renderer1Compositor = m.renderer1.getOrCreateThread(4);
+      m.renderer1Compositor.name = 'Compositor';
+
+      customizeModelCallback(m);
+    }, tv.e.audits.ChromeAuditor);
+  }
+
+  function newInputLatencyEvent(tsStart, tsEnd, opt_args) {
+    var e = new tv.c.trace_model.AsyncSlice(
+        'benchmark', 'InputLatency',
+        tv.b.ui.getColorIdForGeneralPurposeString('InputLatency'),
+        tsStart, opt_args);
+    e.duration = tsEnd - tsStart;
+    return e;
+  }
+
+  function newImplRenderingStatsEvent(ts, opt_args) {
+    var e = new tv.c.trace_model.ThreadSlice(
+        'benchmark', 'BenchmarkInstrumentation::ImplThreadRenderingStats',
+        tv.b.ui.getColorIdForGeneralPurposeString('x'),
+        ts, opt_args, 0);
+    return e;
+  }
+
+  test('simple', function() {
+    var m = createModelWithChromeAuditor(function(m) {
+      var bAsyncSlices = m.browserMain.asyncSliceGroup;
+      bAsyncSlices.push(newInputLatencyEvent(100, 130));
+      bAsyncSlices.push(newInputLatencyEvent(116, 150));
+      bAsyncSlices.push(newInputLatencyEvent(133, 166));
+      bAsyncSlices.push(newInputLatencyEvent(150, 183));
+      bAsyncSlices.push(newInputLatencyEvent(166, 200));
+      bAsyncSlices.push(newInputLatencyEvent(183, 216));
+
+      var rm1Slices = m.renderer1Compositor.sliceGroup;
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(113));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(130));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(147));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(163));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(180));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(197));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(213));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(230));
+      rm1Slices.pushSlices(newImplRenderingStatsEvent(247));
+    });
+
+  });
+});
+</script>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_browser_helper.html b/trace-viewer/trace_viewer/extras/audits/chrome_browser_helper.html
new file mode 100644
index 0000000..e8ac38c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_browser_helper.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="/extras/audits/chrome_process_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var UI_COMP_NAME = 'INPUT_EVENT_LATENCY_UI_COMPONENT';
+  var ORIGINAL_COMP_NAME = 'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT';
+  var BEGIN_COMP_NAME = 'INPUT_EVENT_LATENCY_BEGIN_RWH_COMPONENT';
+  var END_COMP_NAME = 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT';
+
+  function ChromeBrowserHelper(modelHelper, process) {
+    tv.e.audits.ChromeProcessHelper.call(this, modelHelper, process);
+  }
+
+  ChromeBrowserHelper.prototype = {
+    __proto__: tv.e.audits.ChromeProcessHelper.prototype,
+
+    getLoadingEventsInRange: function(rangeOfInterest) {
+      var loadingEvents = [];
+      tv.b.iterItems(this.process.threads, function(tid, thread) {
+        thread.iterateAllEvents(function(event) {
+          if (event.title.indexOf('WebContentsImpl Loading') !== 0)
+            return;
+          if (rangeOfInterest.intersectsExplicitRange(event.start, event.end))
+            loadingEvents.push(event);
+        });
+      });
+      return loadingEvents;
+    },
+
+    get hasLatencyEvents() {
+      var hasLatency = false;
+      this.modelHelper.model.getAllThreads().forEach(function(thread) {
+        thread.iterateAllEvents(function(event) {
+          if (event.title.indexOf('InputLatency') === 0)
+            hasLatency = true;
+        });
+      });
+      return hasLatency;
+    },
+
+    getLatencyEventsInRange: function(rangeOfInterest) {
+      var latencyEvents = [];
+      this.modelHelper.model.getAllThreads().forEach(function(thread) {
+        thread.iterateAllEvents(function(event) {
+          if (event.title.indexOf('InputLatency') !== 0)
+            return;
+          if (rangeOfInterest.intersectsExplicitRange(event.start, event.end))
+            latencyEvents.push(event);
+        });
+      });
+      return latencyEvents;
+    },
+
+    getLatencyDataInRange: function(rangeOfInterest) {
+      var latencyEvents = this.getLatencyEventsInRange(rangeOfInterest);
+
+      // Helper function that computes the input latency for one async slice.
+      function maybeEventToLatencyDatum(event) {
+        if (!('data' in event.args))
+          return;
+
+        var data = event.args.data;
+        if (!(END_COMP_NAME in data))
+          return;
+
+        var latency = 0;
+        var endTime = data[END_COMP_NAME].time;
+        if (ORIGINAL_COMP_NAME in data) {
+          latency = endTime - data[ORIGINAL_COMP_NAME].time;
+        } else if (UI_COMP_NAME in data) {
+          latency = endTime - data[UI_COMP_NAME].time;
+        } else if (BEGIN_COMP_NAME in data) {
+          latency = endTime - data[BEGIN_COMP_NAME].time;
+        } else {
+          throw new Error('No valid begin latency component');
+        }
+        return {
+          'x': event.start,
+          'latency': latency / 1000.0
+        };
+      };
+
+      var latencyData = [];
+      latencyEvents.forEach(function(event) {
+        var latencyDatum = maybeEventToLatencyDatum(event);
+        if (latencyDatum)
+          latencyData.push(latencyDatum);
+      });
+      latencyData.sort(function(a, b) {return a.x - b.x});
+      return latencyData;
+    },
+
+    getAllNetworkEventsInRange: function(rangeOfInterest) {
+      var networkEvents = [];
+      this.modelHelper.model.getAllThreads().forEach(function(thread) {
+        thread.asyncSliceGroup.slices.forEach(function(slice) {
+          var match = false;
+          if (slice.cat == 'net' || // old-style URLRequest/Resource slices.
+              slice.cat == 'disabled-by-default-netlog' || // early netlog.
+              slice.cat == 'netlog') {
+            match = true;
+          }
+
+          if (!match)
+            return;
+
+          if (rangeOfInterest.intersectsExplicitRange(slice.start, slice.end))
+            networkEvents.push(slice);
+        });
+      });
+      return networkEvents;
+    }
+  };
+
+  return {
+    ChromeBrowserHelper: ChromeBrowserHelper
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_model_helper.html b/trace-viewer/trace_viewer/extras/audits/chrome_model_helper.html
new file mode 100644
index 0000000..b43c474
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_model_helper.html
@@ -0,0 +1,113 @@
+<!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/iteration_helpers.html">
+<link rel="import" href="/extras/audits/chrome_browser_helper.html">
+<link rel="import" href="/extras/audits/chrome_renderer_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var CHROME_BROWSER_MAIN_THREAD_NAME = 'CrBrowserMain';
+  var CHROME_RENDERER_THREAD_NAME = 'CrRendererMain';
+
+  function findChromeBrowserProcess(model) {
+    var browsers = [];
+    model.getAllProcesses().forEach(function(process) {
+      if (process.findAllThreadsNamed(
+              CHROME_BROWSER_MAIN_THREAD_NAME).length !== 0)
+        browsers.push(process);
+    }, this);
+    if (browsers.length === 0)
+      return undefined;
+    if (browsers.length > 1)
+      return undefined;
+    return browsers[0];
+  }
+
+  function findChromeRenderProcesses(model) {
+    var rendererProcesses = [];
+    model.getAllProcesses().forEach(function(process) {
+      if (process.findAllThreadsNamed(CHROME_RENDERER_THREAD_NAME).length !== 0)
+        rendererProcesses.push(process);
+    });
+    return rendererProcesses;
+  }
+
+  /**
+   * @constructor
+   */
+  function ChromeModelHelper(model) {
+    this.model_ = model;
+
+    // Find browser.
+    this.browserProcess_ = findChromeBrowserProcess(model);
+    if (this.browserProcess_) {
+      this.browser_ = new tv.e.audits.ChromeBrowserHelper(
+          this, this.browserProcess_);
+    } else {
+      this.browser_ = undefined;
+    }
+
+    // Find renderers.
+    this.rendererProcesses_ = findChromeRenderProcesses(model);
+
+    this.renderers_ = {};
+    this.rendererProcesses_.forEach(function(renderProcess) {
+      var renderer = new tv.e.audits.ChromeRendererHelper(this, renderProcess);
+      this.renderers_[renderer.pid] = renderer;
+    }, this);
+  }
+
+  ChromeModelHelper.supportsModel = function(model) {
+    if (findChromeBrowserProcess(model) !== undefined)
+      return true;
+    if (findChromeRenderProcesses(model).length)
+      return true;
+    return false;
+  }
+
+  ChromeModelHelper.prototype = {
+    get pid() {
+      throw new Error('woah');
+    },
+
+    get process() {
+      throw new Error('woah');
+    },
+
+    get model() {
+      return this.model_;
+    },
+
+    get browserProcess() {
+      return this.browserProcess_;
+    },
+
+    get browser() {
+      return this.browser_;
+    },
+
+    get rendererProcesses() {
+      return this.rendererProcesses_;
+    },
+
+    get renderers() {
+      return this.renderers_;
+    }
+  };
+
+  return {
+    CHROME_RENDERER_THREAD_NAME: CHROME_RENDERER_THREAD_NAME,
+    ChromeModelHelper: ChromeModelHelper
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_model_helper_test.html b/trace-viewer/trace_viewer/extras/audits/chrome_model_helper_test.html
new file mode 100644
index 0000000..90575c3
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_model_helper_test.html
@@ -0,0 +1,127 @@
+<!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="/extras/audits/chrome_model_helper.html">
+<link rel="import" href="/extras/importer/trace_event_importer.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() {
+
+  test('getLatencyData', function() {
+    var events = [];
+    for (var i = 0; i < 10; i++) {
+      var start_ts = i * 10000;
+      var end_ts = i * 10000 + 1000 * (i % 2);
+
+      // Non Input latency related slices
+      events.push({'cat' : 'benchmark', 'pid' : 3507, 'tid': 3507, 'ts' : start_ts, 'ph' : 'S', 'name' : 'Test', 'id' : i}); // @suppress longLineCheck
+      events.push({'cat' : 'benchmark', 'pid' : 3507, 'tid': 3507, 'ts' : end_ts, 'ph' : 'F', 'name' : 'Test', 'id' : i}); // @suppress longLineCheck
+
+      // Input latency sclices
+      events.push({'cat' : 'benchmark', 'pid' : 3507, 'tid': 3507, 'ts' : start_ts, 'ph' : 'S', 'name' : 'InputLatency', 'id' : i}); // @suppress longLineCheck
+      events.push({'cat' : 'benchmark', 'pid' : 3507, 'tid': 3507, 'ts' : end_ts, 'ph' : 'T', 'name' : 'InputLatency', 'args' : {'step' : 'GestureScrollUpdate'}, 'id' : i}); // @suppress longLineCheck
+      events.push({'cat' : 'benchmark', 'pid' : 3507, 'tid': 3507, 'ts' : end_ts, 'ph' : 'F', 'name' : 'InputLatency', 'args' : {'data' : {'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT' : {'time' : start_ts}, 'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT' : {'time' : end_ts}}}, 'id' : i}); // @suppress longLineCheck
+    }
+    events.push({'cat' : '__metadata', 'pid' : 3507, 'tid' : 3507, 'ts' : 0, 'ph' : 'M', 'name' : 'thread_name', 'args' : {'name' : 'CrBrowserMain'}}); // @suppress longLineCheck
+
+    var m = new tv.c.TraceModel(events);
+    var modelHelper = new tv.e.audits.ChromeModelHelper(m);
+    var latencyData = modelHelper.browser.getLatencyDataInRange(m.bounds);
+    assert.equal(latencyData.length, 10);
+    for (var i = 0; i < latencyData.length; i++) {
+      assert.equal(latencyData[i].latency, i % 2);
+    }
+  });
+
+  test('getFrametime', function() {
+    var frame_ts;
+    var events = [];
+    // Browser process 3507
+    events.push({'cat' : '__metadata', 'pid' : 3507, 'tid' : 3507, 'ts' : 0, 'ph' : 'M', 'name' : 'thread_name', 'args' : {'name' : 'CrBrowserMain'}}); // @suppress longLineCheck
+    // Renderer process 3508
+    events.push({'cat' : '__metadata', 'pid' : 3508, 'tid' : 3508, 'ts' : 0, 'ph' : 'M', 'name' : 'thread_name', 'args' : {'name' : 'CrRendererMain'}}); // @suppress longLineCheck
+    // Renderer process 3509
+    events.push({'cat' : '__metadata', 'pid' : 3509, 'tid' : 3509, 'ts' : 0, 'ph' : 'M', 'name' : 'thread_name', 'args' : {'name' : 'CrRendererMain'}}); // @suppress longLineCheck
+
+    frame_ts = 0;
+    // Add impl rendering stats for browser process 3507
+    for (var i = 0; i < 10; i++) {
+      events.push({'cat' : 'benchmark', 'pid' : 3507, 'tid' : 3507, 'ts' : frame_ts, 'ph' : 'i', 'name' : 'BenchmarkInstrumentation::ImplThreadRenderingStats', 's' : 't'}); // @suppress longLineCheck
+      frame_ts += 16000 + 1000 * (i % 2);
+    }
+
+    frame_ts = 0;
+    // Add main rendering stats for renderer process 3508
+    for (var i = 0; i < 10; i++) {
+      events.push({'cat' : 'benchmark', 'pid' : 3508, 'tid' : 3508, 'ts' : frame_ts, 'ph' : 'i', 'name' : 'BenchmarkInstrumentation::MainThreadRenderingStats', 's' : 't'}); // @suppress longLineCheck
+      frame_ts += 16000 + 1000 * (i % 2);
+    }
+
+    frame_ts = 0;
+    // Add impl and main rendering stats for renderer process 3509
+    for (var i = 0; i < 10; i++) {
+      events.push({'cat' : 'benchmark', 'pid' : 3509, 'tid' : 3509, 'ts' : frame_ts, 'ph' : 'i', 'name' : 'BenchmarkInstrumentation::ImplThreadRenderingStats', 's' : 't'}); // @suppress longLineCheck
+      events.push({'cat' : 'benchmark', 'pid' : 3509, 'tid' : 3509, 'ts' : frame_ts, 'ph' : 'i', 'name' : 'BenchmarkInstrumentation::MainThreadRenderingStats', 's' : 't'}); // @suppress longLineCheck
+      frame_ts += 16000 + 1000 * (i % 2);
+    }
+
+    var m = new tv.c.TraceModel(events);
+    var modelHelper = new tv.e.audits.ChromeModelHelper(m);
+
+    // Testing browser impl and main rendering stats.
+    var frameEvents = modelHelper.browser.getFrameEventsInRange(
+        tv.e.audits.IMPL_FRAMETIME_TYPE, m.bounds);
+    var frametimeData = tv.e.audits.getFrametimeDataFromEvents(frameEvents);
+    assert.equal(frametimeData.length, 9);
+    for (var i = 0; i < frametimeData.length; i++) {
+      assert.equal(frametimeData[i].frametime, 16 + i % 2);
+    }
+    // No main rendering stats.
+    frameEvents = modelHelper.browser.getFrameEventsInRange(
+        tv.e.audits.MAIN_FRAMETIME_TYPE, m.bounds);
+    assert.equal(frameEvents.length, 0);
+
+
+    // Testing renderer 3508 impl and main rendering stats.
+    frameEvents = modelHelper.renderers[3508].getFrameEventsInRange(
+        tv.e.audits.MAIN_FRAMETIME_TYPE, m.bounds);
+    frametimeData = tv.e.audits.getFrametimeDataFromEvents(frameEvents);
+    assert.equal(frametimeData.length, 9);
+    for (var i = 0; i < frametimeData.length; i++) {
+      assert.equal(frametimeData[i].frametime, 16 + i % 2);
+    }
+
+    // No impl rendering stats.
+    frameEvents = modelHelper.renderers[3508].getFrameEventsInRange(
+        tv.e.audits.IMPL_FRAMETIME_TYPE, m.bounds);
+    assert.equal(frameEvents.length, 0);
+
+
+    // Testing renderer 3509 impl and main rendering stats.
+    frameEvents = modelHelper.renderers[3509].getFrameEventsInRange(
+        tv.e.audits.IMPL_FRAMETIME_TYPE, m.bounds);
+    frametimeData = tv.e.audits.getFrametimeDataFromEvents(frameEvents);
+    assert.equal(frametimeData.length, 9);
+    for (var i = 0; i < frametimeData.length; i++) {
+      assert.equal(frametimeData[i].frametime, 16 + i % 2);
+    }
+
+    frameEvents = modelHelper.renderers[3509].getFrameEventsInRange(
+        tv.e.audits.MAIN_FRAMETIME_TYPE, m.bounds);
+    frametimeData = tv.e.audits.getFrametimeDataFromEvents(frameEvents);
+    assert.equal(frametimeData.length, 9);
+    for (var i = 0; i < frametimeData.length; i++) {
+      assert.equal(frametimeData[i].frametime, 16 + i % 2);
+    }
+
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_process_helper.html b/trace-viewer/trace_viewer/extras/audits/chrome_process_helper.html
new file mode 100644
index 0000000..70f3641
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_process_helper.html
@@ -0,0 +1,89 @@
+<!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';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+tv.exportTo('tv.e.audits', function() {
+  var MAIN_FRAMETIME_TYPE = 'main_frametime_type';
+  var IMPL_FRAMETIME_TYPE = 'impl_frametime_type';
+
+  var MAIN_RENDERING_STATS =
+      'BenchmarkInstrumentation::MainThreadRenderingStats';
+  var IMPL_RENDERING_STATS =
+      'BenchmarkInstrumentation::ImplThreadRenderingStats';
+
+
+  function getSlicesIntersectingRange(rangeOfInterest, slices) {
+    var slicesInFilterRange = [];
+    for (var i = 0; i < slices.length; i++) {
+      var slice = slices[i];
+      if (rangeOfInterest.intersectsExplicitRange(slice.start, slice.end))
+        slicesInFilterRange.push(slice);
+    }
+    return slicesInFilterRange;
+  }
+
+
+  function ChromeProcessHelper(modelHelper, process) {
+    this.modelHelper = modelHelper;
+    this.process = process;
+  }
+
+  ChromeProcessHelper.prototype = {
+    get pid() {
+      return this.process.pid;
+    },
+
+    getFrameEventsInRange: function(frametimeType, range) {
+      var titleToGet;
+      if (frametimeType == MAIN_FRAMETIME_TYPE)
+        titleToGet = MAIN_RENDERING_STATS;
+      else
+        titleToGet = IMPL_RENDERING_STATS;
+
+      var frameEvents = [];
+      this.process.iterateAllEvents(function(event) {
+        if (event.title !== titleToGet)
+          return;
+        if (range.intersectsExplicitRange(event.start, event.end))
+          frameEvents.push(event);
+
+      });
+
+      frameEvents.sort(function(a, b) {return a.start - b.start});
+      return frameEvents;
+    }
+  };
+
+  function getFrametimeDataFromEvents(frameEvents) {
+    var frametimeData = [];
+    for (var i = 1; i < frameEvents.length; i++) {
+      var diff = frameEvents[i].start - frameEvents[i - 1].start;
+      frametimeData.push({
+        'x': frameEvents[i].start,
+        'frametime': diff
+      });
+    }
+    return frametimeData;
+  }
+
+  return {
+    ChromeProcessHelper: ChromeProcessHelper,
+
+    MAIN_FRAMETIME_TYPE: MAIN_FRAMETIME_TYPE,
+    IMPL_FRAMETIME_TYPE: IMPL_FRAMETIME_TYPE,
+
+    getSlicesIntersectingRange: getSlicesIntersectingRange,
+    getFrametimeDataFromEvents: getFrametimeDataFromEvents
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/chrome_renderer_helper.html b/trace-viewer/trace_viewer/extras/audits/chrome_renderer_helper.html
new file mode 100644
index 0000000..97951bb
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/chrome_renderer_helper.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="/extras/audits/chrome_process_helper.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Utilities for accessing trace data about the Chrome browser.
+ */
+tv.exportTo('tv.e.audits', function() {
+  function ChromeRendererHelper(modelHelper, process) {
+    tv.e.audits.ChromeProcessHelper.call(this, modelHelper, process);
+    var mains = process.findAllThreadsNamed(
+        tv.e.audits.CHROME_RENDERER_THREAD_NAME);
+    if (mains.length !== 1) throw new Error('omgah: more than one renderer!');
+    this.mainThread_ = mains[0];
+  }
+
+  ChromeRendererHelper.prototype = {
+    __proto__: tv.e.audits.ChromeProcessHelper.prototype,
+
+    get mainThread() {
+      return this.mainThread_;
+    }
+  };
+
+  return {
+    ChromeRendererHelper: ChromeRendererHelper
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/utils.html b/trace-viewer/trace_viewer/extras/audits/utils.html
new file mode 100644
index 0000000..7d7e584
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/utils.html
@@ -0,0 +1,86 @@
+<!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/iteration_helpers.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides event merging functionality for grouping/analysis.
+ */
+tv.exportTo('tv.e.audits', function() {
+
+  function mergeEvents(inEvents, mergeThreshold, mergeFunction,
+                       opt_startFunction, opt_endFunction) {
+    var startFunction = opt_startFunction;
+    var endFunction = opt_endFunction;
+    if (!startFunction)
+      startFunction = function(event) { return event.start; };
+    if (!endFunction)
+      endFunction = function(event) { return event.end; };
+
+    var remainingEvents = inEvents.slice();
+    remainingEvents.sort(function(x, y) {
+      return startFunction(x) - startFunction(y);
+    });
+
+    if (remainingEvents.length <= 1) {
+      var merged = [];
+      if (remainingEvents.length == 1) {
+        merged.push(mergeFunction(remainingEvents));
+      }
+      return merged;
+    }
+
+    var mergedEvents = [];
+
+    var currentMergeBuffer = [];
+    var rightEdge;
+    function beginMerging() {
+      currentMergeBuffer.push(remainingEvents[0]);
+      remainingEvents.splice(0, 1);
+      rightEdge = endFunction(currentMergeBuffer[0]);
+    }
+
+    function flushCurrentMergeBuffer() {
+      if (currentMergeBuffer.length == 0)
+        return;
+
+      mergedEvents.push(mergeFunction(currentMergeBuffer));
+      currentMergeBuffer = [];
+
+      // Refill merge buffer if needed.
+      if (remainingEvents.length != 0)
+        beginMerging();
+    }
+
+    beginMerging();
+
+    while (remainingEvents.length) {
+      var currentEvent = remainingEvents[0];
+
+      var distanceFromRightEdge = startFunction(currentEvent) - rightEdge;
+      if (distanceFromRightEdge < mergeThreshold) {
+        rightEdge = Math.max(rightEdge, endFunction(currentEvent));
+        remainingEvents.splice(0, 1);
+        currentMergeBuffer.push(currentEvent);
+        continue;
+      }
+
+      // Too big a gap.
+      flushCurrentMergeBuffer();
+    }
+    flushCurrentMergeBuffer();
+
+    return mergedEvents;
+  }
+
+  return {
+    mergeEvents: mergeEvents
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/audits/utils_test.html b/trace-viewer/trace_viewer/extras/audits/utils_test.html
new file mode 100644
index 0000000..c229af7
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/audits/utils_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="/extras/audits/utils.html">
+<link rel="import" href="/core/test_utils.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function simpleMerger(events) {
+    return {
+      start: events[0].start,
+      end: events[events.length - 1].end
+    };
+  }
+  test('simple', function() {
+    var inEvents = [
+      {start: 0, end: 100},
+      {start: 100, end: 120},
+      {start: 200, end: 220}
+    ];
+
+    var merged = tv.e.audits.mergeEvents(inEvents, 50, simpleMerger);
+
+    assert.equal(merged.length, 2);
+    assert.deepEqual(merged[0], {start: 0, end: 120});
+    assert.deepEqual(merged[1], {start: 200, end: 220});
+  });
+
+  test('overlapping', function() {
+    var inEvents = [
+      {start: 0, end: 100},
+      {start: 80, end: 120},
+      {start: 200, end: 220}
+    ];
+
+    var merged = tv.e.audits.mergeEvents(inEvents, 50, simpleMerger);
+
+    assert.equal(merged.length, 2);
+    assert.deepEqual(merged[0], {start: 0, end: 120});
+    assert.deepEqual(merged[1], {start: 200, end: 220});
+  });
+
+  test('middleOneIsSmall', function() {
+    var inEvents = [
+      {start: 0, end: 100},
+      {start: 40, end: 50},
+      {start: 100, end: 120}
+    ];
+
+    var merged = tv.e.audits.mergeEvents(inEvents, 50, simpleMerger);
+
+    assert.equal(merged.length, 1);
+    assert.deepEqual(merged[0], {start: 0, end: 120});
+  });
+
+  test('firstEventIsSplitPoint', function() {
+    var inEvents = [
+      {start: 0, end: 100},
+      {start: 150, end: 200}
+    ];
+
+    var merged = tv.e.audits.mergeEvents(inEvents, 25, simpleMerger);
+
+    assert.equal(merged.length, 2);
+    assert.deepEqual(merged[0], {start: 0, end: 100});
+    assert.deepEqual(merged[1], {start: 150, end: 200});
+  });
+
+  test('mergeSingleEvent', function() {
+    var inEvents = [
+      {start: 0, end: 100}
+    ];
+
+    var mergeCount = 0;
+    var merged = tv.e.audits.mergeEvents(inEvents, 25, function(events) {
+      assert.deepEqual(events, inEvents);
+      mergeCount++;
+    });
+    assert.equal(mergeCount, 1);
+  });
+
+  test('zeroDurationSplit', function() {
+    var inEvents = [0, 10, 20, 50, 60];
+    var identityFunction = function(x) {
+      return x;
+    };
+    var timestampMerger = function(timestamps) {
+      console.log(timestamps);
+      return {
+        start: timestamps[0],
+        end: timestamps[timestamps.length - 1]
+      };
+    };
+    var merged = tv.e.audits.mergeEvents(inEvents, 15, timestampMerger,
+                                         identityFunction, identityFunction);
+    console.log(merged);
+    assert.equal(merged.length, 2);
+    assert.deepEqual(merged[0], {start: 0, end: 20});
+    assert.deepEqual(merged[1], {start: 50, end: 60});
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/cc.html b/trace-viewer/trace_viewer/extras/cc/cc.html
new file mode 100644
index 0000000..b1d0c52
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/cc.html
@@ -0,0 +1,17 @@
+<!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/cc/display_item_list.html">
+<link rel="import" href="/extras/cc/layer_tree_host_impl.html">
+<link rel="import" href="/extras/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/extras/cc/picture.html">
+<link rel="import" href="/extras/cc/picture_view.html">
+<link rel="import" href="/extras/cc/display_item_view.html">
+<link rel="import" href="/extras/cc/raster_task_view.html">
+<link rel="import" href="/extras/cc/raster_task_selection.html">
+<link rel="import" href="/extras/cc/tile.html">
+<link rel="import" href="/extras/cc/tile_view.html">
diff --git a/trace-viewer/trace_viewer/extras/cc/constants.html b/trace-viewer/trace_viewer/extras/cc/constants.html
new file mode 100644
index 0000000..9b7a7da
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/constants.html
@@ -0,0 +1,23 @@
+<!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.e.cc', function() {
+  var constants = {};
+  constants.ACTIVE_TREE = 0;
+  constants.PENDING_TREE = 1;
+
+  constants.HIGH_PRIORITY_BIN = 0;
+  constants.LOW_PRIORITY_BIN = 1;
+
+  return {
+    constants: constants
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/debug_colors.html b/trace-viewer/trace_viewer/extras/cc/debug_colors.html
new file mode 100644
index 0000000..87c2dba
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/debug_colors.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/base.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Mapping of different tile configuration
+ * to border colors and widths.
+ */
+tv.exportTo('tv.e.cc', function() {
+  var tileTypes = {
+    highRes: 'highRes',
+    lowRes: 'lowRes',
+    extraHighRes: 'extraHighRes',
+    extraLowRes: 'extraLowRes',
+    missing: 'missing',
+    culled: 'culled',
+    solidColor: 'solidColor',
+    picture: 'picture',
+    directPicture: 'directPicture',
+    unknown: 'unknown'
+  };
+
+  var tileBorder = {
+    highRes: {
+      color: 'rgba(80, 200, 200, 0.7)',
+      width: 1
+    },
+    lowRes: {
+      color: 'rgba(212, 83, 192, 0.7)',
+      width: 2
+    },
+    extraHighRes: {
+      color: 'rgba(239, 231, 20, 0.7)',
+      width: 2
+    },
+    extraLowRes: {
+      color: 'rgba(93, 186, 18, 0.7)',
+      width: 2
+    },
+    missing: {
+      color: 'rgba(255, 0, 0, 0.7)',
+      width: 1
+    },
+    culled: {
+      color: 'rgba(160, 100, 0, 0.8)',
+      width: 1
+    },
+    solidColor: {
+      color: 'rgba(128, 128, 128, 0.7)',
+      width: 1
+    },
+    picture: {
+      color: 'rgba(64, 64, 64, 0.7)',
+      width: 1
+    },
+    directPicture: {
+      color: 'rgba(127, 255, 0, 1.0)',
+      width: 1
+    },
+    unknown: {
+      color: 'rgba(0, 0, 0, 1.0)',
+      width: 2
+    }
+  };
+
+  return {
+    tileTypes: tileTypes,
+    tileBorder: tileBorder
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/display_item_debugger.html b/trace-viewer/trace_viewer/extras/cc/display_item_debugger.html
new file mode 100644
index 0000000..ebe572b
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/display_item_debugger.html
@@ -0,0 +1,409 @@
+<!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="/extras/cc/picture.html">
+<link rel="import" href="/extras/cc/picture_ops_list_view.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/base/key_event_manager.html">
+<link rel="import" href="/base/ui/drag_handle.html">
+<link rel="import" href="/base/ui/info_bar.html">
+<link rel="import" href="/base/ui/list_view.html">
+<link rel="import" href="/base/ui/mouse_mode_selector.html">
+<link rel="import" href="/base/ui/overlay.html">
+<link rel="import" href="/base/utils.html">
+
+<template id="display-item-debugger-template">
+  <style>
+  display-item-debugger {
+    -webkit-flex: 1 1 auto;
+    display: -webkit-flex;
+  }
+
+  display-item-debugger > left-panel {
+    -webkit-flex-direction: column;
+    display: -webkit-flex;
+    min-width: 300px;
+    overflow-y: auto;
+  }
+
+  display-item-debugger > left-panel > display-item-info {
+    -webkit-flex: 1 1 auto;
+    padding-top: 2px;
+  }
+
+  display-item-debugger > left-panel > display-item-info .title {
+    font-weight: bold;
+    margin-left: 5px;
+    margin-right: 5px;
+  }
+
+  display-item-debugger > x-drag-handle {
+    -webkit-flex: 0 0 auto;
+  }
+
+  display-item-debugger > right-panel {
+    -webkit-flex: 1 1 auto;
+    display: -webkit-flex;
+  }
+
+  display-item-debugger > left-panel > display-item-info > header {
+    border-bottom: 1px solid #555;
+  }
+
+  display-item-debugger > left-panel > display-item-info > .x-list-view > div {
+    border-bottom: 1px solid #555;
+    padding-top: 3px;
+    padding-bottom: 3px;
+    padding-left: 5px;
+  }
+
+  display-item-debugger > left-panel > display-item-info >
+      .x-list-view > div:hover {
+    background-color: #f0f0f0;
+    cursor: pointer;
+  }
+
+  /******************************************************************************/
+
+  display-item-debugger > right-panel > picture-ops-list-view.hasPictureOps {
+    display: block;
+  }
+
+  display-item-debugger > right-panel > x-drag-handle.hasPictureOps {
+    display: block;
+  }
+
+  display-item-debugger > right-panel > picture-ops-list-view {
+    display: none;
+    overflow-y: auto;
+  }
+
+  display-item-debugger > right-panel > x-drag-handle {
+    display: none;
+  }
+
+  raster-area {
+    -webkit-flex: 1 1 auto;
+    background-color: #ddd;
+    min-height: 200px;
+    min-width: 200px;
+    overflow-y: auto;
+    padding-left: 5px;
+  }
+  </style>
+
+  <left-panel>
+    <display-item-info>
+      <header>
+        <span class='title'>Display Item List</span>
+        <span class='size'></span>
+      </header>
+    </display-item-info>
+  </left-panel>
+  <right-panel>
+    <raster-area><canvas></canvas></raster-area>
+  </right-panel>
+</template>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  /**
+   * DisplayItemDebugger is a view of a DisplayItemListSnapshot for inspecting
+   * a display item list and the pictures within it.
+   *
+   * @constructor
+   */
+  var DisplayItemDebugger = tv.b.ui.define('display-item-debugger');
+
+  DisplayItemDebugger.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      var node = tv.b.instantiateTemplate('#display-item-debugger-template',
+          THIS_DOC);
+
+      this.appendChild(node);
+
+      this.pictureAsImageData_ = undefined;
+      this.zoomScaleValue_ = 1;
+
+      this.sizeInfo_ = this.querySelector('.size');
+      this.rasterArea_ = this.querySelector('raster-area');
+      this.rasterCanvas_ = this.rasterArea_.querySelector('canvas');
+      this.rasterCtx_ = this.rasterCanvas_.getContext('2d');
+
+      this.trackMouse_();
+
+      this.displayItemInfo_ = this.querySelector('display-item-info');
+      this.displayItemInfo_.addEventListener(
+          'click', this.onDisplayItemInfoClick_.bind(this), false);
+
+      this.displayItemListView_ = new tv.b.ui.ListView();
+      this.displayItemListView_.addEventListener('selection-changed',
+          this.onDisplayItemListSelection_.bind(this));
+      this.displayItemInfo_.appendChild(this.displayItemListView_);
+
+      var leftPanel = this.querySelector('left-panel');
+
+      var middleDragHandle = new tv.b.ui.DragHandle();
+      middleDragHandle.horizontal = false;
+      middleDragHandle.target = leftPanel;
+
+      var rightPanel = this.querySelector('right-panel');
+
+      this.infoBar_ = document.createElement('tv-b-ui-info-bar');
+      this.rasterArea_.insertBefore(this.infoBar_, this.rasterCanvas_);
+
+      this.insertBefore(middleDragHandle, rightPanel);
+
+      this.picture_ = undefined;
+
+      this.pictureOpsListView_ = new tv.e.cc.PictureOpsListView();
+      rightPanel.insertBefore(this.pictureOpsListView_, this.rasterArea_);
+
+      this.pictureOpsListDragHandle_ = new tv.b.ui.DragHandle();
+      this.pictureOpsListDragHandle_.horizontal = false;
+      this.pictureOpsListDragHandle_.target = this.pictureOpsListView_;
+      rightPanel.insertBefore(this.pictureOpsListDragHandle_, this.rasterArea_);
+    },
+
+    get picture() {
+      return this.picture_;
+    },
+
+    set displayItemList(displayItemList) {
+      this.displayItemList_ = displayItemList;
+      this.picture = this.displayItemList_;
+
+      this.displayItemListView_.clear();
+      this.displayItemList_.items.forEach(function(item) {
+        var newListItem = document.createElement('div');
+        newListItem.innerText = item;
+        // FIXME: We should improve our output to better format this.
+        var text = item.skp64 ? item.name : item;
+        this.displayItemListView_.addItem(text);
+      }.bind(this));
+    },
+
+    set picture(picture) {
+      this.picture_ = picture;
+
+      // Hide the ops list if we are showing the "main" display item list.
+      var showOpsList = picture && picture !== this.displayItemList_;
+      this.updateDrawOpsList_(showOpsList);
+
+      if (picture) {
+        var size = this.getRasterCanvasSize_();
+        this.rasterCanvas_.width = size.width;
+        this.rasterCanvas_.height = size.height;
+      }
+
+      var bounds = this.rasterArea_.getBoundingClientRect();
+      var selectorBounds = this.mouseModeSelector_.getBoundingClientRect();
+      this.mouseModeSelector_.pos = {
+        x: (bounds.right - selectorBounds.width - 10),
+        y: bounds.top
+      };
+
+      this.rasterize_();
+
+      this.scheduleUpdateContents_();
+    },
+
+    getRasterCanvasSize_: function() {
+      var style = window.getComputedStyle(this.rasterArea_);
+      var width = parseInt(style.width);
+      var height = parseInt(style.height);
+      if (this.picture_) {
+        width = Math.max(width, this.picture_.layerRect.width);
+        height = Math.max(height, this.picture_.layerRect.height);
+      }
+
+      return {
+        width: width,
+        height: height
+      };
+    },
+
+    scheduleUpdateContents_: function() {
+      if (this.updateContentsPending_)
+        return;
+      this.updateContentsPending_ = true;
+      tv.b.requestAnimationFrameInThisFrameIfPossible(
+          this.updateContents_.bind(this)
+      );
+    },
+
+    updateContents_: function() {
+      this.updateContentsPending_ = false;
+
+      if (this.picture_) {
+        this.sizeInfo_.textContent = '(' +
+            this.picture_.layerRect.width + ' x ' +
+            this.picture_.layerRect.height + ')';
+      }
+
+      // Return if picture hasn't finished rasterizing.
+      if (!this.pictureAsImageData_)
+        return;
+
+      this.infoBar_.visible = false;
+      this.infoBar_.removeAllButtons();
+      if (this.pictureAsImageData_.error) {
+        this.infoBar_.message = 'Cannot rasterize...';
+        this.infoBar_.addButton('More info...', function(e) {
+          var overlay = new tv.b.ui.Overlay();
+          overlay.textContent = this.pictureAsImageData_.error;
+          overlay.visible = true;
+          e.stopPropagation();
+          return false;
+        }.bind(this));
+        this.infoBar_.visible = true;
+      }
+
+      this.drawPicture_();
+    },
+
+    drawPicture_: function() {
+      var size = this.getRasterCanvasSize_();
+      if (size.width !== this.rasterCanvas_.width)
+        this.rasterCanvas_.width = size.width;
+      if (size.height !== this.rasterCanvas_.height)
+        this.rasterCanvas_.height = size.height;
+
+      this.rasterCtx_.clearRect(0, 0, size.width, size.height);
+
+      if (!this.picture_ || !this.pictureAsImageData_.imageData)
+        return;
+
+      var imgCanvas = this.pictureAsImageData_.asCanvas();
+      var w = imgCanvas.width;
+      var h = imgCanvas.height;
+      this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h,
+                                0, 0, w * this.zoomScaleValue_,
+                                h * this.zoomScaleValue_);
+    },
+
+    rasterize_: function() {
+      if (this.picture_) {
+        this.picture_.rasterize(
+            {
+              showOverdraw: false
+            },
+            this.onRasterComplete_.bind(this));
+      }
+    },
+
+    onRasterComplete_: function(pictureAsImageData) {
+      this.pictureAsImageData_ = pictureAsImageData;
+      this.scheduleUpdateContents_();
+    },
+
+    onDisplayItemListSelection_: function(e) {
+      var selected = this.displayItemListView_.selectedElement;
+
+      if (!selected) {
+        this.picture = this.displayItemList_;
+        return;
+      }
+
+      var index = Array.prototype.indexOf.call(
+          this.displayItemListView_.children, selected);
+      var displayItem = this.displayItemList_.items[index];
+      if (displayItem && displayItem.skp64)
+        this.picture = new tv.e.cc.Picture(
+            displayItem.skp64, this.displayItemList_.layerRect);
+      else
+        this.picture = undefined;
+    },
+
+    onDisplayItemInfoClick_: function(e) {
+      if (e && e.target == this.displayItemInfo_) {
+        this.displayItemListView_.selectedElement = undefined;
+      }
+    },
+
+    updateDrawOpsList_: function(showOpsList) {
+      if (showOpsList) {
+        this.pictureOpsListView_.picture = this.picture_;
+        if (this.pictureOpsListView_.numOps > 0) {
+          this.pictureOpsListView_.classList.add('hasPictureOps');
+          this.pictureOpsListDragHandle_.classList.add('hasPictureOps');
+        }
+      } else {
+        this.pictureOpsListView_.classList.remove('hasPictureOps');
+        this.pictureOpsListDragHandle_.classList.remove('hasPictureOps');
+      }
+    },
+
+    trackMouse_: function() {
+      this.mouseModeSelector_ = new tv.b.ui.MouseModeSelector(this.rasterArea_);
+      this.rasterArea_.appendChild(this.mouseModeSelector_);
+
+      this.mouseModeSelector_.supportedModeMask =
+          tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM;
+      this.mouseModeSelector_.mode = tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM;
+      this.mouseModeSelector_.defaultMode = tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM;
+      this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector';
+
+      this.mouseModeSelector_.addEventListener('beginzoom',
+          this.onBeginZoom_.bind(this));
+      this.mouseModeSelector_.addEventListener('updatezoom',
+          this.onUpdateZoom_.bind(this));
+      this.mouseModeSelector_.addEventListener('endzoom',
+          this.onEndZoom_.bind(this));
+    },
+
+    onBeginZoom_: function(e) {
+      this.isZooming_ = true;
+
+      this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+
+      e.preventDefault();
+    },
+
+    onUpdateZoom_: function(e) {
+      if (!this.isZooming_)
+        return;
+
+      var currentMouseViewPos = this.extractRelativeMousePosition_(e);
+
+      // Take the distance the mouse has moved and we want to zoom at about
+      // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned
+      // more if people feel it's too slow.
+      this.zoomScaleValue_ +=
+          ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001);
+      this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1);
+
+      this.drawPicture_();
+
+      this.lastMouseViewPos_ = currentMouseViewPos;
+    },
+
+    onEndZoom_: function(e) {
+      this.lastMouseViewPos_ = undefined;
+      this.isZooming_ = false;
+      e.preventDefault();
+    },
+
+    extractRelativeMousePosition_: function(e) {
+      return {
+        x: e.clientX - this.rasterArea_.offsetLeft,
+        y: e.clientY - this.rasterArea_.offsetTop
+      };
+    }
+  };
+
+  return {
+    DisplayItemDebugger: DisplayItemDebugger
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/display_item_debugger_test.html b/trace-viewer/trace_viewer/extras/cc/display_item_debugger_test.html
new file mode 100644
index 0000000..8f5eb36
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/display_item_debugger_test.html
@@ -0,0 +1,91 @@
+<!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="/extras/cc/display_item_list.html">
+<link rel="import" href="/extras/cc/display_item_debugger.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var displayItemList = new tv.e.cc.DisplayItemListSnapshot(
+      {id: '31415'},
+      10,
+      {
+        'params': {
+          'layer_rect': [-15, -15, 46, 833],
+          'items': [
+            'BeginClipDisplayItem',
+            'EndClipDisplayItem'
+          ]
+        },
+        'skp64': '[another skia picture in base64]'});
+    displayItemList.preInitialize();
+    displayItemList.initialize();
+
+    var dbg = new tv.e.cc.DisplayItemDebugger();
+    this.addHTMLOutput(dbg);
+    assert.isUndefined(dbg.displayItemList_);
+    assert.isUndefined(dbg.picture_);
+    dbg.displayItemList = displayItemList;
+    assert.isDefined(dbg.displayItemList_);
+    assert.isDefined(dbg.picture_);
+    assert.equal(dbg.displayItemList_.items.length, 2);
+    dbg.style.border = '1px solid black';
+  });
+
+  test('selections', function() {
+    var displayItemList = new tv.e.cc.DisplayItemListSnapshot(
+      {id: '31415'},
+      10,
+      {
+        'params': {
+          'layer_rect': [-15, -15, 46, 833],
+          'items': [
+            'BeginClipDisplayItem',
+            'TransformDisplayItem',
+            {'name': 'DrawingDisplayItem', 'skp64': '[skia picture in base64]'},
+            'EndTransformDisplayItem',
+            'EndClipDisplayItem'
+          ]
+        },
+        'skp64': '[another skia picture in base64]'});
+    displayItemList.preInitialize();
+    displayItemList.initialize();
+
+    var dbg = new tv.e.cc.DisplayItemDebugger();
+    this.addHTMLOutput(dbg);
+    dbg.displayItemList = displayItemList;
+    assert.isDefined(dbg.displayItemList_);
+    assert.isDefined(dbg.picture_);
+    assert.equal(dbg.displayItemList_.items.length, 5);
+
+    var initialPicture = dbg.picture_;
+    assert.isAbove(initialPicture.guid, 0);
+
+    // Select the drawing display item and make sure the picture updates.
+    var listView = dbg.displayItemListView_;
+    listView.selectedElement = listView.getElementByIndex(3);
+    var updatedPicture = dbg.picture_;
+    assert.isAbove(updatedPicture.guid, 0);
+    assert.notEqual(initialPicture.guid, updatedPicture.guid);
+
+    // Select the TransformDisplayItem and make sure the picture is blank.
+    listView.selectedElement = listView.getElementByIndex(2);
+    assert.isUndefined(dbg.picture_);
+
+    // Deselect a list item and make sure the picture is reset to the original.
+    listView.selectedElement = undefined;
+    updatedPicture = dbg.picture_;
+    assert.isAbove(updatedPicture.guid, 0);
+    assert.equal(initialPicture.guid, updatedPicture.guid);
+
+    dbg.style.border = '1px solid black';
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/display_item_list.html b/trace-viewer/trace_viewer/extras/cc/display_item_list.html
new file mode 100644
index 0000000..0395c3c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/display_item_list.html
@@ -0,0 +1,54 @@
+<!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="/extras/cc/picture.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  function DisplayItemList(skp64, layerRect) {
+    tv.e.cc.Picture.apply(this, arguments);
+  }
+
+  DisplayItemList.prototype = {
+    __proto__: tv.e.cc.Picture.prototype
+  };
+
+  /**
+   * @constructor
+   */
+  function DisplayItemListSnapshot() {
+    tv.e.cc.PictureSnapshot.apply(this, arguments);
+  }
+
+  DisplayItemListSnapshot.prototype = {
+    __proto__: tv.e.cc.PictureSnapshot.prototype,
+
+    initialize: function() {
+      tv.e.cc.PictureSnapshot.prototype.initialize.call(this);
+      this.displayItems_ = this.args.params.items;
+    },
+
+    get items() {
+      return this.displayItems_;
+    }
+  };
+
+  ObjectSnapshot.register(
+      DisplayItemListSnapshot,
+      {typeNames: ['cc::DisplayItemList']});
+
+  return {
+    DisplayItemListSnapshot: DisplayItemListSnapshot,
+    DisplayItemList: DisplayItemList
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/display_item_list_test.html b/trace-viewer/trace_viewer/extras/cc/display_item_list_test.html
new file mode 100644
index 0000000..05500eb
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/display_item_list_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="/extras/cc/cc.html">
+<link rel="import" href="/extras/cc/display_item_list.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('basic', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::DisplayItemList')[0];
+    var snapshot = instance.snapshots[0];
+
+    assert.instanceOf(snapshot, tv.e.cc.DisplayItemListSnapshot);
+    instance.wasDeleted(150);
+  });
+
+  test('getItems', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::DisplayItemList')[0];
+    var snapshot = instance.snapshots[0];
+
+    var items = snapshot.items;
+    assert.equal(items.length, 2);
+
+    assert.equal(items[0], 'BeginClipDisplayItem');
+    assert.equal(items[1], 'EndClipDisplayItem');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/display_item_view.css b/trace-viewer/trace_viewer/extras/cc/display_item_view.css
new file mode 100644
index 0000000..15d34a2
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/display_item_view.css
@@ -0,0 +1,9 @@
+/* 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.
+ */
+
+.display-item-view {
+  -webkit-flex: 1 1 auto !important;
+  display: -webkit-flex;
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/display_item_view.html b/trace-viewer/trace_viewer/extras/cc/display_item_view.html
new file mode 100644
index 0000000..df60758
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/display_item_view.html
@@ -0,0 +1,54 @@
+<!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="stylesheet" href="/extras/cc/display_item_view.css">
+
+<link rel="import" href="/extras/cc/display_item_list.html">
+<link rel="import" href="/extras/cc/display_item_debugger.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /*
+   * Displays a display item snapshot in a human readable form.
+   * @constructor
+   */
+  var DisplayItemSnapshotView = tv.b.ui.define(
+      'display-item-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  DisplayItemSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('display-item-view');
+      this.displayItemDebugger_ = new tv.e.cc.DisplayItemDebugger();
+      this.appendChild(this.displayItemDebugger_);
+    },
+
+    updateContents: function() {
+      if (this.objectSnapshot_ && this.displayItemDebugger_)
+        this.displayItemDebugger_.displayItemList = this.objectSnapshot_;
+    }
+  };
+
+  tv.c.analysis.ObjectSnapshotView.register(
+      DisplayItemSnapshotView,
+      {
+        typeNames: ['cc::DisplayItemList'],
+        showInstances: false
+      });
+
+  return {
+    DisplayItemSnapshotView: DisplayItemSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/images/input-event.png b/trace-viewer/trace_viewer/extras/cc/images/input-event.png
new file mode 100644
index 0000000..a2b7710
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/images/input-event.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/extras/cc/images/input-event.svg b/trace-viewer/trace_viewer/extras/cc/images/input-event.svg
new file mode 100644
index 0000000..00531ac
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/images/input-event.svg
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="New document 1">
+  <defs
+     id="defs4">
+    <filter
+       inkscape:collect="always"
+       id="filter3791">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="2.7246316"
+         id="feGaussianBlur3793" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8"
+     inkscape:cx="195.13782"
+     inkscape:cy="982.30556"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1215"
+     inkscape:window-height="860"
+     inkscape:window-x="2219"
+     inkscape:window-y="113"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3882"
+       style="opacity:0.5"
+       inkscape:export-filename="/tmp/input-event.png"
+       inkscape:export-xdpi="82.07"
+       inkscape:export-ydpi="82.07">
+      <path
+         transform="matrix(1.0152631,0,0,1.0152631,-0.71357503,0.46150497)"
+         sodipodi:type="arc"
+         style="opacity:0.50934604000000006;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter3791);enable-background:accumulate"
+         id="path3755"
+         sodipodi:cx="177.78685"
+         sodipodi:cy="100.79848"
+         sodipodi:rx="42.426407"
+         sodipodi:ry="42.426407"
+         d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z" />
+      <path
+         transform="translate(-2,-2)"
+         d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z"
+         sodipodi:ry="42.426407"
+         sodipodi:rx="42.426407"
+         sodipodi:cy="100.79848"
+         sodipodi:cx="177.78685"
+         id="path2985"
+         style="color:#000000;fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+         sodipodi:type="arc" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path3853"
+         d="m 175.28125,96.03125 0,8.46875 1,0 0,-8.46875 -1,0 z"
+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path3859"
+         d="m 171.53125,99.75 0,1 8.46875,0 0,-1 -8.46875,0 z"
+         style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" />
+    </g>
+    <path
+       transform="matrix(1.2923213,0,0,1.2923213,-53.970887,-31.465544)"
+       d="m 220.21326,100.79848 a 42.426407,42.426407 0 1 1 -84.85282,0 42.426407,42.426407 0 1 1 84.85282,0 z"
+       sodipodi:ry="42.426407"
+       sodipodi:rx="42.426407"
+       sodipodi:cy="100.79848"
+       sodipodi:cx="177.78685"
+       id="path3867"
+       style="color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:8;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       sodipodi:type="arc"
+       inkscape:export-filename="/tmp/input-event.png"
+       inkscape:export-xdpi="82.07"
+       inkscape:export-ydpi="82.07" />
+  </g>
+</svg>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_impl.html b/trace-viewer/trace_viewer/extras/cc/layer_impl.html
new file mode 100644
index 0000000..03277db
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_impl.html
@@ -0,0 +1,199 @@
+<!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/cc/constants.html">
+<link rel="import" href="/extras/cc/region.html">
+<link rel="import" href="/extras/cc/tile_coverage_rect.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var constants = tv.e.cc.constants;
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function LayerImplSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  LayerImplSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+
+      this.layerTreeImpl_ = undefined;
+      this.parentLayer = undefined;
+    },
+
+    initialize: function() {
+      // Defaults.
+      this.invalidation = new tv.e.cc.Region();
+      this.annotatedInvalidation = new tv.e.cc.Region();
+      this.unrecordedRegion = new tv.e.cc.Region();
+      this.pictures = [];
+
+      // Import & validate this.args
+      tv.e.cc.moveRequiredFieldsFromArgsToToplevel(
+          this, ['layerId', 'children',
+                 'layerQuad']);
+      tv.e.cc.moveOptionalFieldsFromArgsToToplevel(
+          this, ['maskLayer', 'replicaLayer',
+                 'idealContentsScale', 'geometryContentsScale',
+                 'layoutRects', 'usingGpuRasterization']);
+
+      // Leave gpu memory usage in both places.
+      this.gpuMemoryUsageInBytes = this.args.gpuMemoryUsage;
+
+      // Leave bounds in both places.
+      this.bounds = tv.b.Rect.fromXYWH(
+          0, 0,
+          this.args.bounds.width, this.args.bounds.height);
+
+      if (this.args.animationBounds) {
+        // AnimationBounds[2] and [5] are the Z-component of the box.
+        this.animationBoundsRect = tv.b.Rect.fromXYWH(
+            this.args.animationBounds[0], this.args.animationBounds[1],
+            this.args.animationBounds[3], this.args.animationBounds[4]);
+      }
+
+      for (var i = 0; i < this.children.length; i++)
+        this.children[i].parentLayer = this;
+      if (this.maskLayer)
+        this.maskLayer.parentLayer = this;
+      if (this.replicaLayer)
+        this.replicaLayer.parentLayer = this;
+      if (!this.geometryContentsScale)
+        this.geometryContentsScale = 1.0;
+
+      this.touchEventHandlerRegion = tv.e.cc.Region.fromArrayOrUndefined(
+          this.args.touchEventHandlerRegion);
+      this.wheelEventHandlerRegion = tv.e.cc.Region.fromArrayOrUndefined(
+          this.args.wheelEventHandlerRegion);
+      this.nonFastScrollableRegion = tv.e.cc.Region.fromArrayOrUndefined(
+          this.args.nonFastScrollableRegion);
+    },
+
+    get layerTreeImpl() {
+      if (this.layerTreeImpl_)
+        return this.layerTreeImpl_;
+      if (this.parentLayer)
+        return this.parentLayer.layerTreeImpl;
+      return undefined;
+    },
+    set layerTreeImpl(layerTreeImpl) {
+      this.layerTreeImpl_ = layerTreeImpl;
+    },
+
+    get activeLayer() {
+      if (this.layerTreeImpl.whichTree == constants.ACTIVE_TREE)
+        return this;
+      var activeTree = this.layerTreeImpl.layerTreeHostImpl.activeTree;
+      return activeTree.findLayerWithId(this.layerId);
+    },
+
+    get pendingLayer() {
+      if (this.layerTreeImpl.whichTree == constants.PENDING_TREE)
+        return this;
+      var pendingTree = this.layerTreeImpl.layerTreeHostImpl.pendingTree;
+      return pendingTree.findLayerWithId(this.layerId);
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function PictureLayerImplSnapshot() {
+    LayerImplSnapshot.apply(this, arguments);
+  }
+
+  PictureLayerImplSnapshot.prototype = {
+    __proto__: LayerImplSnapshot.prototype,
+
+    initialize: function() {
+      LayerImplSnapshot.prototype.initialize.call(this);
+
+      if (this.args.invalidation) {
+        this.invalidation = tv.e.cc.Region.fromArray(this.args.invalidation);
+        delete this.args.invalidation;
+      }
+      if (this.args.annotatedInvalidationRects) {
+        this.annotatedInvalidation = new tv.e.cc.Region();
+        for (var i = 0; i < this.args.annotatedInvalidationRects.length; ++i) {
+          var annotatedRect = this.args.annotatedInvalidationRects[i];
+          var rect = annotatedRect.geometryRect;
+          rect.reason = annotatedRect.reason;
+          this.annotatedInvalidation.addRect(rect);
+        }
+        delete this.args.annotatedInvalidationRects;
+      }
+      if (this.args.unrecordedRegion) {
+        this.unrecordedRegion = tv.e.cc.Region.fromArray(
+            this.args.unrecordedRegion);
+        delete this.args.unrecordedRegion;
+      }
+      if (this.args.pictures) {
+        this.pictures = this.args.pictures;
+
+        // The picture list comes in with an unknown ordering. We resort based
+        // on timestamp order so we will draw the base picture first and the
+        // various fixes on top of that.
+        this.pictures.sort(function(a, b) { return a.ts - b.ts; });
+      }
+
+      this.tileCoverageRects = [];
+      if (this.args.coverageTiles) {
+        for (var i = 0; i < this.args.coverageTiles.length; ++i) {
+          var rect = this.args.coverageTiles[i].geometryRect;
+          var tile = this.args.coverageTiles[i].tile;
+          this.tileCoverageRects.push(new tv.e.cc.TileCoverageRect(rect, tile));
+        }
+        delete this.args.coverageTiles;
+      }
+    }
+  };
+
+  ObjectSnapshot.register(
+      PictureLayerImplSnapshot,
+      {
+        typeName: 'cc::PictureLayerImpl'
+      });
+
+  ObjectSnapshot.register(
+      LayerImplSnapshot,
+      {
+        typeNames: [
+          'cc::LayerImpl',
+          'cc::DelegatedRendererLayerImpl',
+          'cc::HeadsUpDisplayLayerImpl',
+          'cc::IOSurfaceLayerImpl',
+          'cc::NinePatchLayerImpl',
+          'cc::PictureImageLayerImpl',
+          'cc::ScrollbarLayerImpl',
+          'cc::SolidColorLayerImpl',
+          'cc::SurfaceLayerImpl',
+          'cc::TextureLayerImpl',
+          'cc::TiledLayerImpl',
+          'cc::VideoLayerImpl',
+          'cc::PaintedScrollbarLayerImpl',
+          'ClankPatchLayer',
+          'TabBorderLayer',
+          'CounterLayer'
+        ]
+      });
+
+  return {
+    LayerImplSnapshot: LayerImplSnapshot,
+    PictureLayerImplSnapshot: PictureLayerImplSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_picker.css b/trace-viewer/trace_viewer/extras/cc/layer_picker.css
new file mode 100644
index 0000000..7f1303c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_picker.css
@@ -0,0 +1,43 @@
+/* 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.
+ */
+
+layer-picker {
+  -webkit-flex-direction: column;
+  display: -webkit-flex;
+}
+
+layer-picker > top-controls {
+  -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;
+  display: inline;
+  font-size: 14px;
+  padding-left: 2px;
+}
+
+layer-picker > top-controls input[type='checkbox'] {
+    vertical-align: -2px;
+}
+
+layer-picker > .x-list-view {
+  -webkit-flex: 1 1 auto;
+  font-family: monospace;
+  overflow: auto;
+}
+
+layer-picker > x-generic-object-view {
+  -webkit-flex: 0 0 auto;
+  height: 200px;
+  overflow: auto;
+}
+
+layer-picker > x-generic-object-view * {
+  -webkit-user-select: text !important;
+  cursor: text;
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_picker.html b/trace-viewer/trace_viewer/extras/cc/layer_picker.html
new file mode 100644
index 0000000..192554d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_picker.html
@@ -0,0 +1,322 @@
+<!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="/extras/cc/layer_picker.css">
+
+<link rel="import" href="/extras/cc/constants.html">
+<link rel="import" href="/extras/cc/layer_tree_host_impl.html">
+<link rel="import" href="/extras/cc/selection.html">
+<link rel="import" href="/extras/cc/util.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/trace_model/event.html">
+<link rel="import" href="/base/ui/drag_handle.html">
+<link rel="import" href="/base/ui/list_view.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var constants = tv.e.cc.constants;
+  var bytesToRoundedMegabytes = tv.e.cc.bytesToRoundedMegabytes;
+  var RENDER_PASS_QUADS =
+      Math.max(constants.ACTIVE_TREE, constants.PENDING_TREE) + 1;
+
+  /**
+   * @constructor
+   */
+  var LayerPicker = tv.b.ui.define('layer-picker');
+
+  LayerPicker.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.lthi_ = undefined;
+      this.controls_ = document.createElement('top-controls');
+      this.renderPassQuads_ = false;
+
+
+      this.itemList_ = new tv.b.ui.ListView();
+      this.appendChild(this.controls_);
+
+      this.appendChild(this.itemList_);
+
+      this.itemList_.addEventListener(
+          'selection-changed', this.onItemSelectionChanged_.bind(this));
+
+      this.controls_.appendChild(tv.b.ui.createSelector(
+          this, 'whichTree',
+          'layerPicker.whichTree', constants.ACTIVE_TREE,
+          [{label: 'Active tree', value: constants.ACTIVE_TREE},
+           {label: 'Pending tree', value: constants.PENDING_TREE},
+           {label: 'Render pass quads', value: RENDER_PASS_QUADS}]));
+
+      this.showPureTransformLayers_ = false;
+      var showPureTransformLayers = tv.b.ui.createCheckBox(
+          this, 'showPureTransformLayers',
+          'layerPicker.showPureTransformLayers', false,
+          'Transform layers');
+      showPureTransformLayers.classList.add('show-transform-layers');
+      showPureTransformLayers.title =
+          'When checked, pure transform layers are shown';
+      this.controls_.appendChild(showPureTransformLayers);
+    },
+
+    get lthiSnapshot() {
+      return this.lthiSnapshot_;
+    },
+
+    set lthiSnapshot(lthiSnapshot) {
+      this.lthiSnapshot_ = lthiSnapshot;
+      this.updateContents_();
+    },
+
+    get whichTree() {
+      return this.renderPassQuads_ ? constants.ACTIVE_TREE : this.whichTree_;
+    },
+
+    set whichTree(whichTree) {
+      this.whichTree_ = whichTree;
+      this.renderPassQuads_ = (whichTree == RENDER_PASS_QUADS);
+      this.updateContents_();
+      tv.b.dispatchSimpleEvent(this, 'selection-change', false);
+    },
+
+    get layerTreeImpl() {
+      if (this.lthiSnapshot === undefined)
+        return undefined;
+      return this.lthiSnapshot.getTree(this.whichTree);
+    },
+
+    get isRenderPassQuads() {
+      return this.renderPassQuads_;
+    },
+
+    get showPureTransformLayers() {
+      return this.showPureTransformLayers_;
+    },
+
+    set showPureTransformLayers(show) {
+      if (this.showPureTransformLayers_ === show)
+        return;
+      this.showPureTransformLayers_ = show;
+      this.updateContents_();
+    },
+
+    getRenderPassInfos_: function() {
+      if (!this.lthiSnapshot_)
+        return [];
+
+      var renderPassInfo = [];
+      if (!this.lthiSnapshot_.args.frame ||
+          !this.lthiSnapshot_.args.frame.renderPasses)
+        return renderPassInfo;
+
+      var renderPasses = this.lthiSnapshot_.args.frame.renderPasses;
+      for (var i = 0; i < renderPasses.length; ++i) {
+        var info = {renderPass: renderPasses[i],
+                     depth: 0,
+                     id: i,
+                     name: 'cc::RenderPass'};
+        renderPassInfo.push(info);
+      }
+      return renderPassInfo;
+    },
+
+    getLayerInfos_: function() {
+      if (!this.lthiSnapshot_)
+        return [];
+
+      var tree = this.lthiSnapshot_.getTree(this.whichTree_);
+      if (!tree)
+        return [];
+
+      var layerInfos = [];
+
+      var showPureTransformLayers = this.showPureTransformLayers_;
+
+      function isPureTransformLayer(layer) {
+        if (layer.args.compositingReasons &&
+            layer.args.compositingReasons.length != 1 &&
+            layer.args.compositingReasons[0] != 'No reasons given')
+          return false;
+
+        if (layer.args.drawsContent)
+          return false;
+
+        return true;
+      }
+      var visitedLayers = {};
+      function visitLayer(layer, depth, isMask, isReplica) {
+        if (visitedLayers[layer.layerId])
+          return;
+        visitedLayers[layer.layerId] = true;
+        var info = {layer: layer,
+          depth: depth};
+
+        if (layer.args.drawsContent)
+          info.name = layer.objectInstance.name;
+        else
+          info.name = 'cc::LayerImpl';
+
+        if (layer.usingGpuRasterization)
+          info.name += ' (G)';
+
+        info.isMaskLayer = isMask;
+        info.replicaLayer = isReplica;
+
+        if (showPureTransformLayers || !isPureTransformLayer(layer))
+          layerInfos.push(info);
+
+      };
+      tree.iterLayers(visitLayer);
+      return layerInfos;
+    },
+
+    updateContents_: function() {
+      if (this.renderPassQuads_)
+        this.updateRenderPassContents_();
+      else
+        this.updateLayerContents_();
+    },
+
+    updateRenderPassContents_: function() {
+      this.itemList_.clear();
+
+      var selectedRenderPassId;
+      if (this.selection_ && this.selection_.associatedRenderPassId)
+        selectedRenderPassId = this.selection_.associatedRenderPassId;
+
+      var renderPassInfos = this.getRenderPassInfos_();
+      renderPassInfos.forEach(function(renderPassInfo) {
+        var renderPass = renderPassInfo.renderPass;
+        var id = renderPassInfo.id;
+
+        var item = this.createElementWithDepth_(renderPassInfo.depth);
+        var labelEl = item.appendChild(tv.b.ui.createSpan());
+
+        labelEl.textContent = renderPassInfo.name + ' ' + id;
+        item.renderPass = renderPass;
+        item.renderPassId = id;
+        this.itemList_.appendChild(item);
+
+        if (id == selectedRenderPassId) {
+          renderPass.selectionState =
+              tv.c.trace_model.SelectionState.SELECTED;
+        }
+      }, this);
+    },
+
+    updateLayerContents_: function() {
+      this.changingItemSelection_ = true;
+      try {
+        this.itemList_.clear();
+
+        var selectedLayerId;
+        if (this.selection_ && this.selection_.associatedLayerId)
+          selectedLayerId = this.selection_.associatedLayerId;
+
+        var layerInfos = this.getLayerInfos_();
+        layerInfos.forEach(function(layerInfo) {
+          var layer = layerInfo.layer;
+          var id = layer.layerId;
+
+          var item = this.createElementWithDepth_(layerInfo.depth);
+          var labelEl = item.appendChild(tv.b.ui.createSpan());
+
+          labelEl.textContent = layerInfo.name + ' ' + id;
+
+          var notesEl = item.appendChild(tv.b.ui.createSpan());
+          if (layerInfo.isMaskLayer)
+            notesEl.textContent += '(mask)';
+          if (layerInfo.isReplicaLayer)
+            notesEl.textContent += '(replica)';
+
+          if (layer.gpuMemoryUsageInBytes !== undefined) {
+            var rounded = bytesToRoundedMegabytes(layer.gpuMemoryUsageInBytes);
+            if (rounded !== 0)
+              notesEl.textContent += ' (' + rounded + ' MB)';
+          }
+
+          item.layer = layer;
+          this.itemList_.appendChild(item);
+
+          if (layer.layerId == selectedLayerId) {
+            layer.selectionState = tv.c.trace_model.SelectionState.SELECTED;
+            item.selected = true;
+          }
+        }, this);
+      } finally {
+        this.changingItemSelection_ = false;
+      }
+    },
+
+    createElementWithDepth_: function(depth) {
+      var item = document.createElement('div');
+
+      var indentEl = item.appendChild(tv.b.ui.createSpan());
+      indentEl.style.whiteSpace = 'pre';
+      for (var i = 0; i < depth; i++)
+        indentEl.textContent = indentEl.textContent + ' ';
+      return item;
+    },
+
+    onItemSelectionChanged_: function(e) {
+      if (this.changingItemSelection_)
+        return;
+      if (this.renderPassQuads_)
+        this.onRenderPassSelected_(e);
+      else
+        this.onLayerSelected_(e);
+      tv.b.dispatchSimpleEvent(this, 'selection-change', false);
+    },
+
+    onRenderPassSelected_: function(e) {
+      var selectedRenderPass;
+      var selectedRenderPassId;
+      if (this.itemList_.selectedElement) {
+        selectedRenderPass = this.itemList_.selectedElement.renderPass;
+        selectedRenderPassId =
+            this.itemList_.selectedElement.renderPassId;
+      }
+
+      if (selectedRenderPass) {
+        this.selection_ = new tv.e.cc.RenderPassSelection(
+            selectedRenderPass, selectedRenderPassId);
+      } else {
+        this.selection_ = undefined;
+      }
+    },
+
+    onLayerSelected_: function(e) {
+      var selectedLayer;
+      if (this.itemList_.selectedElement)
+        selectedLayer = this.itemList_.selectedElement.layer;
+
+      if (selectedLayer)
+        this.selection_ = new tv.e.cc.LayerSelection(selectedLayer);
+      else
+        this.selection_ = undefined;
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      if (this.selection_ == selection)
+        return;
+      this.selection_ = selection;
+      this.updateContents_();
+    }
+  };
+
+  return {
+    LayerPicker: LayerPicker
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl.html
new file mode 100644
index 0000000..2bd95de
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl.html
@@ -0,0 +1,192 @@
+<!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/cc/constants.html">
+<link rel="import" href="/extras/cc/layer_tree_impl.html">
+<link rel="import" href="/extras/cc/util.html">
+<link rel="import" href="/base/bbox2.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the LayerTreeHostImpl model-level objects.
+ */
+tv.exportTo('tv.e.cc', function() {
+  var constants = tv.e.cc.constants;
+
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+  var ObjectInstance = tv.c.trace_model.ObjectInstance;
+
+  /**
+   * @constructor
+   */
+  function LayerTreeHostImplSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  LayerTreeHostImplSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+    },
+
+    initialize: function() {
+      tv.e.cc.moveRequiredFieldsFromArgsToToplevel(
+          this, ['deviceViewportSize',
+            'activeTree']);
+      tv.e.cc.moveOptionalFieldsFromArgsToToplevel(
+          this, ['pendingTree']);
+
+      // Move active_tiles into this.tiles. If that doesn't exist then, then as
+      // a backward compatability move tiles into this.tiles.
+      if (this.args.activeTiles !== undefined) {
+        this.activeTiles = this.args.activeTiles;
+        delete this.args.activeTiles;
+      } else if (this.args.tiles !== undefined) {
+        this.activeTiles = this.args.tiles;
+        delete this.args.tiles;
+      }
+
+      if (!this.activeTiles)
+        this.activeTiles = [];
+
+      this.activeTree.layerTreeHostImpl = this;
+      this.activeTree.whichTree = constants.ACTIVE_TREE;
+      if (this.pendingTree) {
+        this.pendingTree.layerTreeHostImpl = this;
+        this.pendingTree.whichTree = constants.PENDING_TREE;
+      }
+    },
+
+    /**
+     * Get all of tile scales and their associated names.
+     */
+    getContentsScaleNames: function() {
+      var scales = {};
+      for (var i = 0; i < this.activeTiles.length; ++i) {
+        var tile = this.activeTiles[i];
+        // Return scale -> scale name mappings.
+        // Example:
+        //  0.25 -> LOW_RESOLUTION
+        //  1.0 -> HIGH_RESOLUTION
+        //  0.75 -> NON_IDEAL_RESOLUTION
+        scales[tile.contentsScale] = tile.resolution;
+      }
+      return scales;
+    },
+
+    getTree: function(whichTree) {
+      if (whichTree == constants.ACTIVE_TREE)
+        return this.activeTree;
+      if (whichTree == constants.PENDING_TREE)
+        return this.pendingTree;
+      throw new Exception('Unknown tree type + ' + whichTree);
+    },
+
+    get tilesHaveGpuMemoryUsageInfo() {
+      if (this.tilesHaveGpuMemoryUsageInfo_ !== undefined)
+        return this.tilesHaveGpuMemoryUsageInfo_;
+
+      for (var i = 0; i < this.activeTiles.length; i++) {
+        if (this.activeTiles[i].gpuMemoryUsageInBytes === undefined)
+          continue;
+        this.tilesHaveGpuMemoryUsageInfo_ = true;
+        return true;
+      }
+      this.tilesHaveGpuMemoryUsageInfo_ = false;
+      return false;
+    },
+
+    get gpuMemoryUsageInBytes() {
+      if (!this.tilesHaveGpuMemoryUsageInfo)
+        return;
+
+      var usage = 0;
+      for (var i = 0; i < this.activeTiles.length; i++) {
+        var u = this.activeTiles[i].gpuMemoryUsageInBytes;
+        if (u !== undefined)
+          usage += u;
+      }
+      return usage;
+    },
+
+    get userFriendlyName() {
+      var frameNumber;
+      if (!this.activeTree) {
+        frameNumber = this.objectInstance.snapshots.indexOf(this);
+      } else {
+        if (this.activeTree.sourceFrameNumber === undefined)
+          frameNumber = this.objectInstance.snapshots.indexOf(this);
+        else
+          frameNumber = this.activeTree.sourceFrameNumber;
+      }
+      return 'cc::LayerTreeHostImpl frame ' + frameNumber;
+    }
+  };
+
+  ObjectSnapshot.register(
+      LayerTreeHostImplSnapshot,
+      {typeName: 'cc::LayerTreeHostImpl'});
+
+  /**
+   * @constructor
+   */
+  function LayerTreeHostImplInstance() {
+    ObjectInstance.apply(this, arguments);
+
+    this.allLayersBBox_ = undefined;
+  }
+
+  LayerTreeHostImplInstance.prototype = {
+    __proto__: ObjectInstance.prototype,
+
+    get allContentsScales() {
+      if (this.allContentsScales_)
+        return this.allContentsScales_;
+
+      var scales = {};
+      for (var tileID in this.allTileHistories_) {
+        var tileHistory = this.allTileHistories_[tileID];
+        scales[tileHistory.contentsScale] = true;
+      }
+      this.allContentsScales_ = tv.b.dictionaryKeys(scales);
+      return this.allContentsScales_;
+    },
+
+    get allLayersBBox() {
+      if (this.allLayersBBox_)
+        return this.allLayersBBox_;
+      var bbox = new tv.b.BBox2();
+      function handleTree(tree) {
+        tree.renderSurfaceLayerList.forEach(function(layer) {
+          bbox.addQuad(layer.layerQuad);
+        });
+      }
+      this.snapshots.forEach(function(lthi) {
+        handleTree(lthi.activeTree);
+        if (lthi.pendingTree)
+          handleTree(lthi.pendingTree);
+      });
+      this.allLayersBBox_ = bbox;
+      return this.allLayersBBox_;
+    }
+  };
+
+  ObjectInstance.register(
+      LayerTreeHostImplInstance,
+      {typeName: 'cc::LayerTreeHostImpl'});
+
+  return {
+    LayerTreeHostImplSnapshot: LayerTreeHostImplSnapshot,
+    LayerTreeHostImplInstance: LayerTreeHostImplInstance
+
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_test.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_test.html
new file mode 100644
index 0000000..f3f1574
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_test.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="/extras/cc/layer_tree_host_impl.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('basic', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+    var snapshot = instance.snapshots[0];
+
+    assert.instanceOf(snapshot, tv.e.cc.LayerTreeHostImplSnapshot);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_test_data.js b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_test_data.js
new file mode 100644
index 0000000..7d5036d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_test_data.js
@@ -0,0 +1,330 @@
+// 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.
+
+'use strict';
+
+// A single LTHI sort of manually created from a Google search for cats.
+var g_catLTHIEvents = [
+  {
+    'name': 'cc::Picture',
+    'args': {
+      'snapshot': {
+        'params': {
+          'opaque_rect': [
+            -15,
+            -15,
+            0,
+            0
+          ],
+          'layer_rect': [
+            -15,
+            -15,
+            1260,
+            1697
+          ]
+        },
+        'skp64': 'c2tpYXBpY3QWAAAAOAQAABQDAAADAAAAAWRhZXKoCQAACAAAHgMAAAAIAAAeAwAAAAwAACMAAHBBAABwQRwAAAMAAHDBAABwwQAghUQAQEFEAQAAAKAJAAAYAAAVAQAAAAAAAAAAAAAAAECDRACAPUQIAAAeAwAAABwAAAMAAAAAAAAAAABAg0QAAMhBAQAAAIwAAAAYAAAVAgAAAAAAAAAAAAAAAECDRAAAyEEEAAAcCAAAHgMAAAAcAAADAAAAAAAAyEEAQINEAADQQQEAAADYAAAADAAAIwAAAAAAAMhBGAAAFQIAAAAAAAAAAAAAAABAg0QAAMhBBAAAHBgAABUDAAAAAAAAAAAAyEEAQINEAADQQQgAAB4DAAAACAAAHgMAAAAMAAAjAABwwQAAcMEUAAAGAAAAAAAAAAAAAIhBAACIQQQAABwEAAAcCAAAHgMAAAAcAAADAACAQAAAAEAAAHxCAADAQQEAAABQAQAABAAAHAgAAB4DAAAAHAAAAwAAgEAAAABAAAB8QgAAwEEBAAAAeAEAAAQAABwIAAAeAwAAABwAAAMAAIBAAAAAQAAAfEIAAMBBAQAAAOABAABAAAAUBAAAAAwAAAA1AEgARgBSAFUARwAGAAAAAKDcPwCG4kEAAJBBAAAgQQDSlkEAONVBAEsHQgCxKUIAm0BCBAAAHAgAAB4DAAAAHAAAAwAAgEAAAABAAAB8QgAAwEEBAAAACAIAAAQAABwkAAAUBQAAAAIAAAADAAAAAQAAAAAAYD0AkM1BAACQQQDwhUIIAAAeAwAAAAgAAB4DAAAADAAAIwAAcMEAAHDBFAAABgAAAAABAAAAAACyQgAAsEEEAAAcBAAAHCQAABQFAAAAAgAAAAMAAAABAAAAAABgPQCQzUEAAJBBAJCxQlgAABQGAAAAFAAAADAAUgBRAEwAVwBSAFUATABRAEoACgAAAAAQE0AA3sVBAACQQQCQvUIAkNVCAJDlQgCQ9UIAkPtCAMgBQwDICUMAyA5DAMgRQwDIGUMkAAAUBQAAAAIAAAADAAAAAQAAAAAAYD0AkM1BAACQQQDII0MIAAAeAwAAAAgAAB4DAAAADAAAIwAAcMEAAHDBFAAABgAAAAACAAAAAAA5QwAAiEEEAAAcBAAAHAgAAB4DAAAAHAAAAwAALEMAAABAAIC/QwAAwEEBAAAAZAMAAAQAABwIAAAeAwAAABwAAAMAACxDAAAAQACAv0MAAMBBAQAAAIwDAAAEAAAcCAAAHgMAAAAcAAADAAAsQwAAAEAAgL9DAADAQQEAAAB0BAAAwAAAFAcAAAA2AAAAJgBEAFMAVwBYAFUASAADADAAUgBRAEwAVwBSAFUATABRAEoAAwA2AFEARABTAFYASwBSAFcAAAAbAAAAAKDcPwCG4kEAAJBBAAAyQ8CvO0MAa0NDwDpMQ0B3UUOAJ1pDAOJfQ8CuZ0PAHGxDwCt4Q6BigEPAuoRDwMCGQwBfiUPAq41DAImQQwCPkkMg55ZDgESbQ4B7nUOgQKFDwJilQ2B2qUNA3q1DwG+xQ+DHtUOgFLpDBAAAHAgAAB4DAAAAHAAAAwAALEMAAABAAIC/QwAAwEEBAAAAnAQAAAQAABwkAAAUBQAAAAIAAAADAAAAAQAAAAAAYD0AkM1BAACQQQCYwUMIAAAeAwAAAAgAAB4DAAAADAAAIwAAcMEAAHDBFAAABgAAAAADAAAAAADrQwAAiEEEAAAcBAAAHAgAAB4DAAAAHAAAAwCA5EMAAABAAAD6QwAAwEEBAAAAIAUAAAQAABwIAAAeAwAAABwAAAMAgORDAAAAQAAA+kMAAMBBAQAAAEgFAAAEAAAcCAAAHgMAAAAcAAADAIDkQwAAAEAAAPpDAADAQQEAAACkBQAANAAAFAcAAAAIAAAANgBEAFkASAAEAAAAAKDcPwCG4kEAAJBBAIDnQyBF60PAIu9DQMLyQwQAABwIAAAeAwAAABwAAAMAgORDAAAAQAAA+kMAAMBBAQAAAMwFAAAEAAAcJAAAFAUAAAACAAAAAwAAAAEAAAAAAGA9AJDNQQAAkEEAwvtDCAAAHgMAAAAIAAAeAwAAAAwAACMAAHDBAABwwRQAAAYAAAAABAAAAABAA0QAAIhBBAAAHAQAABwIAAAeAwAAABwAAAMAAABEAAAAQAAAC0QAAMBBAQAAAFAGAAAEAAAcCAAAHgMAAAAcAAADAAAARAAAAEAAAAtEAADAQQEAAAB4BgAABAAAHAgAAB4DAAAAHAAAAwAAAEQAAABAAAALRAAAwEEBAAAA1AYAADQAABQEAAAACAAAAC8AUgBEAEcABAAAAACg3D8AhuJBAACQQQCAAUTAXQNEIIQFRPByB0QEAAAcCAAAHgMAAAAcAAADAAAARAAAAEAAAAtEAADAQQEAAAD8BgAABAAAHDAAABQGAAAABgAAAEEAQgBBAAAAAwAAAACAmD4A3rVBAACAQQCADEQAQA5EAEAQRBgAABUIAAAAAECARAAAAEAAIINEAADAQQwAAA4JAAAAAQAAACQAABQKAAAAAgAAACIAAAABAAAAAICYPgDetUEAAIBBADCBRBgAABUDAAAAAAAAAAAA+kMAQINEAID6QxgAABULAAAAAICARAAA0EEAQINEAAD6QxgAABUMAAAAAICARAAA0EEAoIBEAAD6QxgAABUNAAAAAAAAAAAA/kMAQINEAIA9RBgAABUIAAAAAMBNRAAAAEAAQHhEAACoQQwAAA4JAAAAAgAAACQAABQOAAAAAgAAAJ0DAAABAAAAAIAmwABQ3kEAAIBBAEB4RCQAABQOAAAAAgAAAJ8DAAABAAAAAIAmwABQ3kEAAIBBAEB8RAgAAB4DAAAAHAAAAwDATUQAAEBAAEB4RAAAsEEBAAAAfAgAABAAAB8AAAAADwAAAB8AAAAEAAAcBAAAHAgAAB4DAAAAHAAAAwAAAAAAgPpDAECDRAAA+0MBAAAA1AgAAAwAACMAAAAAAID6QwwAACMAAACAAACAwBgAABUQAAAAAAAAAAAAAAAAQINEAACgQAQAABwIAAAeAwAAABwAAAMAAAAAAAD7QwBAg0QAgP1DAQAAACAJAAAMAAAjAAAAAAAA+0MYAAAVEAAAAAAAAAAAAAAAAECDRAAAoEAEAAAcCAAAHgMAAAAcAAADAAAAAACA/UMAQINEAAD+QwEAAABsCQAADAAAIwAAAAAAgP1DGAAAFRAAAAAAAAAAAAAAAABAg0QAAKBABAAAHBgAABURAAAAAAAAAACA+kMAQINEAAD7QxgAABUSAAAAAAAAAACA/UMAQINEAAD+QwQAABwEAAAcdGNhZiMAAAACAAAADVNrU3JjWGZlcm1vZGUQU2tMaW5lYXJHcmFkaWVudGNmcHQCAAAAAAENTHVjaWRhIEdyYW5kZQQNTHVjaWRhIEdyYW5kZQYMTHVjaWRhR3JhbmRl/v8AAAABCUhlbHZldGljYQQJSGVsdmV0aWNhBglIZWx2ZXRpY2H+/wAAeWFyYcQIAABwbXRiBQAAAD8AAAAWAAAAAAAAAKkAAACJUE5HDQoaCgAAAA1JSERSAAAAPwAAABYIBgAAAHf8RCEAAABwSURBVFiF7ZSxAcQgDMRk8KQMRQdD0lElxf8MueKsCSSMHXPOB1MSYIyh9vicvfcv/t6rdpGQAOcctYeEphZQkgCteb5Bxffe1R4SEiAi1B4SKt7123teuj/Wk6+dd42vnXedfMVXvCEVv9ZSe0h4ASOjDeti06sSAAAAAElFTkSuQmCCAAAAAAAAAAAAAAAMAAAADQAAAAAAAAAPAQAAiVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAYAAACdKY9CAAAA1klEQVQokZWSMWrDQBBF38hrxW5SGJdSkyqtyRXSB3QRnUUXEWzvK+QKwSCxxbIE5CpaZMmFsuBCK/DAr+a9zxQjwBY4AK/AjuX5A67ArwIOVVV9FEXxlmXZcYlu29bVdf1TluW3AO9N03zG4Ecpz/OzAKdpmr7W4DAiohUgwzAwjuMqnCQJgCiAJwQUgPce7/2qkKbpLAY7NMTawz5OxWSgN8a40BKLMcYBfQJ0WuuLtdYppViKtdZprS9AJ8AeODK/xkvkkp75NZwAAmz+IxFhAm7A7Q619kxK1JGuJQAAAABJRU5ErkJgggAAAAAAAAAAANcAAAAWAAAAAAAAAMcAAACJUE5HDQoaCgAAAA1JSERSAAAA1wAAABYIBgAAAFPJCaQAAACOSURBVHic7duxDYQwEETRObSVupJrgpQiiRwS0AIrS9Z7FUzytU78O8/zH+BzlSRjjNU7YCvXdb1xzTlXb4HtVJLc9716B2znWD0AdlVJchwag69VklTV6h2wHZcLmqgKmrhc0ERc0ERc0ERc0ERV0ERc0MSzEJqoCpq4XNBEVdDE5YImqoImlbz//YFvPbqBDWEab+OuAAAAAElFTkSuQmCCAAAAAAAAAAAALwAAABYAAAAAAAAApgAAAIlQTkcNChoKAAAADUlIRFIAAAAvAAAAFggGAAAAUFLFyQAAAG1JREFUWIXt17ENwCAUA9EL8qRMkiVoGZKKMgVZIKSwLPEmOKzfcLXWbkIJoNbq7vis977i55zuli0CGGO4O7YUd8AfAigl8w0CkOTu2BK9fGb1K3r5E+8SHZ9Z/YqOP2fjcpZ3Ocu7CNZ/MNEDHg4NYflXb+0AAAAASUVORK5CYIIAAAAAAAAAAAAAMAAAABYAAAAAAAAArgAAAIlQTkcNChoKAAAADUlIRFIAAAAwAAAAFggGAAAAhvcfrAAAAHVJREFUWIXtk7ERwCAMxGTwpAxFB0PSUSVFMoFT/JmLJpDOfuu9XyTGAVprao8Qc84nYO+tdgnjAGsttUeYohb4igOUkrfjjIBaq9ojjAOYmdojzBkBmV8o73pf0l/gjA1kDjhjA5kv8Aeo+QPUOMAYQ+0R5gZf3A3r3/HMCQAAAABJRU5ErkJgggAAAAAAAAAAAAAgdG5wEgAAAAAAQEEAAIA/AAAAAAAAAAAAAIBA/////wIwAwAAAAAAAAAAAAAAAAABAAAABAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQAAAAP8CMAMAAAAAAAAAAAACAAAATAAAAAAAAAAAAAAAAgAAAOXl5f/R0dH/EAAAAAAAAAAK1yM9AAAAAArXI70AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAyEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQQAAgD8AAAAAAAAAAAAAgECOjo7/ADADAAAAAAAAAGBBAACAPwAAAAAAAAAAAACAQAAAAP8BMIMCAwAAAAEAAAAAAIBBAACAPwAAAAAAAAAAAACAQAAAAP8BMIMCAwAAAAIAAAAAAGBBAACAPwAAAAAAAAAAAACAQAAAAP8BMIMCAwAAAAIAAAAAAGBBAACAPwAAAAAAAAAAAACAQH9/f/8BMIMCAwAAAAEAAAAAAEBBAACAPwAAAAAAAAAAAACAQPj4+P8AMAMAAAAAAAAAQEEAAIA/AAAAAAAAAAAAAIBAAAAAfwAwAwAAAAAAAABgQQAAgD8AAAAAAAAAAAAAgEAAAADMATCDAgMAAAACAAAAAABAQQAAgD8AAAAAAAAAAAAAgEDs7Oz/ADADAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQAAAAP8AMAMAAAAAAAAAQEEAAIA/AAAAAAAAAAAAAIBA/////wAwAwAAAAAAAACAQQAAgD8AAAAAAAAAAAAAgEAAAAD/ATCDAgMAAAABAAAAAABAQQAAgD8AAAAAAAAAAAAAgEAAAAA/ADAAAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQAAAAP8CMAMAAAAAAAAAAAACAAAATAAAAAAAAAAAAAAAAgAAAOXl5f/R0dH/EAAAAAAAAADNzEw+AAAAAM3MTL4AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAoEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQQAAgD8AAAAAAAAAAAAAgED/////ADACAAAAAAAAAEBBAACAPwAAAAAAAAAAAACAQI6Ojv8AMAIAAAAAACBodHACAAAAAgAAAAABAAABAAACAAAAAAoAAAAIAAAAAAAAAAUBAQEABQEBAQAAQIBEAAAAQAAgg0QAAABAACCDRAAAwEEAQIBEAADAQQBggEQAAEBAAACDRAAAQEAAAINEAAC4QQBggEQAALhBAECARAAAAEAAIINEAADAQQAAAAEAAAEAAAIAAAAACgAAAAgAAAAAAAAABQEBAQAFAQEBAADATUQAAABAAEB4RAAAAEAAQHhEAACoQQDATUQAAKhBAABORAAAQEAAAHhEAABAQAAAeEQAAKBBAABORAAAoEEAwE1EAAAAQABAeEQAAKhBAAAgZm9l' // @suppress longLineCheck
+      }
+    },
+    'pid': 1,
+    'ts': 100,
+    'cat': 'disabled-by-default-cc.debug',
+    'tid': 1,
+    'ph': 'O',
+    'id': 'PICTURE_1'
+  },
+  {
+    'name': 'AnalyzeTask',
+    'args': {
+      'data': {
+        'source_frame_number': 107,
+        'tile_id': {
+          'id_ref': 'TILE_1'
+        },
+        'resolution': 'HIGH_RESOLUTION',
+        'is_tile_in_pending_tree_now_bin': true
+      }
+    },
+    'pid': 1,
+    'ts': 101,
+    'cat': 'cc',
+    'tid': 1,
+    'ph': 'B'
+  },
+  {
+    'name': 'AnalyzeTask',
+    'args': {},
+    'pid': 1,
+    'ts': 105,
+    'cat': 'cc',
+    'tid': 1,
+    'ph': 'E'
+  },
+  {
+    'name': 'RasterTask',
+    'args': {
+      'data': {
+        'source_frame_number': 107,
+        'tile_id': {
+          'id_ref': 'TILE_1'
+        },
+        'resolution': 'HIGH_RESOLUTION',
+        'is_tile_in_pending_tree_now_bin': true
+      }
+    },
+    'pid': 1,
+    'ts': 110,
+    'cat': 'cc',
+    'tid': 1,
+    'ph': 'B'
+  },
+  {
+    'name': 'RasterTask',
+    'args': {},
+    'pid': 1,
+    'ts': 150,
+    'cat': 'cc',
+    'tid': 1,
+    'ph': 'E'
+  },
+  {
+    'name': 'RasterTask',
+    'args': {
+      'data': {
+        'source_frame_number': 107,
+        'tile_id': {
+          'id_ref': 'TILE_2'
+        },
+        'resolution': 'HIGH_RESOLUTION',
+        'is_tile_in_pending_tree_now_bin': true
+      }
+    },
+    'pid': 1,
+    'ts': 170,
+    'cat': 'cc',
+    'tid': 1,
+    'ph': 'B'
+  },
+  {
+    'name': 'RasterTask',
+    'args': {},
+    'pid': 1,
+    'ts': 180,
+    'cat': 'cc',
+    'tid': 1,
+    'ph': 'E'
+  },
+  {
+    'name': 'cc::LayerTreeHostImpl',
+    'args': {
+      'snapshot': {
+        'device_viewport_size': {
+          'width': 2460,
+          'height': 1606
+        },
+        'active_tree': {
+          'source_frame_number': 7,
+          'root_layer': {
+            'tilings': [
+              {
+                'content_scale': 2,
+                'content_bounds': {
+                  'width': 2460,
+                  'height': 3334
+                },
+                'num_tiles': 1
+              },
+              {
+                'content_scale': 0.25,
+                'content_bounds': {
+                  'width': 308,
+                  'height': 417
+                },
+                'num_tiles': 1
+              }
+            ],
+            'coverage_tiles': [
+              {
+                'geometry_rect': [0, 0, 256, 256],
+                'tile': {
+                  'id_ref': 'TILE_1'
+                }
+              },
+              {
+                'geometry_rect': [256, 0, 256, 256]
+              },
+              {
+                'geometry_rect': [512, 0, 256, 256]
+              },
+              {
+                'geometry_rect': [0, 256, 256, 512]
+              },
+              {
+                'geometry_rect': [256, 256, 512, 512]
+              }
+            ],
+            'gpu_memory_usage': 22069248,
+            'draws_content': 1,
+            'layer_id': 6,
+            'invalidation': [],
+            'bounds': {
+              'width': 1230,
+              'height': 1667
+            },
+            'children': [
+              {
+                'tilings': [
+                  {
+                    'content_scale': 2,
+                    'content_bounds': {
+                      'width': 200,
+                      'height': 100
+                    },
+                    'num_tiles': 1
+                  }
+                ],
+                'gpu_memory_usage': 128000,
+                'draws_content': 1,
+                'layer_id': 7,
+                'invalidation': [],
+                'bounds': {
+                  'width': 100,
+                  'height': 50
+                },
+                'children': [
+                ],
+                'ideal_contents_scale': 2,
+                'layer_quad': [
+                  0,
+                  0,
+                  200,
+                  0,
+                  200,
+                  100,
+                  0,
+                  100
+                ],
+                'pictures': [
+                ],
+                'id': 'cc::PictureLayerImpl/LAYER_2'
+              }
+            ],
+            'ideal_contents_scale': 2,
+            'layer_quad': [
+              0,
+              -1022,
+              2460,
+              -1022,
+              2460,
+              2312,
+              0,
+              2312
+            ],
+            'pictures': [
+              {
+                'id_ref': 'PICTURE_1'
+              }
+            ],
+            'id': 'cc::PictureLayerImpl/LAYER_1'
+          },
+          'render_surface_layer_list': [
+            {'id_ref': 'LAYER_1'},
+            {'id_ref': 'LAYER_2'}
+          ],
+          'id': 'cc::LayerTreeImpl/0x7d246ee0'
+        },
+        'tiles': [
+          {
+            'active_priority': {
+              'time_to_visible_in_seconds': 0,
+              'resolution': 'HIGH_RESOLUTION',
+              'distance_to_visible_in_pixels': 0
+            },
+            'pending_priority': {
+              'time_to_visible_in_seconds': 3.4028234663852886e+38,
+              'resolution': 'NON_IDEAL_RESOLUTION',
+              'distance_to_visible_in_pixels': 3.4028234663852886e+38
+            },
+            'managed_state': {
+              'resolution': 'HIGH_RESOLUTION',
+              'is_solid_color': false,
+              'is_using_gpu_memory': true,
+              'has_resource': true,
+              'scheduled_priority': 10,
+              'distance_to_visible': 0,
+              'gpu_memory_usage': 1024000
+            },
+            'layer_id': '6',
+            'picture_pile': {
+              'id_ref': 'PICTURE_1'
+            },
+            'contents_scale': 2,
+            'content_rect': [0, 0, 1024, 1024],
+            'id': 'cc::Tile/TILE_1'
+          },
+          {
+            'active_priority': {
+              'time_to_visible_in_seconds': 0,
+              'resolution': 'HIGH_RESOLUTION',
+              'distance_to_visible_in_pixels': 0
+            },
+            'pending_priority': {
+              'time_to_visible_in_seconds': 3.4028234663852886e+38,
+              'resolution': 'NON_IDEAL_RESOLUTION',
+              'distance_to_visible_in_pixels': 3.4028234663852886e+38
+            },
+            'managed_state': {
+              'resolution': 'HIGH_RESOLUTION',
+              'is_solid_color': false,
+              'is_using_gpu_memory': true,
+              'has_resource': true,
+              'scheduled_priority': 12,
+              'distance_to_visible': 0,
+              'gpu_memory_usage': 1024000
+            },
+            'layer_id': '6',
+            'picture_pile': {
+              'id_ref': 'PICTURE_1'
+            },
+            'contents_scale': 2,
+            'content_rect': [0, 1024, 1024, 1024],
+            'id': 'cc::Tile/TILE_2'
+          }
+        ]
+      }
+    },
+    'pid': 1,
+    'ts': 500,
+    'cat': 'disabled-by-default-cc.debug',
+    'tid': 28163,
+    'ph': 'O',
+    'id': 'LTHI_1'
+  },
+  {
+    'name': 'cc::DisplayItemList',
+    'args': {
+      'snapshot': {
+        'params': {
+          'layer_rect': [
+            -15,
+            -15,
+            1260,
+            1697
+          ],
+          'items': [
+            'BeginClipDisplayItem',
+            'EndClipDisplayItem'
+          ]
+        },
+        'skp64': '[base 64 encoded skia picture]'
+      }
+    },
+    'pid': 1,
+    'ts': 300,
+    'cat': 'disabled-by-default-cc.debug',
+    'tid': 1,
+    'ph': 'O',
+    'id': 'PICTURE_3'
+  }
+];
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view.css b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view.css
new file mode 100644
index 0000000..0bc09d8
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view.css
@@ -0,0 +1,18 @@
+/* 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.
+ */
+
+.lthi-s-view {
+  -webkit-flex: 1 1 auto !important;
+  -webkit-flex-direction: row;
+  display: -webkit-flex;
+}
+
+.lthi-s-view > layer-picker {
+  -webkit-flex: 1 1 auto;
+}
+
+.lthi-s-view > x-drag-handle {
+  -webkit-flex: 0 0 auto;
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view.html
new file mode 100644
index 0000000..50693a2
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_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="stylesheet" href="/extras/cc/layer_tree_host_impl_view.css">
+
+<link rel="import" href="/extras/cc/layer_tree_host_impl.html">
+<link rel="import" href="/extras/cc/layer_picker.html">
+<link rel="import" href="/extras/cc/layer_view.html">
+<link rel="import" href="/extras/cc/tile.html">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/base/ui/drag_handle.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /*
+   * Displays a LayerTreeHostImpl snapshot in a human readable form.
+   * @constructor
+   */
+  var LayerTreeHostImplSnapshotView = tv.b.ui.define(
+      'layer-tree-host-impl-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  LayerTreeHostImplSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('lthi-s-view');
+
+      this.selection_ = undefined;
+
+      this.layerPicker_ = new tv.e.cc.LayerPicker();
+      this.layerPicker_.addEventListener(
+          'selection-change',
+          this.onLayerPickerSelectionChanged_.bind(this));
+
+      this.layerView_ = new tv.e.cc.LayerView();
+      this.layerView_.addEventListener(
+          'selection-change',
+          this.onLayerViewSelectionChanged_.bind(this));
+      this.dragHandle_ = new tv.b.ui.DragHandle();
+      this.dragHandle_.horizontal = false;
+      this.dragHandle_.target = this.layerView_;
+
+      this.appendChild(this.layerPicker_);
+      this.appendChild(this.dragHandle_);
+      this.appendChild(this.layerView_);
+
+      // Make sure we have the current values from layerView_ and layerPicker_,
+      // since those might have been created before we added the listener.
+      this.onLayerViewSelectionChanged_();
+      this.onLayerPickerSelectionChanged_();
+
+    },
+
+    get objectSnapshot() {
+      return this.objectSnapshot_;
+    },
+
+    set objectSnapshot(objectSnapshot) {
+      this.objectSnapshot_ = objectSnapshot;
+
+      var lthi = this.objectSnapshot;
+      var layerTreeImpl;
+      if (lthi)
+        layerTreeImpl = lthi.getTree(this.layerPicker_.whichTree);
+
+      this.layerPicker_.lthiSnapshot = lthi;
+      this.layerView_.layerTreeImpl = layerTreeImpl;
+      this.layerView_.regenerateContent();
+
+      if (!this.selection_)
+        return;
+      this.selection = this.selection_.findEquivalent(lthi);
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      if (this.selection_ == selection)
+        return;
+      this.selection_ = selection;
+      this.layerPicker_.selection = selection;
+      this.layerView_.selection = selection;
+      tv.b.dispatchSimpleEvent(this, 'cc-selection-change');
+    },
+
+    onLayerPickerSelectionChanged_: function() {
+      this.selection_ = this.layerPicker_.selection;
+      this.layerView_.selection = this.selection;
+      this.layerView_.layerTreeImpl = this.layerPicker_.layerTreeImpl;
+      this.layerView_.isRenderPassQuads = this.layerPicker_.isRenderPassQuads;
+      this.layerView_.regenerateContent();
+      tv.b.dispatchSimpleEvent(this, 'cc-selection-change');
+    },
+
+    onLayerViewSelectionChanged_: function() {
+      this.selection_ = this.layerView_.selection;
+      this.layerPicker_.selection = this.selection;
+      tv.b.dispatchSimpleEvent(this, 'cc-selection-change');
+    },
+
+    get extraHighlightsByLayerId() {
+      return this.layerView_.extraHighlightsByLayerId;
+    },
+
+    set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+      this.layerView_.extraHighlightsByLayerId = extraHighlightsByLayerId;
+    }
+  };
+
+  tv.c.analysis.ObjectSnapshotView.register(
+      LayerTreeHostImplSnapshotView, {typeName: 'cc::LayerTreeHostImpl'});
+
+  return {
+    LayerTreeHostImplSnapshotView: LayerTreeHostImplSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view_test.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_view_test.html
new file mode 100644
index 0000000..3784410
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_host_impl_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="/extras/cc/layer_tree_host_impl.html">
+<link rel="import" href="/extras/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/extras/cc/raster_task.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+    var snapshot = instance.snapshots[0];
+
+    var view = new tv.e.cc.LayerTreeHostImplSnapshotView();
+    view.style.width = '900px';
+    view.style.height = '400px';
+    view.objectSnapshot = snapshot;
+
+    this.addHTMLOutput(view);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_impl.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_impl.html
new file mode 100644
index 0000000..f53f5f5
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_impl.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="/extras/audits/chrome_model_helper.html">
+<link rel="import" href="/extras/cc/constants.html">
+<link rel="import" href="/extras/cc/layer_impl.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+
+  var constants = tv.e.cc.constants;
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function LayerTreeImplSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  LayerTreeImplSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+      this.layerTreeHostImpl = undefined;
+      this.whichTree = undefined;
+      this.sourceFrameNumber = undefined;
+    },
+
+    initialize: function() {
+      tv.e.cc.moveRequiredFieldsFromArgsToToplevel(
+          this, ['rootLayer',
+            'renderSurfaceLayerList']);
+      if (this.args.sourceFrameNumber)
+        this.sourceFrameNumber = this.args.sourceFrameNumber;
+      this.rootLayer.layerTreeImpl = this;
+
+      if (this.args.swapPromiseTraceIds &&
+          this.args.swapPromiseTraceIds.length) {
+        this.tracedInputLatencies = [];
+
+        var ownProcess = this.objectInstance.parent;
+        var traceModel = ownProcess.model;
+        if (tv.e.audits.ChromeModelHelper.supportsModel(traceModel))
+          this._initializeTracedInputLatencies(traceModel);
+      }
+    },
+
+    _initializeTracedInputLatencies: function(traceModel) {
+      var modelHelper = new tv.e.audits.ChromeModelHelper(traceModel);
+      if (!modelHelper.browser)
+        return;
+
+      var latencyEvents = modelHelper.browser.getLatencyEventsInRange(
+          traceModel.range);
+
+      // Convert all ids to InputLatency Async objects.
+      latencyEvents.forEach(function(event) {
+        for (var i = 0; i < this.args.swapPromiseTraceIds.length; i++) {
+          if (!event.args.data || !event.args.data.trace_id)
+            continue;
+          if (parseInt(event.args.data.trace_id) ===
+              this.args.swapPromiseTraceIds[i])
+            this.tracedInputLatencies.push(event);
+        }
+      }, this);
+    },
+
+    get hasSourceFrameBeenDrawnBefore() {
+      if (this.whichTree == tv.e.cc.constants.PENDING_TREE)
+        return false;
+
+      // Old chrome's don't produce sourceFrameNumber.
+      if (this.sourceFrameNumber === undefined)
+        return;
+
+      var thisLTHI = this.layerTreeHostImpl;
+      var thisLTHIIndex = thisLTHI.objectInstance.snapshots.indexOf(
+        thisLTHI);
+      var prevLTHIIndex = thisLTHIIndex - 1;
+      if (prevLTHIIndex < 0 ||
+          prevLTHIIndex >= thisLTHI.objectInstance.snapshots.length)
+        return false;
+      var prevLTHI = thisLTHI.objectInstance.snapshots[prevLTHIIndex];
+      if (!prevLTHI.activeTree)
+        return false;
+
+      // Old chrome's don't produce sourceFrameNumber.
+      if (prevLTHI.activeTree.sourceFrameNumber === undefined)
+        return;
+      return prevLTHI.activeTree.sourceFrameNumber == this.sourceFrameNumber;
+    },
+
+    get otherTree() {
+      var other = this.whichTree == constants.ACTIVE_TREE ?
+          constants.PENDING_TREE : constants.ACTIVE_TREE;
+      return this.layerTreeHostImpl.getTree(other);
+    },
+
+    get gpuMemoryUsageInBytes() {
+      var totalBytes = 0;
+      this.iterLayers(function(layer) {
+        if (layer.gpuMemoryUsageInBytes !== undefined)
+          totalBytes += layer.gpuMemoryUsageInBytes;
+      });
+      return totalBytes;
+    },
+
+    iterLayers: function(func, thisArg) {
+      var visitedLayers = {};
+      function visitLayer(layer, depth, isMask, isReplica) {
+        if (visitedLayers[layer.layerId])
+          return;
+        visitedLayers[layer.layerId] = true;
+        func.call(thisArg, layer, depth, isMask, isReplica);
+        if (layer.children) {
+          for (var i = 0; i < layer.children.length; i++)
+            visitLayer(layer.children[i], depth + 1);
+        }
+        if (layer.maskLayer)
+          visitLayer(layer.maskLayer, depth + 1, true, false);
+        if (layer.replicaLayer)
+          visitLayer(layer.replicaLayer, depth + 1, false, true);
+      }
+      visitLayer(this.rootLayer, 0, false, false);
+    },
+    findLayerWithId: function(id) {
+      var foundLayer = undefined;
+      function visitLayer(layer) {
+        if (layer.layerId == id)
+          foundLayer = layer;
+      }
+      this.iterLayers(visitLayer);
+      return foundLayer;
+    }
+  };
+
+  ObjectSnapshot.register(
+      LayerTreeImplSnapshot,
+      {typeName: 'cc::LayerTreeImpl'});
+
+  return {
+    LayerTreeImplSnapshot: LayerTreeImplSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_quad_stack_view.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_quad_stack_view.html
new file mode 100644
index 0000000..7581c63
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_quad_stack_view.html
@@ -0,0 +1,1187 @@
+<!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="/extras/cc/picture.html">
+<link rel="import" href="/extras/cc/render_pass.html">
+<link rel="import" href="/extras/cc/tile.html">
+<link rel="import" href="/extras/cc/debug_colors.html">
+<link rel="import" href="/extras/cc/util.html">
+<link rel="import" href="/base/color.html">
+<link rel="import" href="/base/properties.html">
+<link rel="import" href="/base/raf.html">
+<link rel="import" href="/base/quad.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/base/ui/quad_stack_view.html">
+<link rel="import" href="/base/ui/info_bar.html">
+
+<style>
+layer-tree-quad-stack-view {
+  position: relative;
+}
+
+layer-tree-quad-stack-view > top-controls {
+  -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;
+  display: flex;
+  flex-flow: row wrap;
+  flex-direction: row;
+  font-size:  14px;
+  padding-left: 2px;
+  overflow: hidden;
+}
+
+layer-tree-quad-stack-view > top-controls input[type='checkbox'] {
+  vertical-align: -2px;
+}
+
+layer-tree-quad-stack-view > .what-rasterized {
+  color: -webkit-link;
+  cursor: pointer;
+  text-decoration: underline;
+  position: absolute;
+  bottom: 10px;
+  left: 10px;
+}
+
+layer-tree-quad-stack-view > #input-event {
+  content: url('./images/input-event.png');
+  display: none;
+}
+
+</style>
+
+<template id='layer-tree-quad-stack-view-template'>
+  <img id='input-event'/>
+</template>
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Graphical view of  LayerTreeImpl, with controls for
+ * type of layer content shown and info bar for content-loading warnings.
+ */
+tv.exportTo('tv.e.cc', function() {
+
+  var THIS_DOC = document.currentScript.ownerDocument;
+  var TILE_HEATMAP_TYPE = {};
+  TILE_HEATMAP_TYPE.NONE = 'none';
+  TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY = 'scheduledPriority';
+  TILE_HEATMAP_TYPE.USING_GPU_MEMORY = 'usingGpuMemory';
+
+  function createTileRectsSelectorBaseOptions() {
+    return [{label: 'None', value: 'none'},
+            {label: 'Coverage Rects', value: 'coverage'}];
+  }
+
+  var bytesToRoundedMegabytes = tv.e.cc.bytesToRoundedMegabytes;
+
+
+  /**
+   * @constructor
+   */
+  var LayerTreeQuadStackView = tv.b.ui.define('layer-tree-quad-stack-view');
+
+  LayerTreeQuadStackView.prototype = {
+    __proto__: HTMLDivElement.prototype,
+
+    decorate: function() {
+      this.isRenderPassQuads_ = false;
+      this.pictureAsImageData_ = {}; // Maps picture.guid to PictureAsImageData.
+      this.messages_ = [];
+      this.controls_ = document.createElement('top-controls');
+      this.infoBar_ = document.createElement('tv-b-ui-info-bar');
+      this.quadStackView_ = new tv.b.ui.QuadStackView();
+      this.quadStackView_.addEventListener(
+          'selectionchange', this.onQuadStackViewSelectionChange_.bind(this));
+      this.extraHighlightsByLayerId_ = undefined;
+      this.inputEventImageData_ = undefined;
+
+      var m = tv.b.ui.MOUSE_SELECTOR_MODE;
+      var mms = this.quadStackView_.mouseModeSelector;
+      mms.settingsKey = 'tv.e.cc.layerTreeQuadStackView.mouseModeSelector';
+      mms.setKeyCodeForMode(m.SELECTION, 'Z'.charCodeAt(0));
+      mms.setKeyCodeForMode(m.PANSCAN, 'X'.charCodeAt(0));
+      mms.setKeyCodeForMode(m.ZOOM, 'C'.charCodeAt(0));
+      mms.setKeyCodeForMode(m.ROTATE, 'V'.charCodeAt(0));
+
+      var node = tv.b.instantiateTemplate(
+          '#layer-tree-quad-stack-view-template', THIS_DOC);
+      this.appendChild(node);
+      this.appendChild(this.controls_);
+      this.appendChild(this.infoBar_);
+      this.appendChild(this.quadStackView_);
+
+      this.tileRectsSelector_ = tv.b.ui.createSelector(
+          this, 'howToShowTiles',
+          'layerView.howToShowTiles', 'none',
+          createTileRectsSelectorBaseOptions());
+      this.controls_.appendChild(this.tileRectsSelector_);
+
+      var tileHeatmapText = tv.b.ui.createSpan({
+        textContent: 'Tile heatmap:'
+      });
+      this.controls_.appendChild(tileHeatmapText);
+
+      var tileHeatmapSelector = tv.b.ui.createSelector(
+          this, 'tileHeatmapType',
+          'layerView.tileHeatmapType', TILE_HEATMAP_TYPE.NONE,
+          [{label: 'None',
+            value: TILE_HEATMAP_TYPE.NONE},
+           {label: 'Scheduled Priority',
+            value: TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY},
+           {label: 'Is using GPU memory',
+            value: TILE_HEATMAP_TYPE.USING_GPU_MEMORY}
+          ]);
+      this.controls_.appendChild(tileHeatmapSelector);
+
+      var showOtherLayersCheckbox = tv.b.ui.createCheckBox(
+          this, 'showOtherLayers',
+          'layerView.showOtherLayers', true,
+          'Other layers/passes');
+      showOtherLayersCheckbox.title =
+          'When checked, show all layers, selected or not.';
+      this.controls_.appendChild(showOtherLayersCheckbox);
+
+      var showInvalidationsCheckbox = tv.b.ui.createCheckBox(
+          this, 'showInvalidations',
+          'layerView.showInvalidations', true,
+          'Invalidations');
+      showInvalidationsCheckbox.title =
+          'When checked, compositing invalidations are highlighted in red';
+      this.controls_.appendChild(showInvalidationsCheckbox);
+
+      var showUnrecordedRegionCheckbox = tv.b.ui.createCheckBox(
+          this, 'showUnrecordedRegion',
+          'layerView.showUnrecordedRegion', true,
+          'Unrecorded area');
+      showUnrecordedRegionCheckbox.title =
+          'When checked, unrecorded areas are highlighted in yellow';
+      this.controls_.appendChild(showUnrecordedRegionCheckbox);
+
+      var showBottlenecksCheckbox = tv.b.ui.createCheckBox(
+          this, 'showBottlenecks',
+          'layerView.showBottlenecks', true,
+          'Bottlenecks');
+      showBottlenecksCheckbox.title =
+          'When checked, scroll bottlenecks are highlighted';
+      this.controls_.appendChild(showBottlenecksCheckbox);
+
+      var showLayoutRectsCheckbox = tv.b.ui.createCheckBox(
+          this, 'showLayoutRects',
+          'layerView.showLayoutRects', false,
+          'Layout rects');
+      showLayoutRectsCheckbox.title =
+          'When checked, shows rects for regions where layout happened';
+      this.controls_.appendChild(showLayoutRectsCheckbox);
+
+      var showContentsCheckbox = tv.b.ui.createCheckBox(
+          this, 'showContents',
+          'layerView.showContents', true,
+          'Contents');
+      showContentsCheckbox.title =
+          'When checked, show the rendered contents inside the layer outlines';
+      this.controls_.appendChild(showContentsCheckbox);
+
+      var showAnimationBoundsCheckbox = tv.b.ui.createCheckBox(
+          this, 'showAnimationBounds',
+          'layerView.showAnimationBounds', false,
+          'Animation Bounds');
+      showAnimationBoundsCheckbox.title = 'When checked, show a border around' +
+          ' a layer showing the extent of its animation.';
+      this.controls_.appendChild(showAnimationBoundsCheckbox);
+
+      var showInputEventsCheckbox = tv.b.ui.createCheckBox(
+          this, 'showInputEvents',
+          'layerView.showInputEvents', true,
+          'Input events');
+      showInputEventsCheckbox.title = 'When checked, input events are ' +
+          'displayed as circles.';
+      this.controls_.appendChild(showInputEventsCheckbox);
+
+      this.whatRasterizedLink_ = document.createElement('a');
+      this.whatRasterizedLink_.classList.add('what-rasterized');
+      this.whatRasterizedLink_.textContent = 'What rasterized?';
+      this.whatRasterizedLink_.addEventListener(
+          'click', this.onWhatRasterizedLinkClicked_.bind(this));
+      this.appendChild(this.whatRasterizedLink_);
+    },
+
+    get layerTreeImpl() {
+      return this.layerTreeImpl_;
+    },
+
+    set isRenderPassQuads(newValue) {
+      this.isRenderPassQuads_ = newValue;
+    },
+
+    set layerTreeImpl(layerTreeImpl) {
+      if (this.layerTreeImpl_ === layerTreeImpl)
+        return;
+
+      // FIXME(pdr): We may want to clear pictureAsImageData_ here to save
+      //             memory at the cost of performance. Note that
+      //             pictureAsImageData_ will be cleared when this is
+      //             destructed, but this view might live for several
+      //             layerTreeImpls.
+      this.layerTreeImpl_ = layerTreeImpl;
+      this.selection = undefined;
+    },
+
+    get extraHighlightsByLayerId() {
+      return this.extraHighlightsByLayerId_;
+    },
+
+    set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+      this.extraHighlightsByLayerId_ = extraHighlightsByLayerId;
+      this.scheduleUpdateContents_();
+    },
+
+    get showOtherLayers() {
+      return this.showOtherLayers_;
+    },
+
+    set showOtherLayers(show) {
+      this.showOtherLayers_ = show;
+      this.updateContents_();
+    },
+
+    get showAnimationBounds() {
+      return this.showAnimationBounds_;
+    },
+
+    set showAnimationBounds(show) {
+      this.showAnimationBounds_ = show;
+      this.updateContents_();
+    },
+
+    get showInputEvents() {
+      return this.showInputEvents_;
+    },
+
+    set showInputEvents(show) {
+      this.showInputEvents_ = show;
+      this.updateContents_();
+    },
+
+    get showContents() {
+      return this.showContents_;
+    },
+
+    set showContents(show) {
+      this.showContents_ = show;
+      this.updateContents_();
+    },
+
+    get showInvalidations() {
+      return this.showInvalidations_;
+    },
+
+    set showInvalidations(show) {
+      this.showInvalidations_ = show;
+      this.updateContents_();
+    },
+
+    get showUnrecordedRegion() {
+      return this.showUnrecordedRegion_;
+    },
+
+    set showUnrecordedRegion(show) {
+      this.showUnrecordedRegion_ = show;
+      this.updateContents_();
+    },
+
+    get showBottlenecks() {
+      return this.showBottlenecks_;
+    },
+
+    set showBottlenecks(show) {
+      this.showBottlenecks_ = show;
+      this.updateContents_();
+    },
+
+    get showLayoutRects() {
+      return this.showLayoutRects_;
+    },
+
+    set showLayoutRects(show) {
+      this.showLayoutRects_ = show;
+      this.updateContents_();
+    },
+
+    get howToShowTiles() {
+      return this.howToShowTiles_;
+    },
+
+    set howToShowTiles(val) {
+      // Make sure val is something we expect.
+      console.assert(
+          (val === 'none') ||
+          (val === 'coverage') ||
+          !isNaN(parseFloat(val)));
+
+      this.howToShowTiles_ = val;
+      this.updateContents_();
+    },
+
+    get tileHeatmapType() {
+      return this.tileHeatmapType_;
+    },
+
+    set tileHeatmapType(val) {
+      this.tileHeatmapType_ = val;
+      this.updateContents_();
+    },
+
+    get selection() {
+      return this.selection_;
+    },
+
+    set selection(selection) {
+      if (this.selection === selection)
+        return;
+      this.selection_ = selection;
+      tv.b.dispatchSimpleEvent(this, 'selection-change');
+      this.updateContents_();
+    },
+
+    regenerateContent: function() {
+      this.updateTilesSelector_();
+      this.updateContents_();
+    },
+
+    loadDataForImageElement_: function(image, callback) {
+      var imageContent = window.getComputedStyle(image).content;
+      image.src = imageContent.replace(/url\((.*)\)/, '$1');
+      image.onload = function() {
+        var canvas = document.createElement('canvas');
+        var ctx = canvas.getContext('2d');
+        canvas.width = image.width;
+        canvas.height = image.height;
+        ctx.drawImage(image, 0, 0);
+        var imageData = ctx.getImageData(
+            0, 0, canvas.width, canvas.height);
+        callback(imageData);
+      }
+    },
+
+    onQuadStackViewSelectionChange_: function(e) {
+      var selectableQuads = e.quads.filter(function(q) {
+        return q.selectionToSetIfClicked !== undefined;
+      });
+      if (selectableQuads.length == 0) {
+        this.selection = undefined;
+        return;
+      }
+
+      // Sort the quads low to high on stackingGroupId.
+      selectableQuads.sort(function(x, y) {
+        var z = x.stackingGroupId - y.stackingGroupId;
+        if (z != 0)
+          return z;
+        return x.selectionToSetIfClicked.specicifity -
+            y.selectionToSetIfClicked.specicifity;
+      });
+
+      // TODO(nduca): Support selecting N things at once.
+      var quadToSelect = selectableQuads[selectableQuads.length - 1];
+      this.selection = quadToSelect.selectionToSetIfClicked;
+    },
+
+    scheduleUpdateContents_: function() {
+      if (this.updateContentsPending_)
+        return;
+      this.updateContentsPending_ = true;
+      tv.b.requestAnimationFrameInThisFrameIfPossible(
+          this.updateContents_, this);
+    },
+
+    updateContents_: function() {
+      if (!this.layerTreeImpl_) {
+        this.quadStackView_.headerText = 'No tree';
+        this.quadStackView_.quads = [];
+        return;
+      }
+
+
+      var status = this.computePictureLoadingStatus_();
+      if (!status.picturesComplete)
+        return;
+
+      var lthi = this.layerTreeImpl_.layerTreeHostImpl;
+      var lthiInstance = lthi.objectInstance;
+      var worldViewportRect = tv.b.Rect.fromXYWH(
+          0, 0,
+          lthi.deviceViewportSize.width, lthi.deviceViewportSize.height);
+      this.quadStackView_.deviceRect = worldViewportRect;
+      if (this.isRenderPassQuads_)
+        this.quadStackView_.quads = this.generateRenderPassQuads();
+      else
+        this.quadStackView_.quads = this.generateLayerQuads();
+
+      this.updateWhatRasterizedLinkState_();
+
+      var message = '';
+      if (lthi.tilesHaveGpuMemoryUsageInfo) {
+        var thisTreeUsageInBytes = this.layerTreeImpl_.gpuMemoryUsageInBytes;
+        var otherTreeUsageInBytes = lthi.gpuMemoryUsageInBytes -
+            thisTreeUsageInBytes;
+        message += bytesToRoundedMegabytes(thisTreeUsageInBytes) +
+                'MB on this tree';
+        if (otherTreeUsageInBytes) {
+          message += ', ' +
+              bytesToRoundedMegabytes(otherTreeUsageInBytes) +
+              'MB on the other tree';
+        }
+      } else {
+        if (this.layerTreeImpl_) {
+          var thisTreeUsageInBytes = this.layerTreeImpl_.gpuMemoryUsageInBytes;
+          message += bytesToRoundedMegabytes(thisTreeUsageInBytes) +
+              'MB on this tree';
+
+          if (this.layerTreeImpl_.otherTree) {
+            // Older Chromes don't report enough data to know how much memory is
+            // being used across both trees. We know the memory consumed by each
+            // tree, but there is resource sharing *between the trees* so we
+            // can't simply sum up the per-tree costs. We need either the total
+            // plus one tree, to guess the unique on the other tree, etc. Newer
+            // chromes report memory per tile, which allows LTHI to compute the
+            // total tile memory usage, letting us figure things out properly.
+            message += ', ???MB on other tree. ';
+          }
+        }
+      }
+
+      if (lthi.args.tileManagerBasicState) {
+        var tmgs = lthi.args.tileManagerBasicState.globalState;
+        message += ' (softMax=' +
+          bytesToRoundedMegabytes(tmgs.softMemoryLimitInBytes) +
+          'MB, hardMax=' +
+          bytesToRoundedMegabytes(tmgs.hardMemoryLimitInBytes) + 'MB, ' +
+          tmgs.memoryLimitPolicy + ')';
+
+      } else {
+        // Old Chromes do not have a globalState on the LTHI dump.
+        // But they do issue a DidManage event wiht the globalstate. Find that
+        // event so that we show some global state.
+        var thread = lthi.snapshottedOnThread;
+        var didManageTilesSlices = thread.sliceGroup.slices.filter(function(s) {
+          if (s.category !== 'tv.e.cc')
+            return false;
+          if (s.title !== 'DidManage')
+            return false;
+          if (s.end > lthi.ts)
+            return false;
+          return true;
+        });
+        didManageTilesSlices.sort(function(x, y) {
+          return x.end - y.end;
+        });
+        if (didManageTilesSlices.length > 0) {
+          var newest = didManageTilesSlices[didManageTilesSlices.length - 1];
+          var tmgs = newest.args.state.global_state;
+          message += ' (softMax=' +
+            bytesToRoundedMegabytes(tmgs.soft_memory_limit_in_bytes) +
+            'MB, hardMax=' +
+            bytesToRoundedMegabytes(tmgs.hard_memory_limit_in_bytes) + 'MB, ' +
+            tmgs.memory_limit_policy + ')';
+        }
+      }
+
+      if (this.layerTreeImpl_.otherTree)
+        message += ' (Another tree exists)';
+
+
+      if (message.length)
+        this.quadStackView_.headerText = message;
+      else
+        this.quadStackView_.headerText = undefined;
+
+      this.updateInfoBar_(status.messages);
+    },
+
+    updateTilesSelector_: function() {
+      var data = createTileRectsSelectorBaseOptions();
+
+      if (this.layerTreeImpl_) {
+        // First get all of the scales information from LTHI.
+        var lthi = this.layerTreeImpl_.layerTreeHostImpl;
+        var scaleNames = lthi.getContentsScaleNames();
+        for (var scale in scaleNames) {
+          data.push({
+            label: 'Scale ' + scale + ' (' + scaleNames[scale] + ')',
+            value: scale
+          });
+        }
+      }
+
+      // Then create a new selector and replace the old one.
+      var new_selector = tv.b.ui.createSelector(
+          this, 'howToShowTiles',
+          'layerView.howToShowTiles', 'none',
+          data);
+      this.controls_.replaceChild(new_selector, this.tileRectsSelector_);
+      this.tileRectsSelector_ = new_selector;
+    },
+
+    computePictureLoadingStatus_: function() {
+      // Figure out if we can draw the quads yet. While we're at it, figure out
+      // if we have any warnings we need to show.
+      var layers = this.layers;
+      var status = {
+        messages: [],
+        picturesComplete: true
+      };
+      if (this.showContents) {
+        var hasPendingRasterizeImage = false;
+        var firstPictureError = undefined;
+        var hasMissingLayerRect = false;
+        var hasUnresolvedPictureRef = false;
+        for (var i = 0; i < layers.length; i++) {
+          var layer = layers[i];
+          for (var ir = 0; ir < layer.pictures.length; ++ir) {
+            var picture = layer.pictures[ir];
+
+            if (picture.idRef) {
+              hasUnresolvedPictureRef = true;
+              continue;
+            }
+            if (!picture.layerRect) {
+              hasMissingLayerRect = true;
+              continue;
+            }
+
+            var pictureAsImageData = this.pictureAsImageData_[picture.guid];
+            if (!pictureAsImageData) {
+              hasPendingRasterizeImage = true;
+              this.pictureAsImageData_[picture.guid] =
+                  tv.e.cc.PictureAsImageData.Pending(this);
+              picture.rasterize(
+                  {stopIndex: undefined},
+                  function(pictureImageData) {
+                    var picture_ = pictureImageData.picture;
+                    this.pictureAsImageData_[picture_.guid] = pictureImageData;
+                    this.scheduleUpdateContents_();
+                  }.bind(this));
+              continue;
+            }
+            if (pictureAsImageData.isPending()) {
+              hasPendingRasterizeImage = true;
+              continue;
+            }
+            if (pictureAsImageData.error) {
+              if (!firstPictureError)
+                firstPictureError = pictureAsImageData.error;
+              break;
+            }
+          }
+        }
+        if (hasPendingRasterizeImage) {
+          status.picturesComplete = false;
+        } else {
+          if (hasUnresolvedPictureRef) {
+            status.messages.push({
+              header: 'Missing picture',
+              details: 'Your trace didnt have pictures for every layer. ' +
+                  'Old chrome versions had this problem'});
+          }
+          if (hasMissingLayerRect) {
+            status.messages.push({
+              header: 'Missing layer rect',
+              details: 'Your trace may be corrupt or from a very old ' +
+                  'Chrome revision.'});
+          }
+          if (firstPictureError) {
+            status.messages.push({
+              header: 'Cannot rasterize',
+              details: firstPictureError});
+          }
+        }
+      }
+      if (this.showInputEvents && this.layerTreeImpl.tracedInputLatencies &&
+          this.inputEventImageData_ === undefined) {
+        var image = this.querySelector('#input-event');
+        if (!image.src) {
+          this.loadDataForImageElement_(image, function(imageData) {
+            this.inputEventImageData_ = imageData;
+            this.updateContentsPending_ = false;
+            this.scheduleUpdateContents_();
+          }.bind(this));
+        }
+        status.picturesComplete = false;
+      }
+      return status;
+    },
+
+    get selectedRenderPass() {
+      if (this.selection)
+        return this.selection.renderPass_;
+    },
+
+    get selectedLayer() {
+      if (this.selection) {
+        var selectedLayerId = this.selection.associatedLayerId;
+        return this.layerTreeImpl_.findLayerWithId(selectedLayerId);
+      }
+    },
+
+    get renderPasses() {
+      var renderPasses =
+          this.layerTreeImpl.layerTreeHostImpl.args.frame.renderPasses;
+      if (!this.showOtherLayers) {
+        var selectedRenderPass = this.selectedRenderPass;
+        if (selectedRenderPass)
+          renderPasses = [selectedRenderPass];
+      }
+      return renderPasses;
+    },
+
+    get layers() {
+      var layers = this.layerTreeImpl.renderSurfaceLayerList;
+      if (!this.showOtherLayers) {
+        var selectedLayer = this.selectedLayer;
+        if (selectedLayer)
+          layers = [selectedLayer];
+      }
+      return layers;
+    },
+
+    appendImageQuads_: function(quads, layer, layerQuad) {
+      // Generate image quads for the layer
+      for (var ir = 0; ir < layer.pictures.length; ++ir) {
+        var picture = layer.pictures[ir];
+        if (!picture.layerRect)
+          continue;
+
+        var unitRect = picture.layerRect.asUVRectInside(layer.bounds);
+        var iq = layerQuad.projectUnitRect(unitRect);
+
+        var pictureData = this.pictureAsImageData_[picture.guid];
+        if (this.showContents && pictureData && pictureData.imageData) {
+          iq.imageData = pictureData.imageData;
+          iq.borderColor = 'rgba(0,0,0,0)';
+        } else {
+          iq.imageData = undefined;
+        }
+
+        iq.stackingGroupId = layerQuad.stackingGroupId;
+        quads.push(iq);
+      }
+    },
+
+    appendAnimationQuads_: function(quads, layer, layerQuad) {
+      if (!layer.animationBoundsRect)
+        return;
+
+      var rect = layer.animationBoundsRect;
+      var abq = tv.b.Quad.fromRect(rect);
+
+      abq.backgroundColor = 'rgba(164,191,48,0.5)';
+      abq.borderColor = 'rgba(205,255,0,0.75)';
+      abq.borderWidth = 3.0;
+      abq.stackingGroupId = layerQuad.stackingGroupId;
+      abq.selectionToSetIfClicked = new tv.e.cc.AnimationRectSelection(
+          layer, rect);
+      quads.push(abq);
+    },
+
+    appendInvalidationQuads_: function(quads, layer, layerQuad) {
+      if (layer.layerTreeImpl.hasSourceFrameBeenDrawnBefore)
+        return;
+
+      // Generate the invalidation rect quads.
+      for (var ir = 0; ir < layer.annotatedInvalidation.rects.length; ir++) {
+        var rect = layer.annotatedInvalidation.rects[ir];
+        var unitRect = rect.asUVRectInside(layer.bounds);
+        var iq = layerQuad.projectUnitRect(unitRect);
+        iq.backgroundColor = 'rgba(0, 255, 0, 0.1)';
+        if (rect.reason === 'renderer insertion')
+            iq.backgroundColor = 'rgba(0, 255, 128, 0.1)';
+        iq.borderColor = 'rgba(0, 255, 0, 1)';
+        iq.stackingGroupId = layerQuad.stackingGroupId;
+        iq.selectionToSetIfClicked = new tv.e.cc.LayerRectSelection(
+            layer, 'Invalidation rect (' + rect.reason + ')', rect, rect);
+        quads.push(iq);
+      }
+
+      // Show unannotated invalidation rect quads if no annotated rects are
+      // available.
+      if (layer.annotatedInvalidation.rects.length === 0) {
+        for (var ir = 0; ir < layer.invalidation.rects.length; ir++) {
+          var rect = layer.invalidation.rects[ir];
+          var unitRect = rect.asUVRectInside(layer.bounds);
+          var iq = layerQuad.projectUnitRect(unitRect);
+          iq.backgroundColor = 'rgba(0, 255, 0, 0.1)';
+          iq.borderColor = 'rgba(0, 255, 0, 1)';
+          iq.stackingGroupId = layerQuad.stackingGroupId;
+          iq.selectionToSetIfClicked = new tv.e.cc.LayerRectSelection(
+              layer, 'Invalidation rect', rect, rect);
+          quads.push(iq);
+        }
+      }
+    },
+
+    appendUnrecordedRegionQuads_: function(quads, layer, layerQuad) {
+      // Generate the unrecorded region quads.
+      for (var ir = 0; ir < layer.unrecordedRegion.rects.length; ir++) {
+        var rect = layer.unrecordedRegion.rects[ir];
+        var unitRect = rect.asUVRectInside(layer.bounds);
+        var iq = layerQuad.projectUnitRect(unitRect);
+        iq.backgroundColor = 'rgba(240, 230, 140, 0.3)';
+        iq.borderColor = 'rgba(240, 230, 140, 1)';
+        iq.stackingGroupId = layerQuad.stackingGroupId;
+        iq.selectionToSetIfClicked = new tv.e.cc.LayerRectSelection(
+            layer, 'Unrecorded area', rect, rect);
+        quads.push(iq);
+      }
+    },
+
+    appendBottleneckQuads_: function(quads, layer, layerQuad, stackingGroupId) {
+      function processRegion(region, label, borderColor) {
+        var backgroundColor = borderColor.clone();
+        backgroundColor.a = 0.4 * (borderColor.a || 1.0);
+
+        if (!region || !region.rects)
+          return;
+
+        for (var ir = 0; ir < region.rects.length; ir++) {
+          var rect = region.rects[ir];
+          var unitRect = rect.asUVRectInside(layer.bounds);
+          var iq = layerQuad.projectUnitRect(unitRect);
+          iq.backgroundColor = backgroundColor.toString();
+          iq.borderColor = borderColor.toString();
+          iq.borderWidth = 4.0;
+          iq.stackingGroupId = stackingGroupId;
+          iq.selectionToSetIfClicked = new tv.e.cc.LayerRectSelection(
+              layer, label, rect, rect);
+          quads.push(iq);
+        }
+      }
+
+      processRegion(layer.touchEventHandlerRegion, 'Touch listener',
+                    tv.b.Color.fromString('rgb(228, 226, 27)'));
+      processRegion(layer.wheelEventHandlerRegion, 'Wheel listener',
+                    tv.b.Color.fromString('rgb(176, 205, 29)'));
+      processRegion(layer.nonFastScrollableRegion, 'Repaints on scroll',
+                    tv.b.Color.fromString('rgb(213, 134, 32)'));
+    },
+
+    appendTileCoverageRectQuads_: function(
+        quads, layer, layerQuad, heatmapType) {
+      if (!layer.tileCoverageRects)
+        return;
+
+      var tiles = [];
+      for (var ct = 0; ct < layer.tileCoverageRects.length; ++ct) {
+        var tile = layer.tileCoverageRects[ct].tile;
+        if (tile !== undefined)
+          tiles.push(tile);
+      }
+
+      var lthi = this.layerTreeImpl_.layerTreeHostImpl;
+      var minMax =
+          this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType);
+      var heatmapResult =
+          this.computeHeatmapColors_(tiles, minMax, heatmapType);
+      var heatIndex = 0;
+
+      for (var ct = 0; ct < layer.tileCoverageRects.length; ++ct) {
+        var rect = layer.tileCoverageRects[ct].geometryRect;
+        rect = rect.scale(1.0 / layer.geometryContentsScale);
+
+        var tile = layer.tileCoverageRects[ct].tile;
+
+        var unitRect = rect.asUVRectInside(layer.bounds);
+        var quad = layerQuad.projectUnitRect(unitRect);
+
+        quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+        quad.stackingGroupId = layerQuad.stackingGroupId;
+        var type = tv.e.cc.tileTypes.missing;
+        if (tile) {
+          type = tile.getTypeForLayer(layer);
+          quad.backgroundColor = heatmapResult[heatIndex].color;
+          ++heatIndex;
+        }
+
+        quad.borderColor = tv.e.cc.tileBorder[type].color;
+        quad.borderWidth = tv.e.cc.tileBorder[type].width;
+        var label;
+        if (tile)
+          label = 'coverageRect';
+        else
+          label = 'checkerboard coverageRect';
+        quad.selectionToSetIfClicked = new tv.e.cc.LayerRectSelection(
+            layer, label, rect, layer.tileCoverageRects[ct]);
+
+        quads.push(quad);
+      }
+    },
+
+    appendLayoutRectQuads_: function(quads, layer, layerQuad) {
+      if (!layer.layoutRects) {
+        return;
+      }
+
+      for (var ct = 0; ct < layer.layoutRects.length; ++ct) {
+        var rect = layer.layoutRects[ct].geometryRect;
+        rect = rect.scale(1.0 / layer.geometryContentsScale);
+
+        var unitRect = rect.asUVRectInside(layer.bounds);
+        var quad = layerQuad.projectUnitRect(unitRect);
+
+        quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+        quad.stackingGroupId = layerQuad.stackingGroupId;
+
+        quad.borderColor = 'rgba(0, 0, 200, 0.7)';
+        quad.borderWidth = 2;
+        var label;
+        label = 'Layout rect';
+        quad.selectionToSetIfClicked = new tv.e.cc.LayerRectSelection(
+            layer, label, rect);
+
+        quads.push(quad);
+      }
+    },
+
+    getValueForHeatmap_: function(tile, heatmapType) {
+      if (heatmapType == TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY) {
+        return tile.scheduledPriority == 0 ?
+            undefined :
+            tile.scheduledPriority;
+      } else if (heatmapType == TILE_HEATMAP_TYPE.USING_GPU_MEMORY) {
+        if (tile.isSolidColor)
+          return 0.5;
+        return tile.isUsingGpuMemory ? 0 : 1;
+      }
+    },
+
+    getMinMaxForHeatmap_: function(tiles, heatmapType) {
+      var range = new tv.b.Range();
+      if (heatmapType == TILE_HEATMAP_TYPE.USING_GPU_MEMORY) {
+        range.addValue(0);
+        range.addValue(1);
+        return range;
+      }
+
+      for (var i = 0; i < tiles.length; ++i) {
+        var value = this.getValueForHeatmap_(tiles[i], heatmapType);
+        if (value === undefined)
+          continue;
+        range.addValue(value);
+      }
+      if (range.range === 0)
+        range.addValue(1);
+      return range;
+    },
+
+    computeHeatmapColors_: function(tiles, minMax, heatmapType) {
+      var min = minMax.min;
+      var max = minMax.max;
+
+      var color = function(value) {
+        var hue = 120 * (1 - (value - min) / (max - min));
+        if (hue < 0)
+          hue = 0;
+        return 'hsla(' + hue + ', 100%, 50%, 0.5)';
+      };
+
+      var values = [];
+      for (var i = 0; i < tiles.length; ++i) {
+        var tile = tiles[i];
+        var value = this.getValueForHeatmap_(tile, heatmapType);
+        var res = {
+          value: value,
+          color: value !== undefined ? color(value) : undefined
+        };
+        values.push(res);
+      }
+
+      return values;
+    },
+
+    appendTilesWithScaleQuads_: function(
+        quads, layer, layerQuad, scale, heatmapType) {
+      var lthi = this.layerTreeImpl_.layerTreeHostImpl;
+
+      var tiles = [];
+      for (var i = 0; i < lthi.activeTiles.length; ++i) {
+        var tile = lthi.activeTiles[i];
+
+        if (Math.abs(tile.contentsScale - scale) > 1e-6)
+          continue;
+
+        // TODO(vmpstr): Make the stiching of tiles and layers a part of
+        // tile construction (issue 346)
+        if (layer.layerId != tile.layerId)
+          continue;
+
+        tiles.push(tile);
+      }
+
+      var minMax =
+          this.getMinMaxForHeatmap_(lthi.activeTiles, heatmapType);
+      var heatmapResult =
+          this.computeHeatmapColors_(tiles, minMax, heatmapType);
+
+      for (var i = 0; i < tiles.length; ++i) {
+        var tile = tiles[i];
+        var rect = tile.layerRect;
+        if (!tile.layerRect)
+          continue;
+        var unitRect = rect.asUVRectInside(layer.bounds);
+        var quad = layerQuad.projectUnitRect(unitRect);
+
+        quad.backgroundColor = 'rgba(0, 0, 0, 0)';
+        quad.stackingGroupId = layerQuad.stackingGroupId;
+
+        var type = tile.getTypeForLayer(layer);
+        quad.borderColor = tv.e.cc.tileBorder[type].color;
+        quad.borderWidth = tv.e.cc.tileBorder[type].width;
+
+        quad.backgroundColor = heatmapResult[i].color;
+        var data = {
+          tileType: type
+        };
+        if (heatmapType !== TILE_HEATMAP_TYPE.NONE)
+          data[heatmapType] = heatmapResult[i].value;
+        quad.selectionToSetIfClicked = new tv.e.cc.TileSelection(tile, data);
+        quads.push(quad);
+      }
+    },
+
+    appendHighlightQuadsForLayer_: function(
+        quads, layer, layerQuad, highlights) {
+      highlights.forEach(function(highlight) {
+        var rect = highlight.rect;
+
+        var unitRect = rect.asUVRectInside(layer.bounds);
+        var quad = layerQuad.projectUnitRect(unitRect);
+
+        var colorId = tv.b.ui.getColorIdForGeneralPurposeString(
+            highlight.colorKey);
+        colorId += tv.b.ui.getColorPaletteHighlightIdBoost();
+
+        var color = tv.b.Color.fromString(tv.b.ui.getColorPalette()[colorId]);
+
+        var quadForDrawing = quad.clone();
+        quadForDrawing.backgroundColor = color.withAlpha(0.5).toString();
+        quadForDrawing.borderColor = color.withAlpha(1.0).darken().toString();
+        quadForDrawing.stackingGroupId = layerQuad.stackingGroupId;
+        quads.push(quadForDrawing);
+
+      }, this);
+    },
+
+    generateRenderPassQuads: function() {
+      if (!this.layerTreeImpl.layerTreeHostImpl.args.frame)
+        return [];
+      var renderPasses = this.renderPasses;
+      if (!renderPasses)
+        return [];
+
+      var quads = [];
+      for (var i = 0; i < renderPasses.length; ++i) {
+        var quadList = renderPasses[i].quadList;
+        for (var j = 0; j < quadList.length; ++j) {
+          var drawQuad = quadList[j];
+          var quad = drawQuad.rectAsTargetSpaceQuad.clone();
+          quad.borderColor = 'rgb(170, 204, 238)';
+          quad.borderWidth = 2;
+          quad.stackingGroupId = i;
+          quads.push(quad);
+        }
+      }
+      return quads;
+    },
+
+    generateLayerQuads: function() {
+      this.updateContentsPending_ = false;
+
+      // Generate the quads for the view.
+      var layers = this.layers;
+      var quads = [];
+      var nextStackingGroupId = 0;
+      var alreadyVisitedLayerIds = {};
+
+
+      var selectionHighlightsByLayerId;
+      if (this.selection)
+        selectionHighlightsByLayerId = this.selection.highlightsByLayerId;
+      else
+        selectionHighlightsByLayerId = {};
+
+      var extraHighlightsByLayerId = this.extraHighlightsByLayerId || {};
+
+      for (var i = 1; i <= layers.length; i++) {
+        // Generate quads back-to-front.
+        var layer = layers[layers.length - i];
+        alreadyVisitedLayerIds[layer.layerId] = true;
+        if (layer.objectInstance.name == 'cc::NinePatchLayerImpl')
+          continue;
+
+        var layerQuad = layer.layerQuad.clone();
+        if (layer.usingGpuRasterization) {
+          var pixelRatio = window.devicePixelRatio || 1;
+          layerQuad.borderWidth = 2.0 * pixelRatio;
+          layerQuad.borderColor = 'rgba(154,205,50,0.75)';
+        } else {
+          layerQuad.borderColor = 'rgba(0,0,0,0.75)';
+        }
+        layerQuad.stackingGroupId = nextStackingGroupId++;
+        layerQuad.selectionToSetIfClicked = new tv.e.cc.LayerSelection(layer);
+        layerQuad.layer = layer;
+        if (this.showOtherLayers && this.selectedLayer == layer)
+          layerQuad.upperBorderColor = 'rgb(156,189,45)';
+
+        if (this.showAnimationBounds)
+          this.appendAnimationQuads_(quads, layer, layerQuad);
+
+        this.appendImageQuads_(quads, layer, layerQuad);
+        quads.push(layerQuad);
+
+
+        if (this.showInvalidations)
+          this.appendInvalidationQuads_(quads, layer, layerQuad);
+        if (this.showUnrecordedRegion)
+          this.appendUnrecordedRegionQuads_(quads, layer, layerQuad);
+        if (this.showBottlenecks)
+          this.appendBottleneckQuads_(quads, layer, layerQuad,
+                                      layerQuad.stackingGroupId);
+        if (this.showLayoutRects)
+          this.appendLayoutRectQuads_(quads, layer, layerQuad);
+
+        if (this.howToShowTiles === 'coverage') {
+          this.appendTileCoverageRectQuads_(
+              quads, layer, layerQuad, this.tileHeatmapType);
+        } else if (this.howToShowTiles !== 'none') {
+          this.appendTilesWithScaleQuads_(
+              quads, layer, layerQuad,
+              this.howToShowTiles, this.tileHeatmapType);
+        }
+
+        var highlights;
+        highlights = extraHighlightsByLayerId[layer.layerId];
+        if (highlights) {
+          this.appendHighlightQuadsForLayer_(
+              quads, layer, layerQuad, highlights);
+        }
+
+        highlights = selectionHighlightsByLayerId[layer.layerId];
+        if (highlights) {
+          this.appendHighlightQuadsForLayer_(
+              quads, layer, layerQuad, highlights);
+        }
+      }
+
+      this.layerTreeImpl.iterLayers(function(layer, depth, isMask, isReplica) {
+        if (!this.showOtherLayers && this.selectedLayer != layer)
+          return;
+        if (alreadyVisitedLayerIds[layer.layerId])
+          return;
+        var layerQuad = layer.layerQuad;
+        var stackingGroupId = nextStackingGroupId++;
+        if (this.showBottlenecks)
+          this.appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId);
+      }, this);
+
+      var tracedInputLatencies = this.layerTreeImpl.tracedInputLatencies;
+      if (this.showInputEvents && tracedInputLatencies) {
+        for (var i = 0; i < tracedInputLatencies.length; i++) {
+          var coordinatesArray = tracedInputLatencies[i].args.data.coordinates;
+          for (var j = 0; j < coordinatesArray.length; j++) {
+            var inputQuad = tv.b.Quad.fromXYWH(
+                coordinatesArray[j].x - 25,
+                coordinatesArray[j].y - 25,
+                50,
+                50);
+            inputQuad.borderColor = 'rgba(0, 0, 0, 0)';
+            inputQuad.imageData = this.inputEventImageData_;
+            quads.push(inputQuad);
+          }
+        }
+      }
+
+      return quads;
+    },
+
+    updateInfoBar_: function(infoBarMessages) {
+      if (infoBarMessages.length) {
+        this.infoBar_.removeAllButtons();
+        this.infoBar_.message = 'Some problems were encountered...';
+        this.infoBar_.addButton('More info...', function(e) {
+          var overlay = new tv.b.ui.Overlay();
+          overlay.textContent = '';
+          infoBarMessages.forEach(function(message) {
+            var title = document.createElement('h3');
+            title.textContent = message.header;
+
+            var details = document.createElement('div');
+            details.textContent = message.details;
+
+            overlay.appendChild(title);
+            overlay.appendChild(details);
+          });
+          overlay.visible = true;
+
+          e.stopPropagation();
+          return false;
+        });
+        this.infoBar_.visible = true;
+      } else {
+        this.infoBar_.removeAllButtons();
+        this.infoBar_.message = '';
+        this.infoBar_.visible = false;
+      }
+    },
+
+    getWhatRasterized_: function() {
+      var lthi = this.layerTreeImpl_.layerTreeHostImpl;
+      var renderProcess = lthi.objectInstance.parent;
+      var tasks = [];
+      renderProcess.iterateAllEvents(function(event) {
+        if (!(event instanceof tv.c.trace_model.Slice))
+          return;
+
+        var tile = tv.e.cc.getTileFromRasterTaskSlice(event);
+        if (tile === undefined)
+          return false;
+
+        if (tile.containingSnapshot == lthi)
+          tasks.push(event);
+      }, this);
+      return tasks;
+    },
+
+    updateWhatRasterizedLinkState_: function() {
+      var tasks = this.getWhatRasterized_();
+      if (tasks.length) {
+        this.whatRasterizedLink_.textContent = tasks.length + ' raster tasks';
+        this.whatRasterizedLink_.style.display = '';
+      } else {
+        this.whatRasterizedLink_.textContent = '';
+        this.whatRasterizedLink_.style.display = 'none';
+      }
+    },
+
+    onWhatRasterizedLinkClicked_: function() {
+      var tasks = this.getWhatRasterized_();
+      var event = new tv.c.RequestSelectionChangeEvent();
+      event.selection = new tv.c.Selection(tasks);
+      this.dispatchEvent(event);
+    }
+  };
+
+  return {
+    LayerTreeQuadStackView: LayerTreeQuadStackView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_tree_quad_stack_view_test.html b/trace-viewer/trace_viewer/extras/cc/layer_tree_quad_stack_view_test.html
new file mode 100644
index 0000000..fc3e9b0
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_tree_quad_stack_view_test.html
@@ -0,0 +1,46 @@
+<!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/cc/cc.html">
+<link rel="import" href="/extras/cc/layer_tree_quad_stack_view.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('tileCoverageRectCount', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = m.processes[1];
+
+    var instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+    var lthi = instance.snapshots[0];
+    var numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+    var layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+
+    var view = new tv.e.cc.LayerTreeQuadStackView();
+    view.layerTreeImpl = lthi.activeTree;
+    view.selection = new tv.e.cc.LayerSelection(layer);
+    view.howToShowTiles = 'none';
+    view.showInvalidations = false;
+    view.showContents = false;
+    view.showOtherLAyers = false;
+
+    // There should be some quads drawn with all "show" checkboxes off,
+    // but that number can change with new features added.
+    var aQuads = view.generateLayerQuads();
+    view.howToShowTiles = 'coverage';
+    var bQuads = view.generateLayerQuads();
+    var numCoverageRects = bQuads.length - aQuads.length;
+
+    // We know we have 5 coverage rects in lthi cats.
+    assert.equal(numCoverageRects, 5);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_view.css b/trace-viewer/trace_viewer/extras/cc/layer_view.css
new file mode 100644
index 0000000..603aa34
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_view.css
@@ -0,0 +1,57 @@
+/* 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.
+ */
+
+layer-view {
+  -webkit-flex-direction: column;
+  display:  -webkit-flex;
+  left: 0;
+  position: relative;
+  top: 0;
+}
+
+layer-view > layer-tree-quad-stack-view {
+  -webkit-flex: 1 1 100%;
+  -webkit-flex-direction: column;
+  display: -webkit-flex;
+  width: 100%;
+}
+
+layer-view > layer-view-analysis {
+  height: 150px; /* fixed height given by drag control */
+  overflow-y: auto;
+}
+
+layer-view > layer-view-analysis * {
+  -webkit-user-select: text;
+}
+
+.labeled-option-group {
+  -webkit-flex: 0 0 auto;
+  -webkit-flex-direction: column;
+  -webkit-align-items: left;
+  display: -webkit-flex;
+}
+
+.labeled-option {
+  border-top: 5px solid white;
+  border-bottom: 5px solid white;
+}
+
+.edit-categories {
+  padding-left: 6px;
+}
+
+.edit-categories:after {
+  padding-left: 15px;
+  font-size: 125%;
+}
+
+.labeled-option-group:not(.categories-expanded) .edit-categories:after {
+  content: '\25B8'; /* Right triangle */
+}
+
+.labeled-option-group.categories-expanded .edit-categories:after {
+  content: '\25BE'; /* Down triangle */
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_view.html b/trace-viewer/trace_viewer/extras/cc/layer_view.html
new file mode 100644
index 0000000..5f1c9a6
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_view.html
@@ -0,0 +1,147 @@
+<!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="/extras/cc/layer_view.css">
+
+<link rel="import" href="/extras/cc/constants.html">
+<link rel="import" href="/extras/cc/layer_tree_quad_stack_view.html">
+<link rel="import" href="/extras/cc/picture.html">
+<link rel="import" href="/base/raf.html">
+<link rel="import" href="/base/settings.html">
+<link rel="import" href="/base/ui/drag_handle.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview LayerView coordinates graphical and analysis views of layers.
+ */
+
+tv.exportTo('tv.e.cc', function() {
+  var constants = tv.e.cc.constants;
+
+  /**
+   * @constructor
+   */
+  var LayerView = tv.b.ui.define('layer-view');
+
+  LayerView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.layerTreeQuadStackView_ = new tv.e.cc.LayerTreeQuadStackView();
+      this.dragBar_ = new tv.b.ui.DragHandle();
+      this.analysisEl_ = document.createElement('layer-view-analysis');
+      this.analysisEl_.addEventListener('requestSelectionChange',
+          this.onRequestSelectionChangeFromAnalysisEl_.bind(this));
+
+      this.dragBar_.target = this.analysisEl_;
+
+      this.appendChild(this.layerTreeQuadStackView_);
+      this.appendChild(this.dragBar_);
+      this.appendChild(this.analysisEl_);
+
+      this.layerTreeQuadStackView_.addEventListener('selection-change',
+          function() {
+            this.layerTreeQuadStackViewSelectionChanged_();
+          }.bind(this));
+      this.layerTreeQuadStackViewSelectionChanged_();
+    },
+
+    get layerTreeImpl() {
+      return this.layerTreeQuadStackView_.layerTreeImpl;
+    },
+
+    set layerTreeImpl(newValue) {
+      return this.layerTreeQuadStackView_.layerTreeImpl = newValue;
+    },
+
+    set isRenderPassQuads(newValue) {
+      return this.layerTreeQuadStackView_.isRenderPassQuads = newValue;
+    },
+
+    get selection() {
+      return this.layerTreeQuadStackView_.selection;
+    },
+
+    set selection(newValue) {
+      this.layerTreeQuadStackView_.selection = newValue;
+    },
+
+    regenerateContent: function() {
+      this.layerTreeQuadStackView_.regenerateContent();
+    },
+
+    layerTreeQuadStackViewSelectionChanged_: function() {
+      var selection = this.layerTreeQuadStackView_.selection;
+      if (selection) {
+        this.dragBar_.style.display = '';
+        this.analysisEl_.style.display = '';
+        this.analysisEl_.textContent = '';
+
+        var layer = selection.layer;
+        if (layer && layer.args && layer.args.pictures) {
+          this.analysisEl_.appendChild(
+              this.createPictureBtn_(layer.args.pictures));
+        }
+
+        var analysis = selection.createAnalysis();
+        this.analysisEl_.appendChild(analysis);
+      } else {
+        this.dragBar_.style.display = 'none';
+        this.analysisEl_.style.display = 'none';
+        var analysis = this.analysisEl_.firstChild;
+        if (analysis)
+          this.analysisEl_.removeChild(analysis);
+        this.layerTreeQuadStackView_.style.height =
+            window.getComputedStyle(this).height;
+      }
+      tv.b.dispatchSimpleEvent(this, 'selection-change');
+    },
+
+    createPictureBtn_: function(pictures) {
+      if (!(pictures instanceof Array))
+        pictures = [pictures];
+
+      var link = document.createElement('tv-c-analysis-link');
+      link.selection = function() {
+        var layeredPicture = new tv.e.cc.LayeredPicture(pictures);
+        var snapshot = new tv.e.cc.PictureSnapshot(layeredPicture);
+        snapshot.picture = layeredPicture;
+
+        var selection = new tv.c.Selection();
+        selection.push(snapshot);
+        return selection;
+      };
+      link.textContent = 'View in Picture Debugger';
+      return link;
+    },
+
+    onRequestSelectionChangeFromAnalysisEl_: function(e) {
+      if (!(e.selection instanceof tv.e.cc.Selection))
+        return;
+
+      e.stopPropagation();
+      this.selection = e.selection;
+    },
+
+    get extraHighlightsByLayerId() {
+      return this.layerTreeQuadStackView_.extraHighlightsByLayerId;
+    },
+
+    set extraHighlightsByLayerId(extraHighlightsByLayerId) {
+      this.layerTreeQuadStackView_.extraHighlightsByLayerId =
+          extraHighlightsByLayerId;
+    }
+  };
+
+  return {
+    LayerView: LayerView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/layer_view_test.html b/trace-viewer/trace_viewer/extras/cc/layer_view_test.html
new file mode 100644
index 0000000..2c7bebe
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/layer_view_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="/extras/cc/cc.html">
+<link rel="import" href="/extras/cc/layer_view.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = m.processes[1];
+
+    var instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+    var lthi = instance.snapshots[0];
+    var numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+    var layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+
+    var view = new tv.e.cc.LayerView();
+    view.style.height = '500px';
+    view.layerTreeImpl = lthi.activeTree;
+    view.selection = new tv.e.cc.LayerSelection(layer);
+
+    this.addHTMLOutput(view);
+  });
+
+  test('instantiate_withTileHighlight', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = m.processes[1];
+
+    var instance = p.objects.getAllInstancesNamed('cc::LayerTreeHostImpl')[0];
+    var lthi = instance.snapshots[0];
+    var numLayers = lthi.activeTree.renderSurfaceLayerList.length;
+    var layer = lthi.activeTree.renderSurfaceLayerList[numLayers - 1];
+    var tile = lthi.activeTiles[0];
+
+    var view = new tv.e.cc.LayerView();
+    view.style.height = '500px';
+    view.layerTreeImpl = lthi.activeTree;
+    view.selection = new tv.e.cc.TileSelection(tile);
+    this.addHTMLOutput(view);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture.html b/trace-viewer/trace_viewer/extras/cc/picture.html
new file mode 100644
index 0000000..01d4b30
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture.html
@@ -0,0 +1,416 @@
+<!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/cc/picture_as_image_data.html">
+<link rel="import" href="/extras/cc/util.html">
+<link rel="import" href="/base/guid.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/base/raf.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  // Number of pictures created. Used as an uniqueId because we are immutable.
+  var PictureCount = 0;
+  var OPS_TIMING_ITERATIONS = 3;
+
+  function Picture(skp64, layerRect) {
+    this.skp64_ = skp64;
+    this.layerRect_ = layerRect;
+
+    this.guid_ = tv.b.GUID.allocate();
+  }
+
+  Picture.prototype = {
+    get canSave() {
+      return true;
+    },
+
+    get layerRect() {
+      return this.layerRect_;
+    },
+
+    get guid() {
+      return this.guid_;
+    },
+
+    getBase64SkpData: function() {
+      return this.skp64_;
+    },
+
+    getOps: function() {
+      if (!PictureSnapshot.CanGetOps()) {
+        console.error(PictureSnapshot.HowToEnablePictureDebugging());
+        return undefined;
+      }
+
+      var ops = window.chrome.skiaBenchmarking.getOps({
+        skp64: this.skp64_,
+        params: {
+          layer_rect: this.layerRect_.toArray()
+        }
+      });
+
+      if (!ops)
+        console.error('Failed to get picture ops.');
+
+      return ops;
+    },
+
+    getOpTimings: function() {
+      if (!PictureSnapshot.CanGetOpTimings()) {
+        console.error(PictureSnapshot.HowToEnablePictureDebugging());
+        return undefined;
+      }
+
+      var opTimings = window.chrome.skiaBenchmarking.getOpTimings({
+        skp64: this.skp64_,
+        params: {
+          layer_rect: this.layerRect_.toArray()
+        }
+      });
+
+      if (!opTimings)
+        console.error('Failed to get picture op timings.');
+
+      return opTimings;
+    },
+
+    /**
+     * Tag each op with the time it takes to rasterize.
+     *
+     * FIXME: We should use real statistics to get better numbers here, see
+     *        https://code.google.com/p/trace-viewer/issues/detail?id=357
+     *
+     * @param {Array} ops Array of Skia operations.
+     * @return {Array} Skia ops where op.cmd_time contains the associated time
+     *         for a given op.
+     */
+    tagOpsWithTimings: function(ops) {
+      var opTimings = new Array();
+      for (var iteration = 0; iteration < OPS_TIMING_ITERATIONS; iteration++) {
+        opTimings[iteration] = this.getOpTimings();
+        if (!opTimings[iteration] || !opTimings[iteration].cmd_times)
+          return ops;
+        if (opTimings[iteration].cmd_times.length != ops.length)
+          return ops;
+      }
+
+      for (var opIndex = 0; opIndex < ops.length; opIndex++) {
+        var min = Number.MAX_VALUE;
+        for (var i = 0; i < OPS_TIMING_ITERATIONS; i++)
+          min = Math.min(min, opTimings[i].cmd_times[opIndex]);
+        ops[opIndex].cmd_time = min;
+      }
+
+      return ops;
+    },
+
+    /**
+     * Rasterize the picture.
+     *
+     * @param {{opt_stopIndex: number, params}} The SkPicture operation to
+     *     rasterize up to. If not defined, the entire SkPicture is rasterized.
+     * @param {{opt_showOverdraw: bool, params}} Defines whether pixel overdraw
+           should be visualized in the image.
+     * @param {function(tv.e.cc.PictureAsImageData)} The callback function that
+     *     is called after rasterization is complete or fails.
+     */
+    rasterize: function(params, rasterCompleteCallback) {
+      if (!PictureSnapshot.CanRasterize() || !PictureSnapshot.CanGetOps()) {
+        rasterCompleteCallback(new tv.e.cc.PictureAsImageData(
+            this, tv.e.cc.PictureSnapshot.HowToEnablePictureDebugging()));
+        return;
+      }
+
+      var raster = window.chrome.skiaBenchmarking.rasterize(
+          {
+            skp64: this.skp64_,
+            params: {
+              layer_rect: this.layerRect_.toArray()
+            }
+          },
+          {
+            stop: params.stopIndex === undefined ? -1 : params.stopIndex,
+            overdraw: !!params.showOverdraw,
+            params: { }
+          });
+
+      if (raster) {
+        var canvas = document.createElement('canvas');
+        var ctx = canvas.getContext('2d');
+        canvas.width = raster.width;
+        canvas.height = raster.height;
+        var imageData = ctx.createImageData(raster.width, raster.height);
+        imageData.data.set(new Uint8ClampedArray(raster.data));
+        rasterCompleteCallback(new tv.e.cc.PictureAsImageData(this, imageData));
+      } else {
+        var error = 'Failed to rasterize picture. ' +
+                'Your recording may be from an old Chrome version. ' +
+                'The SkPicture format is not backward compatible.';
+        rasterCompleteCallback(new tv.e.cc.PictureAsImageData(this, error));
+      }
+    }
+  };
+
+  function LayeredPicture(pictures) {
+    this.guid_ = tv.b.GUID.allocate();
+    this.pictures_ = pictures;
+    this.layerRect_ = undefined;
+  }
+
+  LayeredPicture.prototype = {
+    __proto__: Picture.prototype,
+
+    get canSave() {
+      return false;
+    },
+
+    get typeName() {
+      return 'cc::LayeredPicture';
+    },
+
+    get layerRect() {
+      if (this.layerRect_ !== undefined)
+        return this.layerRect_;
+
+      this.layerRect_ = {
+        x: 0,
+        y: 0,
+        width: 0,
+        height: 0
+      };
+
+      for (var i = 0; i < this.pictures_.length; ++i) {
+        var rect = this.pictures_[i].layerRect;
+        this.layerRect_.x = Math.min(this.layerRect_.x, rect.x);
+        this.layerRect_.y = Math.min(this.layerRect_.y, rect.y);
+        this.layerRect_.width =
+            Math.max(this.layerRect_.width, rect.x + rect.width);
+        this.layerRect_.height =
+            Math.max(this.layerRect_.height, rect.y + rect.height);
+      }
+      return this.layerRect_;
+    },
+
+    get guid() {
+      return this.guid_;
+    },
+
+    getBase64SkpData: function() {
+      throw new Error('Not available with a LayeredPicture.');
+    },
+
+    getOps: function() {
+      var ops = [];
+      for (var i = 0; i < this.pictures_.length; ++i)
+        ops = ops.concat(this.pictures_[i].getOps());
+      return ops;
+    },
+
+    getOpTimings: function() {
+      var opTimings = this.pictures_[0].getOpTimings();
+      for (var i = 1; i < this.pictures_.length; ++i) {
+        var timings = this.pictures_[i].getOpTimings();
+        opTimings.cmd_times = opTimings.cmd_times.concat(timings.cmd_times);
+        opTimings.total_time += timings.total_time;
+      }
+      return opTimings;
+    },
+
+    tagOpsWithTimings: function(ops) {
+      var opTimings = new Array();
+      for (var iteration = 0; iteration < OPS_TIMING_ITERATIONS; iteration++) {
+        opTimings[iteration] = this.getOpTimings();
+        if (!opTimings[iteration] || !opTimings[iteration].cmd_times)
+          return ops;
+      }
+
+      for (var opIndex = 0; opIndex < ops.length; opIndex++) {
+        var min = Number.MAX_VALUE;
+        for (var i = 0; i < OPS_TIMING_ITERATIONS; i++)
+          min = Math.min(min, opTimings[i].cmd_times[opIndex]);
+        ops[opIndex].cmd_time = min;
+      }
+      return ops;
+    },
+
+    rasterize: function(params, rasterCompleteCallback) {
+      this.picturesAsImageData_ = [];
+      var rasterCallback = function(pictureAsImageData) {
+        this.picturesAsImageData_.push(pictureAsImageData);
+        if (this.picturesAsImageData_.length !== this.pictures_.length)
+          return;
+
+        var canvas = document.createElement('canvas');
+        var ctx = canvas.getContext('2d');
+        canvas.width = this.layerRect.width;
+        canvas.height = this.layerRect.height;
+
+        // TODO(dsinclair): Verify these finish in the order started.
+        //   Do the rasterize calls run sync or asyn? As the imageData
+        //   going to be in the same order as the pictures_ list?
+        for (var i = 0; i < this.picturesAsImageData_.length; ++i) {
+          ctx.putImageData(this.picturesAsImageData_[i].imageData,
+                           this.pictures_[i].layerRect.x,
+                           this.pictures_[i].layerRect.y);
+        }
+        this.picturesAsImageData_ = [];
+
+        rasterCompleteCallback(new tv.e.cc.PictureAsImageData(this,
+            ctx.getImageData(this.layerRect.x, this.layerRect.y,
+                             this.layerRect.width, this.layerRect.height)));
+      }.bind(this);
+
+      for (var i = 0; i < this.pictures_.length; ++i)
+        this.pictures_[i].rasterize(params, rasterCallback);
+    }
+  };
+
+
+  /**
+   * @constructor
+   */
+  function PictureSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  PictureSnapshot.HasSkiaBenchmarking = function() {
+    if (!window.chrome)
+      return false;
+    if (!window.chrome.skiaBenchmarking)
+      return false;
+    return true;
+  }
+
+  PictureSnapshot.CanRasterize = function() {
+    if (!PictureSnapshot.HasSkiaBenchmarking())
+      return false;
+    if (!window.chrome.skiaBenchmarking.rasterize)
+      return false;
+    return true;
+  }
+
+  PictureSnapshot.CanGetOps = function() {
+    if (!PictureSnapshot.HasSkiaBenchmarking())
+      return false;
+    if (!window.chrome.skiaBenchmarking.getOps)
+      return false;
+    return true;
+  }
+
+  PictureSnapshot.CanGetOpTimings = function() {
+    if (!PictureSnapshot.HasSkiaBenchmarking())
+      return false;
+    if (!window.chrome.skiaBenchmarking.getOpTimings)
+      return false;
+    return true;
+  }
+
+  PictureSnapshot.CanGetInfo = function() {
+    if (!PictureSnapshot.HasSkiaBenchmarking())
+      return false;
+    if (!window.chrome.skiaBenchmarking.getInfo)
+      return false;
+    return true;
+  }
+
+  PictureSnapshot.HowToEnablePictureDebugging = function() {
+    var usualReason = [
+      'For pictures to show up, you need to have Chrome running with ',
+      '--enable-skia-benchmarking. Please restart chrome with this flag ',
+      'and try again.'
+    ].join('');
+
+    if (!window.chrome)
+      return usualReason;
+    if (!window.chrome.skiaBenchmarking)
+      return usualReason;
+    if (!window.chrome.skiaBenchmarking.rasterize)
+      return 'Your chrome is old';
+    if (!window.chrome.skiaBenchmarking.getOps)
+      return 'Your chrome is old: skiaBenchmarking.getOps not found';
+    if (!window.chrome.skiaBenchmarking.getOpTimings)
+      return 'Your chrome is old: skiaBenchmarking.getOpTimings not found';
+    if (!window.chrome.skiaBenchmarking.getInfo)
+      return 'Your chrome is old: skiaBenchmarking.getInfo not found';
+    return 'Rasterizing is on';
+  }
+
+  PictureSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+      this.rasterResult_ = undefined;
+    },
+
+    initialize: function() {
+      // If we have an alias args, that means this picture was represented
+      // by an alias, and the real args is in alias.args.
+      if (this.args.alias)
+        this.args = this.args.alias.args;
+
+      if (!this.args.params.layerRect)
+        throw new Error('Missing layer rect');
+
+      this.layerRect_ = this.args.params.layerRect;
+      this.picture_ = new Picture(this.args.skp64, this.args.params.layerRect);
+    },
+
+    set picture(picture) {
+      this.picture_ = picture;
+    },
+
+    get canSave() {
+      return this.picture_.canSave;
+    },
+
+    get layerRect() {
+      return this.layerRect_ ? this.layerRect_ : this.picture_.layerRect;
+    },
+
+    get guid() {
+      return this.picture_.guid;
+    },
+
+    getBase64SkpData: function() {
+      return this.picture_.getBase64SkpData();
+    },
+
+    getOps: function() {
+      return this.picture_.getOps();
+    },
+
+    getOpTimings: function() {
+      return this.picture_.getOpTimings();
+    },
+
+    tagOpsWithTimings: function(ops) {
+      return this.picture_.tagOpsWithTimings(ops);
+    },
+
+    rasterize: function(params, rasterCompleteCallback) {
+      this.picture_.rasterize(params, rasterCompleteCallback);
+    }
+  };
+
+  ObjectSnapshot.register(
+      PictureSnapshot,
+      {typeNames: ['cc::Picture']});
+
+  return {
+    PictureSnapshot: PictureSnapshot,
+    Picture: Picture,
+    LayeredPicture: LayeredPicture
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_as_image_data.html b/trace-viewer/trace_viewer/extras/cc/picture_as_image_data.html
new file mode 100644
index 0000000..4775f71
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_as_image_data.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/base.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /**
+   * @constructor
+   */
+  function PictureAsImageData(picture, errorOrImageData) {
+    this.picture_ = picture;
+    if (errorOrImageData instanceof ImageData) {
+      this.error_ = undefined;
+      this.imageData_ = errorOrImageData;
+    } else {
+      this.error_ = errorOrImageData;
+      this.imageData_ = undefined;
+    }
+  };
+
+  /**
+   * Creates a new pending PictureAsImageData (no image data and no error).
+   *
+   * @return {PictureAsImageData} a new pending PictureAsImageData.
+   */
+  PictureAsImageData.Pending = function(picture) {
+    return new PictureAsImageData(picture, undefined);
+  };
+
+  PictureAsImageData.prototype = {
+    get picture() {
+      return this.picture_;
+    },
+
+    get error() {
+      return this.error_;
+    },
+
+    get imageData() {
+      return this.imageData_;
+    },
+
+    isPending: function() {
+      return this.error_ === undefined && this.imageData_ === undefined;
+    },
+
+    asCanvas: function() {
+      if (!this.imageData_)
+        return;
+
+      var canvas = document.createElement('canvas');
+      var ctx = canvas.getContext('2d');
+
+      canvas.width = this.imageData_.width;
+      canvas.height = this.imageData_.height;
+      ctx.putImageData(this.imageData_, 0, 0);
+      return canvas;
+    }
+  };
+
+  return {
+    PictureAsImageData: PictureAsImageData
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_debugger.html b/trace-viewer/trace_viewer/extras/cc/picture_debugger.html
new file mode 100644
index 0000000..930323f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_debugger.html
@@ -0,0 +1,481 @@
+<!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/cc/picture.html">
+<link rel="import" href="/extras/cc/picture_ops_chart_summary_view.html">
+<link rel="import" href="/extras/cc/picture_ops_chart_view.html">
+<link rel="import" href="/extras/cc/picture_ops_list_view.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/base/key_event_manager.html">
+<link rel="import" href="/base/ui/drag_handle.html">
+<link rel="import" href="/base/ui/info_bar.html">
+<link rel="import" href="/base/ui/list_view.html">
+<link rel="import" href="/base/ui/mouse_mode_selector.html">
+<link rel="import" href="/base/ui/overlay.html">
+<link rel="import" href="/base/utils.html">
+
+<template id="picture-debugger-template">
+  <style>
+  picture-debugger {
+    -webkit-flex: 1 1 auto;
+    -webkit-flex-direction: row;
+    display: -webkit-flex;
+  }
+
+  picture-debugger > x-generic-object-view {
+    -webkit-flex-direction: column;
+    display: -webkit-flex;
+    width: 400px;
+  }
+
+  picture-debugger > left-panel {
+    -webkit-flex-direction: column;
+    display: -webkit-flex;
+    min-width: 300px;
+  }
+
+  picture-debugger > left-panel > picture-info {
+    -webkit-flex: 0 0 auto;
+    padding-top: 2px;
+  }
+
+  picture-debugger > left-panel > picture-info .title {
+    font-weight: bold;
+    margin-left: 5px;
+    margin-right: 5px;
+  }
+
+  picture-debugger > x-drag-handle {
+    -webkit-flex: 0 0 auto;
+  }
+
+  picture-debugger .filename {
+    -webkit-user-select: text;
+    margin-left: 5px;
+  }
+
+  picture-debugger > right-panel {
+    -webkit-flex: 1 1 auto;
+    -webkit-flex-direction: column;
+    display: -webkit-flex;
+  }
+
+  picture-debugger > right-panel > picture-ops-chart-view {
+    min-height: 150px;
+    min-width : 0;
+    overflow-x: auto;
+    overflow-y: hidden;
+  }
+
+  /******************************************************************************/
+
+  raster-area {
+    background-color: #ddd;
+    min-height: 200px;
+    min-width: 200px;
+    overflow-y: auto;
+    padding-left: 5px;
+  }
+  </style>
+
+  <left-panel>
+    <picture-info>
+      <div>
+        <span class='title'>Skia Picture</span>
+        <span class='size'></span>
+      </div>
+      <div>
+        <input class='filename' type='text' value='skpicture.skp' />
+        <button class='export'>Export</button>
+      </div>
+    </picture-info>
+  </left-panel>
+  <right-panel>
+    <picture-ops-chart-view></picture-ops-chart-view>
+    <raster-area><canvas></canvas></raster-area>
+  </right-panel>
+</template>
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var THIS_DOC = document.currentScript.ownerDocument;
+
+  /**
+   * PictureDebugger is a view of a PictureSnapshot for inspecting
+   * the picture in detail. (e.g., timing information, etc.)
+   *
+   * @constructor
+   */
+  var PictureDebugger = tv.b.ui.define('picture-debugger');
+
+  PictureDebugger.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      var node = tv.b.instantiateTemplate('#picture-debugger-template',
+          THIS_DOC);
+
+      this.appendChild(node);
+
+      this.pictureAsImageData_ = undefined;
+      this.showOverdraw_ = false;
+      this.zoomScaleValue_ = 1;
+
+      this.sizeInfo_ = this.querySelector('.size');
+      this.rasterArea_ = this.querySelector('raster-area');
+      this.rasterCanvas_ = this.rasterArea_.querySelector('canvas');
+      this.rasterCtx_ = this.rasterCanvas_.getContext('2d');
+
+      this.filename_ = this.querySelector('.filename');
+
+      this.drawOpsChartSummaryView_ = new tv.e.cc.PictureOpsChartSummaryView();
+      this.drawOpsChartView_ = new tv.e.cc.PictureOpsChartView();
+      this.drawOpsChartView_.addEventListener(
+          'selection-changed', this.onChartBarClicked_.bind(this));
+
+      this.exportButton_ = this.querySelector('.export');
+      this.exportButton_.addEventListener(
+          'click', this.onSaveAsSkPictureClicked_.bind(this));
+
+      this.trackMouse_();
+
+      var overdrawCheckbox = tv.b.ui.createCheckBox(
+          this, 'showOverdraw',
+          'pictureView.showOverdraw', false,
+          'Show overdraw');
+
+      var chartCheckbox = tv.b.ui.createCheckBox(
+          this, 'showSummaryChart',
+          'pictureView.showSummaryChart', false,
+          'Show timing summary');
+
+      var pictureInfo = this.querySelector('picture-info');
+      pictureInfo.appendChild(overdrawCheckbox);
+      pictureInfo.appendChild(chartCheckbox);
+
+      this.drawOpsView_ = new tv.e.cc.PictureOpsListView();
+      this.drawOpsView_.addEventListener(
+          'selection-changed', this.onChangeDrawOps_.bind(this));
+
+      var leftPanel = this.querySelector('left-panel');
+      leftPanel.appendChild(this.drawOpsChartSummaryView_);
+      leftPanel.appendChild(this.drawOpsView_);
+
+      var middleDragHandle = new tv.b.ui.DragHandle();
+      middleDragHandle.horizontal = false;
+      middleDragHandle.target = leftPanel;
+
+      var rightPanel = this.querySelector('right-panel');
+      rightPanel.replaceChild(
+          this.drawOpsChartView_,
+          rightPanel.querySelector('picture-ops-chart-view'));
+
+      this.infoBar_ = document.createElement('tv-b-ui-info-bar');
+      this.rasterArea_.appendChild(this.infoBar_);
+
+      this.insertBefore(middleDragHandle, rightPanel);
+
+      this.picture_ = undefined;
+
+      tv.b.KeyEventManager.instance.addListener(
+          'keypress', this.onKeyPress_, this);
+
+      // Add a mutation observer so that when the view is resized we can
+      // update the chart summary view.
+      this.mutationObserver_ = new MutationObserver(
+          this.onMutation_.bind(this));
+      this.mutationObserver_.observe(leftPanel, { attributes: true });
+    },
+
+    onKeyPress_: function(e) {
+      if (e.keyCode == 'h'.charCodeAt(0)) {
+        this.moveSelectedOpBy(-1);
+        e.preventDefault();
+        e.stopPropagation();
+      } else if (e.keyCode == 'l'.charCodeAt(0)) {
+        this.moveSelectedOpBy(1);
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    },
+
+    onMutation_: function(mutations) {
+
+      for (var m = 0; m < mutations.length; m++) {
+        // A style change would indicate that the element has resized
+        // so we should re-render the chart.
+        if (mutations[m].attributeName === 'style') {
+          this.drawOpsChartSummaryView_.requiresRedraw = true;
+          this.drawOpsChartSummaryView_.updateChartContents();
+
+          this.drawOpsChartView_.dimensionsHaveChanged = true;
+          this.drawOpsChartView_.updateChartContents();
+          break;
+        }
+      }
+    },
+
+    onSaveAsSkPictureClicked_: function() {
+      // Decode base64 data into a String
+      var rawData = atob(this.picture_.getBase64SkpData());
+
+      // Convert this String into an Uint8Array
+      var length = rawData.length;
+      var arrayBuffer = new ArrayBuffer(length);
+      var uint8Array = new Uint8Array(arrayBuffer);
+      for (var c = 0; c < length; c++)
+        uint8Array[c] = rawData.charCodeAt(c);
+
+      // Create a blob URL from the binary array.
+      var blob = new Blob([uint8Array], {type: 'application/octet-binary'});
+      var blobUrl = window.webkitURL.createObjectURL(blob);
+
+      // Create a link and click on it. BEST API EVAR!
+      var link = document.createElementNS('http://www.w3.org/1999/xhtml', 'a');
+      link.href = blobUrl;
+      link.download = this.filename_.value;
+      var event = document.createEvent('MouseEvents');
+      event.initMouseEvent(
+          'click', true, false, window, 0, 0, 0, 0, 0,
+          false, false, false, false, 0, null);
+      link.dispatchEvent(event);
+    },
+
+    get picture() {
+      return this.picture_;
+    },
+
+    set picture(picture) {
+      this.drawOpsView_.picture = picture;
+      this.drawOpsChartView_.picture = picture;
+      this.drawOpsChartSummaryView_.picture = picture;
+      this.picture_ = picture;
+
+      this.exportButton_.disabled = !this.picture_.canSave;
+
+      if (picture) {
+        var size = this.getRasterCanvasSize_();
+        this.rasterCanvas_.width = size.width;
+        this.rasterCanvas_.height = size.height;
+      }
+
+      var bounds = this.rasterArea_.getBoundingClientRect();
+      var selectorBounds = this.mouseModeSelector_.getBoundingClientRect();
+      this.mouseModeSelector_.pos = {
+        x: (bounds.right - selectorBounds.width - 10),
+        y: bounds.top
+      };
+
+      this.rasterize_();
+
+      this.scheduleUpdateContents_();
+    },
+
+    getRasterCanvasSize_: function() {
+      var style = window.getComputedStyle(this.rasterArea_);
+      var width =
+          Math.max(parseInt(style.width), this.picture_.layerRect.width);
+      var height =
+          Math.max(parseInt(style.height), this.picture_.layerRect.height);
+
+      return {
+        width: width,
+        height: height
+      };
+    },
+
+    scheduleUpdateContents_: function() {
+      if (this.updateContentsPending_)
+        return;
+      this.updateContentsPending_ = true;
+      tv.b.requestAnimationFrameInThisFrameIfPossible(
+          this.updateContents_.bind(this)
+      );
+    },
+
+    updateContents_: function() {
+      this.updateContentsPending_ = false;
+
+      if (this.picture_) {
+        this.sizeInfo_.textContent = '(' +
+            this.picture_.layerRect.width + ' x ' +
+            this.picture_.layerRect.height + ')';
+      }
+
+      this.drawOpsChartView_.updateChartContents();
+      this.drawOpsChartView_.scrollSelectedItemIntoViewIfNecessary();
+
+      // Return if picture hasn't finished rasterizing.
+      if (!this.pictureAsImageData_)
+        return;
+
+      this.infoBar_.visible = false;
+      this.infoBar_.removeAllButtons();
+      if (this.pictureAsImageData_.error) {
+        this.infoBar_.message = 'Cannot rasterize...';
+        this.infoBar_.addButton('More info...', function(e) {
+          var overlay = new tv.b.ui.Overlay();
+          overlay.textContent = this.pictureAsImageData_.error;
+          overlay.visible = true;
+          e.stopPropagation();
+          return false;
+        }.bind(this));
+        this.infoBar_.visible = true;
+      }
+
+      this.drawPicture_();
+    },
+
+    drawPicture_: function() {
+      var size = this.getRasterCanvasSize_();
+      if (size.width !== this.rasterCanvas_.width)
+        this.rasterCanvas_.width = size.width;
+      if (size.height !== this.rasterCanvas_.height)
+        this.rasterCanvas_.height = size.height;
+
+      this.rasterCtx_.clearRect(0, 0, size.width, size.height);
+
+      if (!this.pictureAsImageData_.imageData)
+        return;
+
+      var imgCanvas = this.pictureAsImageData_.asCanvas();
+      var w = imgCanvas.width;
+      var h = imgCanvas.height;
+      this.rasterCtx_.drawImage(imgCanvas, 0, 0, w, h,
+                                0, 0, w * this.zoomScaleValue_,
+                                h * this.zoomScaleValue_);
+    },
+
+    rasterize_: function() {
+      if (this.picture_) {
+        this.picture_.rasterize(
+            {
+              stopIndex: this.drawOpsView_.selectedOpIndex,
+              showOverdraw: this.showOverdraw_
+            },
+            this.onRasterComplete_.bind(this));
+      }
+    },
+
+    onRasterComplete_: function(pictureAsImageData) {
+      this.pictureAsImageData_ = pictureAsImageData;
+      this.scheduleUpdateContents_();
+    },
+
+    moveSelectedOpBy: function(increment) {
+      if (this.selectedOpIndex === undefined) {
+        this.selectedOpIndex = 0;
+        return;
+      }
+      this.selectedOpIndex = tv.b.clamp(
+          this.selectedOpIndex + increment,
+          0, this.numOps);
+    },
+
+    get numOps() {
+      return this.drawOpsView_.numOps;
+    },
+
+    get selectedOpIndex() {
+      return this.drawOpsView_.selectedOpIndex;
+    },
+
+    set selectedOpIndex(index) {
+      this.drawOpsView_.selectedOpIndex = index;
+      this.drawOpsChartView_.selectedOpIndex = index;
+    },
+
+    onChartBarClicked_: function(e) {
+      this.drawOpsView_.selectedOpIndex =
+          this.drawOpsChartView_.selectedOpIndex;
+    },
+
+    onChangeDrawOps_: function(e) {
+      this.rasterize_();
+      this.scheduleUpdateContents_();
+
+      this.drawOpsChartView_.selectedOpIndex =
+          this.drawOpsView_.selectedOpIndex;
+    },
+
+    set showOverdraw(v) {
+      this.showOverdraw_ = v;
+      this.rasterize_();
+    },
+
+    set showSummaryChart(chartShouldBeVisible) {
+      if (chartShouldBeVisible)
+        this.drawOpsChartSummaryView_.show();
+      else
+        this.drawOpsChartSummaryView_.hide();
+    },
+
+    trackMouse_: function() {
+      this.mouseModeSelector_ = new tv.b.ui.MouseModeSelector(this.rasterArea_);
+      this.rasterArea_.appendChild(this.mouseModeSelector_);
+
+      this.mouseModeSelector_.supportedModeMask =
+          tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM;
+      this.mouseModeSelector_.mode = tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM;
+      this.mouseModeSelector_.defaultMode = tv.b.ui.MOUSE_SELECTOR_MODE.ZOOM;
+      this.mouseModeSelector_.settingsKey = 'pictureDebugger.mouseModeSelector';
+
+      this.mouseModeSelector_.addEventListener('beginzoom',
+          this.onBeginZoom_.bind(this));
+      this.mouseModeSelector_.addEventListener('updatezoom',
+          this.onUpdateZoom_.bind(this));
+      this.mouseModeSelector_.addEventListener('endzoom',
+          this.onEndZoom_.bind(this));
+    },
+
+    onBeginZoom_: function(e) {
+      this.isZooming_ = true;
+
+      this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e);
+
+      e.preventDefault();
+    },
+
+    onUpdateZoom_: function(e) {
+      if (!this.isZooming_)
+        return;
+
+      var currentMouseViewPos = this.extractRelativeMousePosition_(e);
+
+      // Take the distance the mouse has moved and we want to zoom at about
+      // 1/1000th of that speed. 0.01 feels jumpy. This could possibly be tuned
+      // more if people feel it's too slow.
+      this.zoomScaleValue_ +=
+          ((this.lastMouseViewPos_.y - currentMouseViewPos.y) * 0.001);
+      this.zoomScaleValue_ = Math.max(this.zoomScaleValue_, 0.1);
+
+      this.drawPicture_();
+
+      this.lastMouseViewPos_ = currentMouseViewPos;
+    },
+
+    onEndZoom_: function(e) {
+      this.lastMouseViewPos_ = undefined;
+      this.isZooming_ = false;
+      e.preventDefault();
+    },
+
+    extractRelativeMousePosition_: function(e) {
+      return {
+        x: e.clientX - this.rasterArea_.offsetLeft,
+        y: e.clientY - this.rasterArea_.offsetTop
+      };
+    }
+  };
+
+  return {
+    PictureDebugger: PictureDebugger
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_debugger_test.html b/trace-viewer/trace_viewer/extras/cc/picture_debugger_test.html
new file mode 100644
index 0000000..53aeec4
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_debugger_test.html
@@ -0,0 +1,32 @@
+<!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/cc/picture.html">
+<link rel="import" href="/extras/cc/picture_debugger.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('instantiate', function() {
+    var picture = new tv.e.cc.PictureSnapshot({id: '31415'}, 10, {
+      'params': {
+        'opaque_rect': [-15, -15, 0, 0],
+        'layer_rect': [-15, -15, 46, 833]
+      },
+      'skp64': 'DAAAAHYEAADzAQAABwAAAAFkYWVy8AAAAAgAAB4DAAAADAAAIAAAgD8AAIA/CAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADoAAAACAAAHgMAAAAMAAAjAAAAAAAAAAAMAAAjAAAAAAAAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADkAAAAGAAAFQEAAAAAAAAAAAAAAADAjkQAgPlDGAAAFQIAAAAAAAAAAAAAAADAjkQAgPlDCAAAHgMAAAAcAAADAAAAAAAAAAAAwI5EAID5QwEAAADgAAAAGAAAFQMAAAAAAKBAAACgQAAAgEIAAIBCBAAAHAQAABwEAAAcBAAAHHRjYWYBAAAADVNrU3JjWGZlcm1vZGVjZnB0AAAAAHlhcmGgAAAAIHRucAMAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgEAAAP//ADABAAAAAAAAAEBBAACAPwAAAAAAAIA/AAAAAAAAgED/////AjABAAAAAAAAAAAAAAAAAAEAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEEAAIA/AAAAAAAAgD8AAAAAAACAQP8AAP8AMAEAAAAAACBmb2U=' // @suppress longLineCheck
+    });
+    picture.preInitialize();
+    picture.initialize();
+
+    var dbg = new tv.e.cc.PictureDebugger();
+    this.addHTMLOutput(dbg);
+    dbg.picture = picture;
+    dbg.style.border = '1px solid black';
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_summary_view.css b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_summary_view.css
new file mode 100644
index 0000000..ab92b1f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_summary_view.css
@@ -0,0 +1,18 @@
+/* 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.
+ */
+
+picture-ops-chart-summary-view {
+  -webkit-flex: 0 0 auto;
+  font-size: 0;
+  margin: 0;
+  min-height: 200px;
+  min-width: 200px;
+  overflow: hidden;
+  padding: 0;
+}
+
+picture-ops-chart-summary-view.hidden {
+  display: none;
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_summary_view.html b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_summary_view.html
new file mode 100644
index 0000000..261c6dc
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_summary_view.html
@@ -0,0 +1,470 @@
+<!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="stylesheet" href="/extras/cc/picture_ops_chart_summary_view.css">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var OPS_TIMING_ITERATIONS = 3;
+  var CHART_PADDING_LEFT = 65;
+  var CHART_PADDING_RIGHT = 40;
+  var AXIS_PADDING_LEFT = 60;
+  var AXIS_PADDING_RIGHT = 35;
+  var AXIS_PADDING_TOP = 25;
+  var AXIS_PADDING_BOTTOM = 45;
+  var AXIS_LABEL_PADDING = 5;
+  var AXIS_TICK_SIZE = 10;
+  var LABEL_PADDING = 5;
+  var LABEL_INTERLEAVE_OFFSET = 15;
+  var BAR_PADDING = 5;
+  var VERTICAL_TICKS = 5;
+  var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
+
+  /**
+   * Provides a chart showing the cumulative time spent in Skia operations
+   * during picture rasterization.
+   *
+   * @constructor
+   */
+  var PictureOpsChartSummaryView = tv.b.ui.define(
+      'picture-ops-chart-summary-view');
+
+  PictureOpsChartSummaryView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.picture_ = undefined;
+      this.pictureDataProcessed_ = false;
+
+      this.chartScale_ = window.devicePixelRatio;
+
+      this.chart_ = document.createElement('canvas');
+      this.chartCtx_ = this.chart_.getContext('2d');
+      this.appendChild(this.chart_);
+
+      this.opsTimingData_ = [];
+
+      this.chartWidth_ = 0;
+      this.chartHeight_ = 0;
+      this.requiresRedraw_ = true;
+
+      this.currentBarMouseOverTarget_ = null;
+
+      this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+    },
+
+    get requiresRedraw() {
+      return this.requiresRedraw_;
+    },
+
+    set requiresRedraw(requiresRedraw) {
+      this.requiresRedraw_ = requiresRedraw;
+    },
+
+    get picture() {
+      return this.picture_;
+    },
+
+    set picture(picture) {
+      this.picture_ = picture;
+      this.pictureDataProcessed_ = false;
+
+      if (this.classList.contains('hidden'))
+        return;
+
+      this.processPictureData_();
+      this.requiresRedraw = true;
+      this.updateChartContents();
+    },
+
+    hide: function() {
+      this.classList.add('hidden');
+    },
+
+    show: function() {
+
+      this.classList.remove('hidden');
+
+      if (this.pictureDataProcessed_)
+        return;
+
+      this.processPictureData_();
+      this.requiresRedraw = true;
+      this.updateChartContents();
+
+    },
+
+    onMouseMove_: function(e) {
+
+      var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
+      this.currentBarMouseOverTarget_ = null;
+
+      var x = e.offsetX;
+      var y = e.offsetY;
+
+      var chartLeft = CHART_PADDING_LEFT;
+      var chartRight = this.chartWidth_ - CHART_PADDING_RIGHT;
+      var chartTop = AXIS_PADDING_TOP;
+      var chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM;
+      var chartInnerWidth = chartRight - chartLeft;
+
+      if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) {
+
+        this.currentBarMouseOverTarget_ = Math.floor(
+            (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length);
+
+        this.currentBarMouseOverTarget_ = tv.b.clamp(
+            this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1);
+
+      }
+
+      if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
+        return;
+
+      this.drawChartContents_();
+    },
+
+    updateChartContents: function() {
+
+      if (this.requiresRedraw)
+        this.updateChartDimensions_();
+
+      this.drawChartContents_();
+    },
+
+    updateChartDimensions_: function() {
+      this.chartWidth_ = this.offsetWidth;
+      this.chartHeight_ = this.offsetHeight;
+
+      // Scale up the canvas according to the devicePixelRatio, then reduce it
+      // down again via CSS. Finally we apply a scale to the canvas so that
+      // things are drawn at the correct size.
+      this.chart_.width = this.chartWidth_ * this.chartScale_;
+      this.chart_.height = this.chartHeight_ * this.chartScale_;
+
+      this.chart_.style.width = this.chartWidth_ + 'px';
+      this.chart_.style.height = this.chartHeight_ + 'px';
+
+      this.chartCtx_.scale(this.chartScale_, this.chartScale_);
+    },
+
+    processPictureData_: function() {
+
+      this.resetOpsTimingData_();
+      this.pictureDataProcessed_ = true;
+
+      if (!this.picture_)
+        return;
+
+      var ops = this.picture_.getOps();
+      if (!ops)
+        return;
+
+      ops = this.picture_.tagOpsWithTimings(ops);
+
+      // Check that there are valid times.
+      if (ops[0].cmd_time === undefined)
+        return;
+
+      this.collapseOpsToTimingBuckets_(ops);
+    },
+
+    drawChartContents_: function() {
+
+      this.clearChartContents_();
+
+      if (this.opsTimingData_.length === 0) {
+        this.showNoTimingDataMessage_();
+        return;
+      }
+
+      this.drawChartAxes_();
+      this.drawBars_();
+      this.drawLineAtBottomOfChart_();
+
+      if (this.currentBarMouseOverTarget_ === null)
+        return;
+
+      this.drawTooltip_();
+    },
+
+    drawLineAtBottomOfChart_: function() {
+      this.chartCtx_.strokeStyle = '#AAA';
+      this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
+      this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
+      this.chartCtx_.stroke();
+    },
+
+    drawTooltip_: function() {
+
+      var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_];
+      var tooltipTitle = tooltipData.cmd_string;
+      var tooltipTime = tooltipData.cmd_time.toFixed(4);
+
+      var tooltipWidth = 110;
+      var tooltipHeight = 40;
+      var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
+          CHART_PADDING_LEFT;
+      var barWidth = chartInnerWidth / this.opsTimingData_.length;
+      var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
+
+      var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
+          barWidth - tooltipOffset;
+      var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
+
+      this.chartCtx_.save();
+
+      this.chartCtx_.shadowOffsetX = 0;
+      this.chartCtx_.shadowOffsetY = 5;
+      this.chartCtx_.shadowBlur = 4;
+      this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
+
+      this.chartCtx_.strokeStyle = '#888';
+      this.chartCtx_.fillStyle = '#EEE';
+      this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
+
+      this.chartCtx_.shadowColor = 'transparent';
+      this.chartCtx_.translate(0.5, 0.5);
+      this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
+
+      this.chartCtx_.restore();
+
+      this.chartCtx_.fillStyle = '#222';
+      this.chartCtx_.textBaseline = 'top';
+      this.chartCtx_.font = '800 12px Arial';
+      this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
+
+      this.chartCtx_.fillStyle = '#555';
+      this.chartCtx_.textBaseline = 'top';
+      this.chartCtx_.font = '400 italic 10px Arial';
+      this.chartCtx_.fillText('Total: ' + tooltipTime + 'ms',
+          left + 8, top + 22);
+    },
+
+    drawBars_: function() {
+
+      var len = this.opsTimingData_.length;
+      var max = this.opsTimingData_[0].cmd_time;
+      var min = this.opsTimingData_[len - 1].cmd_time;
+
+      var width = this.chartWidth_ - CHART_PADDING_LEFT - CHART_PADDING_RIGHT;
+      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+      var barWidth = Math.floor(width / len);
+
+      var opData;
+      var opTiming;
+      var opHeight;
+      var opLabel;
+      var barLeft;
+
+      for (var b = 0; b < len; b++) {
+
+        opData = this.opsTimingData_[b];
+        opTiming = opData.cmd_time / max;
+
+        opHeight = Math.round(Math.max(1, opTiming * height));
+        opLabel = opData.cmd_string;
+        barLeft = CHART_PADDING_LEFT + b * barWidth;
+
+        this.chartCtx_.fillStyle = this.getOpColor_(opLabel);
+
+        this.chartCtx_.fillRect(barLeft + BAR_PADDING, AXIS_PADDING_TOP +
+            height - opHeight, barWidth - 2 * BAR_PADDING, opHeight);
+      }
+
+    },
+
+    getOpColor_: function(opName) {
+
+      var characters = opName.split('');
+      var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
+
+      return 'hsl(' + hue + ', 30%, 50%)';
+    },
+
+    reduceNameToHue: function(previousValue, currentValue, index, array) {
+      // Get the char code and apply a magic adjustment value so we get
+      // pretty colors from around the rainbow.
+      return Math.round(previousValue + currentValue.charCodeAt(0) *
+          HUE_CHAR_CODE_ADJUSTMENT);
+    },
+
+    drawChartAxes_: function() {
+
+      var len = this.opsTimingData_.length;
+      var max = this.opsTimingData_[0].cmd_time;
+      var min = this.opsTimingData_[len - 1].cmd_time;
+
+      var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
+      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+
+      var totalBarWidth = this.chartWidth_ - CHART_PADDING_LEFT -
+          CHART_PADDING_RIGHT;
+      var barWidth = Math.floor(totalBarWidth / len);
+      var tickYInterval = height / (VERTICAL_TICKS - 1);
+      var tickYPosition = 0;
+      var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
+      var tickVal = 0;
+
+      this.chartCtx_.fillStyle = '#333';
+      this.chartCtx_.strokeStyle = '#777';
+      this.chartCtx_.save();
+
+      // Translate half a pixel to avoid blurry lines.
+      this.chartCtx_.translate(0.5, 0.5);
+
+      // Sides.
+
+      this.chartCtx_.save();
+
+      this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+      this.chartCtx_.moveTo(0, 0);
+      this.chartCtx_.lineTo(0, height);
+      this.chartCtx_.lineTo(width, height);
+
+      // Y-axis ticks.
+      this.chartCtx_.font = '10px Arial';
+      this.chartCtx_.textAlign = 'right';
+      this.chartCtx_.textBaseline = 'middle';
+
+      for (var t = 0; t < VERTICAL_TICKS; t++) {
+
+        tickYPosition = Math.round(t * tickYInterval);
+        tickVal = (max - t * tickValInterval).toFixed(4);
+
+        this.chartCtx_.moveTo(0, tickYPosition);
+        this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
+        this.chartCtx_.fillText(tickVal,
+            -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
+
+      }
+
+      this.chartCtx_.stroke();
+
+      this.chartCtx_.restore();
+
+
+      // Labels.
+
+      this.chartCtx_.save();
+
+      this.chartCtx_.translate(CHART_PADDING_LEFT + Math.round(barWidth * 0.5),
+          AXIS_PADDING_TOP + height + LABEL_PADDING);
+
+      this.chartCtx_.font = '10px Arial';
+      this.chartCtx_.textAlign = 'center';
+      this.chartCtx_.textBaseline = 'top';
+
+      var labelTickLeft;
+      var labelTickBottom;
+      for (var l = 0; l < len; l++) {
+
+        labelTickLeft = Math.round(l * barWidth);
+        labelTickBottom = l % 2 * LABEL_INTERLEAVE_OFFSET;
+
+        this.chartCtx_.save();
+        this.chartCtx_.moveTo(labelTickLeft, -LABEL_PADDING);
+        this.chartCtx_.lineTo(labelTickLeft, labelTickBottom);
+        this.chartCtx_.stroke();
+        this.chartCtx_.restore();
+
+        this.chartCtx_.fillText(this.opsTimingData_[l].cmd_string,
+            labelTickLeft, labelTickBottom);
+      }
+
+      this.chartCtx_.restore();
+
+      this.chartCtx_.restore();
+    },
+
+    clearChartContents_: function() {
+      this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
+    },
+
+    showNoTimingDataMessage_: function() {
+      this.chartCtx_.font = '800 italic 14px Arial';
+      this.chartCtx_.fillStyle = '#333';
+      this.chartCtx_.textAlign = 'center';
+      this.chartCtx_.textBaseline = 'middle';
+      this.chartCtx_.fillText('No timing data available.',
+          this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
+    },
+
+    collapseOpsToTimingBuckets_: function(ops) {
+
+      var opsTimingDataIndexHash_ = {};
+      var timingData = this.opsTimingData_;
+      var op;
+      var opIndex;
+
+      for (var i = 0; i < ops.length; i++) {
+
+        op = ops[i];
+
+        if (op.cmd_time === undefined)
+          continue;
+
+        // Try to locate the entry for the current operation
+        // based on its name. If that fails, then create one for it.
+        opIndex = opsTimingDataIndexHash_[op.cmd_string] || null;
+
+        if (opIndex === null) {
+          timingData.push({
+            cmd_time: 0,
+            cmd_string: op.cmd_string
+          });
+
+          opIndex = timingData.length - 1;
+          opsTimingDataIndexHash_[op.cmd_string] = opIndex;
+        }
+
+        timingData[opIndex].cmd_time += op.cmd_time;
+
+      }
+
+      timingData.sort(this.sortTimingBucketsByOpTimeDescending_);
+
+      this.collapseTimingBucketsToOther_(4);
+    },
+
+    collapseTimingBucketsToOther_: function(count) {
+
+      var timingData = this.opsTimingData_;
+      var otherSource = timingData.splice(count, timingData.length - count);
+      var otherDestination = null;
+
+      if (!otherSource.length)
+        return;
+
+      timingData.push({
+        cmd_time: 0,
+        cmd_string: 'Other'
+      });
+
+      otherDestination = timingData[timingData.length - 1];
+      for (var i = 0; i < otherSource.length; i++) {
+        otherDestination.cmd_time += otherSource[i].cmd_time;
+      }
+    },
+
+    sortTimingBucketsByOpTimeDescending_: function(a, b) {
+      return b.cmd_time - a.cmd_time;
+    },
+
+    resetOpsTimingData_: function() {
+      this.opsTimingData_.length = 0;
+    }
+  };
+
+  return {
+    PictureOpsChartSummaryView: PictureOpsChartSummaryView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_view.css b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_view.css
new file mode 100644
index 0000000..caca4ae
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_view.css
@@ -0,0 +1,18 @@
+/* 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.
+ */
+
+picture-ops-chart-view {
+  display: block;
+  height: 180px;
+  margin: 0;
+  padding: 0;
+  position: relative;
+}
+
+picture-ops-chart-view > .use-percentile-scale {
+  left: 0;
+  position: absolute;
+  top: 0;
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_view.html b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_view.html
new file mode 100644
index 0000000..556ea8a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_chart_view.html
@@ -0,0 +1,498 @@
+<!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="/extras/cc/picture_ops_chart_view.css">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var BAR_PADDING = 1;
+  var BAR_WIDTH = 5;
+  var CHART_PADDING_LEFT = 65;
+  var CHART_PADDING_RIGHT = 30;
+  var CHART_PADDING_BOTTOM = 35;
+  var CHART_PADDING_TOP = 20;
+  var AXIS_PADDING_LEFT = 55;
+  var AXIS_PADDING_RIGHT = 30;
+  var AXIS_PADDING_BOTTOM = 35;
+  var AXIS_PADDING_TOP = 20;
+  var AXIS_TICK_SIZE = 5;
+  var AXIS_LABEL_PADDING = 5;
+  var VERTICAL_TICKS = 5;
+  var HUE_CHAR_CODE_ADJUSTMENT = 5.7;
+
+  /**
+   * Provides a chart showing the cumulative time spent in Skia operations
+   * during picture rasterization.
+   *
+   * @constructor
+   */
+  var PictureOpsChartView = tv.b.ui.define('picture-ops-chart-view');
+
+  PictureOpsChartView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.picture_ = undefined;
+      this.pictureOps_ = undefined;
+      this.opCosts_ = undefined;
+
+      this.chartScale_ = window.devicePixelRatio;
+
+      this.chart_ = document.createElement('canvas');
+      this.chartCtx_ = this.chart_.getContext('2d');
+      this.appendChild(this.chart_);
+
+      this.selectedOpIndex_ = undefined;
+      this.chartWidth_ = 0;
+      this.chartHeight_ = 0;
+      this.dimensionsHaveChanged_ = true;
+
+      this.currentBarMouseOverTarget_ = undefined;
+
+      this.ninetyFifthPercentileCost_ = 0;
+      this.totalOpCost_ = 0;
+
+      this.chart_.addEventListener('click', this.onClick_.bind(this));
+      this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+
+      this.usePercentileScale_ = false;
+      this.usePercentileScaleCheckbox_ = tv.b.ui.createCheckBox(
+          this, 'usePercentileScale',
+          'PictureOpsChartView.usePercentileScale', false,
+          'Limit to 95%-ile');
+      this.usePercentileScaleCheckbox_.classList.add('use-percentile-scale');
+      this.appendChild(this.usePercentileScaleCheckbox_);
+    },
+
+    get dimensionsHaveChanged() {
+      return this.dimensionsHaveChanged_;
+    },
+
+    set dimensionsHaveChanged(dimensionsHaveChanged) {
+      this.dimensionsHaveChanged_ = dimensionsHaveChanged;
+    },
+
+    get usePercentileScale() {
+      return this.usePercentileScale_;
+    },
+
+    set usePercentileScale(usePercentileScale) {
+      this.usePercentileScale_ = usePercentileScale;
+      this.drawChartContents_();
+    },
+
+    get numOps() {
+      return this.opCosts_.length;
+    },
+
+    get selectedOpIndex() {
+      return this.selectedOpIndex_;
+    },
+
+    set selectedOpIndex(selectedOpIndex) {
+      if (selectedOpIndex < 0) throw new Error('Invalid index');
+      if (selectedOpIndex >= this.numOps) throw new Error('Invalid index');
+
+      this.selectedOpIndex_ = selectedOpIndex;
+    },
+
+    get picture() {
+      return this.picture_;
+    },
+
+    set picture(picture) {
+      this.picture_ = picture;
+      this.pictureOps_ = picture.tagOpsWithTimings(picture.getOps());
+      this.currentBarMouseOverTarget_ = undefined;
+      this.processPictureData_();
+      this.dimensionsHaveChanged = true;
+    },
+
+    processPictureData_: function() {
+      if (this.pictureOps_ === undefined)
+        return;
+
+      var totalOpCost = 0;
+
+      // Take a copy of the picture ops data for sorting.
+      this.opCosts_ = this.pictureOps_.map(function(op) {
+        totalOpCost += op.cmd_time;
+        return op.cmd_time;
+      });
+      this.opCosts_.sort();
+
+      var ninetyFifthPercentileCostIndex = Math.floor(
+          this.opCosts_.length * 0.95);
+      this.ninetyFifthPercentileCost_ =
+          this.opCosts_[ninetyFifthPercentileCostIndex];
+      this.maxCost_ = this.opCosts_[this.opCosts_.length - 1];
+
+      this.totalOpCost_ = totalOpCost;
+    },
+
+    extractBarIndex_: function(e) {
+
+      var index = undefined;
+
+      if (this.pictureOps_ === undefined ||
+          this.pictureOps_.length === 0)
+        return index;
+
+      var x = e.offsetX;
+      var y = e.offsetY;
+
+      var totalBarWidth = (BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length;
+
+      var chartLeft = CHART_PADDING_LEFT;
+      var chartTop = 0;
+      var chartBottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
+      var chartRight = chartLeft + totalBarWidth;
+
+      if (x < chartLeft || x > chartRight || y < chartTop || y > chartBottom)
+        return index;
+
+      index = Math.floor((x - chartLeft) / totalBarWidth *
+          this.pictureOps_.length);
+
+      index = tv.b.clamp(index, 0, this.pictureOps_.length - 1);
+
+      return index;
+    },
+
+    onClick_: function(e) {
+
+      var barClicked = this.extractBarIndex_(e);
+
+      if (barClicked === undefined)
+        return;
+
+      // If we click on the already selected item we should deselect.
+      if (barClicked === this.selectedOpIndex)
+        this.selectedOpIndex = undefined;
+      else
+        this.selectedOpIndex = barClicked;
+
+      e.preventDefault();
+
+      tv.b.dispatchSimpleEvent(this, 'selection-changed', false);
+    },
+
+    onMouseMove_: function(e) {
+
+      var lastBarMouseOverTarget = this.currentBarMouseOverTarget_;
+      this.currentBarMouseOverTarget_ = this.extractBarIndex_(e);
+
+      if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget)
+        return;
+
+      this.drawChartContents_();
+    },
+
+    scrollSelectedItemIntoViewIfNecessary: function() {
+
+      if (this.selectedOpIndex === undefined)
+        return;
+
+      var width = this.offsetWidth;
+      var left = this.scrollLeft;
+      var right = left + width;
+      var targetLeft = CHART_PADDING_LEFT +
+          (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
+
+      if (targetLeft > left && targetLeft < right)
+        return;
+
+      this.scrollLeft = (targetLeft - width * 0.5);
+    },
+
+    updateChartContents: function() {
+
+      if (this.dimensionsHaveChanged)
+        this.updateChartDimensions_();
+
+      this.drawChartContents_();
+    },
+
+    updateChartDimensions_: function() {
+
+      if (!this.pictureOps_)
+        return;
+
+      var width = CHART_PADDING_LEFT + CHART_PADDING_RIGHT +
+          ((BAR_WIDTH + BAR_PADDING) * this.pictureOps_.length);
+
+      if (width < this.offsetWidth)
+        width = this.offsetWidth;
+
+      // Allow the element to be its natural size as set by flexbox, then lock
+      // the width in before we set the width of the canvas.
+      this.chartWidth_ = width;
+      this.chartHeight_ = this.getBoundingClientRect().height;
+
+      // Scale up the canvas according to the devicePixelRatio, then reduce it
+      // down again via CSS. Finally we apply a scale to the canvas so that
+      // things are drawn at the correct size.
+      this.chart_.width = this.chartWidth_ * this.chartScale_;
+      this.chart_.height = this.chartHeight_ * this.chartScale_;
+
+      this.chart_.style.width = this.chartWidth_ + 'px';
+      this.chart_.style.height = this.chartHeight_ + 'px';
+
+      this.chartCtx_.scale(this.chartScale_, this.chartScale_);
+
+      this.dimensionsHaveChanged = false;
+    },
+
+    drawChartContents_: function() {
+
+      this.clearChartContents_();
+
+      if (this.pictureOps_ === undefined ||
+          this.pictureOps_.length === 0 ||
+          this.pictureOps_[0].cmd_time === undefined) {
+
+        this.showNoTimingDataMessage_();
+        return;
+      }
+
+      this.drawSelection_();
+      this.drawBars_();
+      this.drawChartAxes_();
+      this.drawLinesAtTickMarks_();
+      this.drawLineAtBottomOfChart_();
+
+      if (this.currentBarMouseOverTarget_ === undefined)
+        return;
+
+      this.drawTooltip_();
+    },
+
+    drawSelection_: function() {
+
+      if (this.selectedOpIndex === undefined)
+        return;
+
+      var width = (BAR_WIDTH + BAR_PADDING) * this.selectedOpIndex;
+      this.chartCtx_.fillStyle = 'rgb(223, 235, 230)';
+      this.chartCtx_.fillRect(CHART_PADDING_LEFT, CHART_PADDING_TOP,
+          width, this.chartHeight_ - CHART_PADDING_TOP - CHART_PADDING_BOTTOM);
+    },
+
+    drawChartAxes_: function() {
+
+      var min = this.opCosts_[0];
+      var max = this.opCosts_[this.opCosts_.length - 1];
+      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+
+      var tickYInterval = height / (VERTICAL_TICKS - 1);
+      var tickYPosition = 0;
+      var tickValInterval = (max - min) / (VERTICAL_TICKS - 1);
+      var tickVal = 0;
+
+      this.chartCtx_.fillStyle = '#333';
+      this.chartCtx_.strokeStyle = '#777';
+      this.chartCtx_.save();
+
+      // Translate half a pixel to avoid blurry lines.
+      this.chartCtx_.translate(0.5, 0.5);
+
+      // Sides.
+      this.chartCtx_.beginPath();
+      this.chartCtx_.moveTo(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+      this.chartCtx_.lineTo(AXIS_PADDING_LEFT, this.chartHeight_ -
+          AXIS_PADDING_BOTTOM);
+      this.chartCtx_.lineTo(this.chartWidth_ - AXIS_PADDING_RIGHT,
+          this.chartHeight_ - AXIS_PADDING_BOTTOM);
+      this.chartCtx_.stroke();
+      this.chartCtx_.closePath();
+
+      // Y-axis ticks.
+      this.chartCtx_.translate(AXIS_PADDING_LEFT, AXIS_PADDING_TOP);
+
+      this.chartCtx_.font = '10px Arial';
+      this.chartCtx_.textAlign = 'right';
+      this.chartCtx_.textBaseline = 'middle';
+
+      this.chartCtx_.beginPath();
+      for (var t = 0; t < VERTICAL_TICKS; t++) {
+
+        tickYPosition = Math.round(t * tickYInterval);
+        tickVal = (max - t * tickValInterval).toFixed(4);
+
+        this.chartCtx_.moveTo(0, tickYPosition);
+        this.chartCtx_.lineTo(-AXIS_TICK_SIZE, tickYPosition);
+        this.chartCtx_.fillText(tickVal,
+            -AXIS_TICK_SIZE - AXIS_LABEL_PADDING, tickYPosition);
+
+      }
+
+      this.chartCtx_.stroke();
+      this.chartCtx_.closePath();
+
+      this.chartCtx_.restore();
+    },
+
+    drawLinesAtTickMarks_: function() {
+
+      var height = this.chartHeight_ - AXIS_PADDING_TOP - AXIS_PADDING_BOTTOM;
+      var width = this.chartWidth_ - AXIS_PADDING_LEFT - AXIS_PADDING_RIGHT;
+      var tickYInterval = height / (VERTICAL_TICKS - 1);
+      var tickYPosition = 0;
+
+      this.chartCtx_.save();
+
+      this.chartCtx_.translate(AXIS_PADDING_LEFT + 0.5, AXIS_PADDING_TOP + 0.5);
+      this.chartCtx_.beginPath();
+      this.chartCtx_.strokeStyle = 'rgba(0,0,0,0.05)';
+
+      for (var t = 0; t < VERTICAL_TICKS; t++) {
+        tickYPosition = Math.round(t * tickYInterval);
+
+        this.chartCtx_.moveTo(0, tickYPosition);
+        this.chartCtx_.lineTo(width, tickYPosition);
+        this.chartCtx_.stroke();
+      }
+
+      this.chartCtx_.restore();
+      this.chartCtx_.closePath();
+    },
+
+    drawLineAtBottomOfChart_: function() {
+      this.chartCtx_.strokeStyle = '#AAA';
+      this.chartCtx_.beginPath();
+      this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5);
+      this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5);
+      this.chartCtx_.stroke();
+      this.chartCtx_.closePath();
+    },
+
+    drawTooltip_: function() {
+
+      var tooltipData = this.pictureOps_[this.currentBarMouseOverTarget_];
+      var tooltipTitle = tooltipData.cmd_string;
+      var tooltipTime = tooltipData.cmd_time.toFixed(4);
+      var toolTipTimePercentage =
+          ((tooltipData.cmd_time / this.totalOpCost_) * 100).toFixed(2);
+
+      var tooltipWidth = 120;
+      var tooltipHeight = 40;
+      var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT -
+          CHART_PADDING_LEFT;
+      var barWidth = BAR_WIDTH + BAR_PADDING;
+      var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5);
+
+      var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ *
+          barWidth - tooltipOffset;
+      var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5);
+
+      this.chartCtx_.save();
+
+      this.chartCtx_.shadowOffsetX = 0;
+      this.chartCtx_.shadowOffsetY = 5;
+      this.chartCtx_.shadowBlur = 4;
+      this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)';
+
+      this.chartCtx_.strokeStyle = '#888';
+      this.chartCtx_.fillStyle = '#EEE';
+      this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight);
+
+      this.chartCtx_.shadowColor = 'transparent';
+      this.chartCtx_.translate(0.5, 0.5);
+      this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight);
+
+      this.chartCtx_.restore();
+
+      this.chartCtx_.fillStyle = '#222';
+      this.chartCtx_.textAlign = 'left';
+      this.chartCtx_.textBaseline = 'top';
+      this.chartCtx_.font = '800 12px Arial';
+      this.chartCtx_.fillText(tooltipTitle, left + 8, top + 8);
+
+      this.chartCtx_.fillStyle = '#555';
+      this.chartCtx_.font = '400 italic 10px Arial';
+      this.chartCtx_.fillText(tooltipTime + 'ms (' +
+          toolTipTimePercentage + '%)', left + 8, top + 22);
+    },
+
+    drawBars_: function() {
+
+      var op;
+      var opColor = 0;
+      var opHeight = 0;
+      var opWidth = BAR_WIDTH + BAR_PADDING;
+      var opHover = false;
+
+      var bottom = this.chartHeight_ - CHART_PADDING_BOTTOM;
+      var maxHeight = this.chartHeight_ - CHART_PADDING_BOTTOM -
+          CHART_PADDING_TOP;
+
+      var maxValue;
+      if (this.usePercentileScale)
+        maxValue = this.ninetyFifthPercentileCost_;
+      else
+        maxValue = this.maxCost_;
+
+      for (var b = 0; b < this.pictureOps_.length; b++) {
+
+        op = this.pictureOps_[b];
+        opHeight = Math.round(
+            (op.cmd_time / maxValue) * maxHeight);
+        opHeight = Math.max(opHeight, 1);
+        opHover = (b === this.currentBarMouseOverTarget_);
+        opColor = this.getOpColor_(op.cmd_string, opHover);
+
+        if (b === this.selectedOpIndex)
+          this.chartCtx_.fillStyle = '#FFFF00';
+        else
+          this.chartCtx_.fillStyle = opColor;
+
+        this.chartCtx_.fillRect(CHART_PADDING_LEFT + b * opWidth,
+            bottom - opHeight, BAR_WIDTH, opHeight);
+      }
+
+    },
+
+    getOpColor_: function(opName, hover) {
+
+      var characters = opName.split('');
+
+      var hue = characters.reduce(this.reduceNameToHue, 0) % 360;
+      var saturation = 30;
+      var lightness = hover ? '75%' : '50%';
+
+      return 'hsl(' + hue + ', ' + saturation + '%, ' + lightness + '%)';
+    },
+
+    reduceNameToHue: function(previousValue, currentValue, index, array) {
+      // Get the char code and apply a magic adjustment value so we get
+      // pretty colors from around the rainbow.
+      return Math.round(previousValue + currentValue.charCodeAt(0) *
+          HUE_CHAR_CODE_ADJUSTMENT);
+    },
+
+    clearChartContents_: function() {
+      this.chartCtx_.clearRect(0, 0, this.chartWidth_, this.chartHeight_);
+    },
+
+    showNoTimingDataMessage_: function() {
+      this.chartCtx_.font = '800 italic 14px Arial';
+      this.chartCtx_.fillStyle = '#333';
+      this.chartCtx_.textAlign = 'center';
+      this.chartCtx_.textBaseline = 'middle';
+      this.chartCtx_.fillText('No timing data available.',
+          this.chartWidth_ * 0.5, this.chartHeight_ * 0.5);
+    }
+  };
+
+  return {
+    PictureOpsChartView: PictureOpsChartView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view.css b/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view.css
new file mode 100644
index 0000000..f160243
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view.css
@@ -0,0 +1,56 @@
+/* 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.
+ */
+
+picture-ops-list-view {
+  -webkit-flex-direction: column;
+  border-top: 1px solid grey;
+  display: -webkit-flex;
+}
+
+picture-ops-list-view > .x-list-view {
+  -webkit-flex: 1 1 auto;
+  overflow: auto;
+}
+
+picture-ops-list-view > .x-list-view .list-item {
+  border-bottom: 1px solid #555;
+  font-size: small;
+  font-weight: bold;
+  padding-bottom: 5px;
+  padding-left: 5px;
+}
+
+picture-ops-list-view > .x-list-view .list-item:hover {
+  background-color: #f0f0f0;
+  cursor: pointer;
+}
+
+picture-ops-list-view > .x-list-view .list-item > * {
+  color: #777;
+  font-size: x-small;
+  font-weight: normal;
+  margin-left: 1em;
+  max-width: 300px; /* force long strings to wrap */
+}
+
+picture-ops-list-view > .x-list-view .list-item > .elementInfo {
+  color: purple;
+  font-size: small;
+  font-weight: bold;
+}
+
+picture-ops-list-view > .x-list-view .list-item > .time {
+  color: rgb(136, 0, 0);
+}
+
+.x-list-view:focus > .list-item[beforeSelection] {
+  background-color: rgb(171, 217, 202);
+  outline: 1px dotted rgba(0, 0, 0, 0.1);
+  outline-offset: 0;
+}
+
+.x-list-view > .list-item[beforeSelection] {
+  background-color: rgb(103, 199, 165);
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view.html b/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view.html
new file mode 100644
index 0000000..b428599
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view.html
@@ -0,0 +1,236 @@
+<!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="/extras/cc/picture_ops_list_view.css">
+
+<link rel="import" href="/extras/cc/constants.html">
+<link rel="import" href="/extras/cc/selection.html">
+<link rel="import" href="/base/ui/list_view.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var OPS_TIMING_ITERATIONS = 3; // Iterations to average op timing info over.
+  var ANNOTATION = 'Comment';
+  var BEGIN_ANNOTATION = 'BeginCommentGroup';
+  var END_ANNOTATION = 'EndCommentGroup';
+  var ANNOTATION_ID = 'ID: ';
+  var ANNOTATION_CLASS = 'CLASS: ';
+  var ANNOTATION_TAG = 'TAG: ';
+
+  var constants = tv.e.cc.constants;
+
+  /**
+   * @constructor
+   */
+  var PictureOpsListView = tv.b.ui.define('picture-ops-list-view');
+
+  PictureOpsListView.prototype = {
+    __proto__: HTMLUnknownElement.prototype,
+
+    decorate: function() {
+      this.opsList_ = new tv.b.ui.ListView();
+      this.appendChild(this.opsList_);
+
+      this.selectedOp_ = undefined;
+      this.selectedOpIndex_ = undefined;
+      this.opsList_.addEventListener(
+          'selection-changed', this.onSelectionChanged_.bind(this));
+
+      this.picture_ = undefined;
+    },
+
+    get picture() {
+      return this.picture_;
+    },
+
+    set picture(picture) {
+      this.picture_ = picture;
+      this.updateContents_();
+    },
+
+    updateContents_: function() {
+      this.opsList_.clear();
+
+      if (!this.picture_)
+        return;
+
+      var ops = this.picture_.getOps();
+      if (!ops)
+        return;
+
+      ops = this.picture_.tagOpsWithTimings(ops);
+
+      ops = this.opsTaggedWithAnnotations_(ops);
+
+      for (var i = 0; i < ops.length; i++) {
+        var op = ops[i];
+        var item = document.createElement('div');
+        item.opIndex = op.opIndex;
+        item.textContent = i + ') ' + op.cmd_string;
+
+        // Display the element info associated with the op, if available.
+        if (op.elementInfo.tag || op.elementInfo.id || op.elementInfo.class) {
+          var elementInfo = document.createElement('span');
+          elementInfo.classList.add('elementInfo');
+          var tag = op.elementInfo.tag ? op.elementInfo.tag : 'unknown';
+          var id = op.elementInfo.id ? 'id=' + op.elementInfo.id : undefined;
+          var className = op.elementInfo.class ? 'class=' +
+              op.elementInfo.class : undefined;
+          elementInfo.textContent =
+              '<' + tag + (id ? ' ' : '') +
+              (id ? id : '') + (className ? ' ' : '') +
+              (className ? className : '') + '>';
+          item.appendChild(elementInfo);
+        }
+
+        // Display the Skia params.
+        // FIXME: now that we have structured data, we should format it.
+        // (https://github.com/google/trace-viewer/issues/782)
+        if (op.info.length > 0) {
+          var infoItem = document.createElement('div');
+          infoItem.textContent = JSON.stringify(op.info);
+          item.appendChild(infoItem);
+        }
+
+        // Display the op timing, if available.
+        if (op.cmd_time && op.cmd_time >= 0.0001) {
+          var time = document.createElement('span');
+          time.classList.add('time');
+          var rounded = op.cmd_time.toFixed(4);
+          time.textContent = '(' + rounded + 'ms)';
+          item.appendChild(time);
+        }
+
+        this.opsList_.appendChild(item);
+      }
+    },
+
+    onSelectionChanged_: function(e) {
+      var beforeSelectedOp = true;
+
+      // Deselect on re-selection.
+      if (this.opsList_.selectedElement === this.selectedOp_) {
+        this.opsList_.selectedElement = undefined;
+        beforeSelectedOp = false;
+        this.selectedOpIndex_ = undefined;
+      }
+
+      this.selectedOp_ = this.opsList_.selectedElement;
+
+      // Set selection on all previous ops.
+      var ops = this.opsList_.children;
+      for (var i = 0; i < ops.length; i++) {
+        var op = ops[i];
+        if (op === this.selectedOp_) {
+          beforeSelectedOp = false;
+          this.selectedOpIndex_ = op.opIndex;
+        } else if (beforeSelectedOp) {
+          op.setAttribute('beforeSelection', 'beforeSelection');
+        } else {
+          op.removeAttribute('beforeSelection');
+        }
+      }
+
+      tv.b.dispatchSimpleEvent(this, 'selection-changed', false);
+    },
+
+    get numOps() {
+      return this.opsList_.children.length;
+    },
+
+    get selectedOpIndex() {
+      return this.selectedOpIndex_;
+    },
+
+    set selectedOpIndex(s) {
+      this.selectedOpIndex_ = s;
+
+      if (s === undefined) {
+        this.opsList_.selectedElement = this.selectedOp_;
+        this.onSelectionChanged_();
+      } else {
+        if (s < 0) throw new Error('Invalid index');
+        if (s >= this.numOps) throw new Error('Invalid index');
+        this.opsList_.selectedElement = this.opsList_.getElementByIndex(s + 1);
+        tv.b.scrollIntoViewIfNeeded(this.opsList_.selectedElement);
+      }
+    },
+
+    /**
+     * Return Skia operations tagged by annotation.
+     *
+     * The ops returned from Picture.getOps() contain both Skia ops and
+     * annotations threaded together. This function removes all annotations
+     * from the list and tags each op with the associated annotations.
+     * Additionally, the last {tag, id, class} is stored as elementInfo on
+     * each op.
+     *
+     * @param {Array} ops Array of Skia operations and annotations.
+     * @return {Array} Skia ops where op.annotations contains the associated
+     *         annotations for a given op.
+     */
+    opsTaggedWithAnnotations_: function(ops) {
+      // This algorithm works by walking all the ops and pushing any
+      // annotations onto a stack. When a non-annotation op is found, the
+      // annotations stack is traversed and stored with the op.
+      var annotationGroups = new Array();
+      var opsWithoutAnnotations = new Array();
+      for (var opIndex = 0; opIndex < ops.length; opIndex++) {
+        var op = ops[opIndex];
+        op.opIndex = opIndex;
+        switch (op.cmd_string) {
+          case BEGIN_ANNOTATION:
+            annotationGroups.push(new Array());
+            break;
+          case END_ANNOTATION:
+            annotationGroups.pop();
+            break;
+          case ANNOTATION:
+            annotationGroups[annotationGroups.length - 1].push(op);
+            break;
+          default:
+            var annotations = new Array();
+            var elementInfo = {};
+            annotationGroups.forEach(function(annotationGroup) {
+              elementInfo = {};
+              annotationGroup.forEach(function(annotation) {
+                annotation.info.forEach(function(info) {
+                  if (info.indexOf(ANNOTATION_TAG) != -1)
+                    elementInfo.tag = info.substring(
+                        info.indexOf(ANNOTATION_TAG) +
+                        ANNOTATION_TAG.length).toLowerCase();
+                  else if (info.indexOf(ANNOTATION_ID) != -1)
+                    elementInfo.id = info.substring(
+                        info.indexOf(ANNOTATION_ID) +
+                        ANNOTATION_ID.length);
+                  else if (info.indexOf(ANNOTATION_CLASS) != -1)
+                    elementInfo.class = info.substring(
+                        info.indexOf(ANNOTATION_CLASS) +
+                        ANNOTATION_CLASS.length);
+
+                  annotations.push(info);
+                });
+              });
+            });
+            op.annotations = annotations;
+            op.elementInfo = elementInfo;
+            opsWithoutAnnotations.push(op);
+        }
+      }
+
+      return opsWithoutAnnotations;
+    }
+  };
+
+  return {
+    PictureOpsListView: PictureOpsListView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view_test.html b/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view_test.html
new file mode 100644
index 0000000..056948d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_ops_list_view_test.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="/extras/cc/picture_ops_list_view.html">
+<link rel="import" href="/extras/cc/picture.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var PictureOpsListView = tv.e.cc.PictureOpsListView;
+
+  test('instantiate', function() {
+    if (!tv.e.cc.PictureSnapshot.CanRasterize())
+      return;
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+    var snapshot = instance.snapshots[0];
+
+    var view = new PictureOpsListView();
+    view.picture = snapshot;
+    assert.equal(view.opsList_.children.length, 142);
+  });
+
+  test('selection', function() {
+    if (!tv.e.cc.PictureSnapshot.CanRasterize())
+      return;
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+    var snapshot = instance.snapshots[0];
+
+    var view = new PictureOpsListView();
+    view.picture = snapshot;
+    var didSelectionChange = 0;
+    view.addEventListener('selection-changed', function() {
+      didSelectionChange = true;
+    });
+    assert.isFalse(didSelectionChange);
+    view.opsList_.selectedElement = view.opsList_.children[3];
+    assert.isTrue(didSelectionChange);
+  });
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_test.html b/trace-viewer/trace_viewer/extras/cc/picture_test.html
new file mode 100644
index 0000000..3e02ee2
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_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="/extras/cc/cc.html">
+<link rel="import" href="/extras/cc/picture.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('basic', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+    var snapshot = instance.snapshots[0];
+
+    assert.instanceOf(snapshot, tv.e.cc.PictureSnapshot);
+    instance.wasDeleted(150);
+  });
+
+  test('getOps', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('cc::Picture')[0];
+    var snapshot = instance.snapshots[0];
+
+    var ops = snapshot.getOps();
+    if (!ops)
+      return;
+    assert.equal(ops.length, 142);
+
+    var op0 = ops[0];
+    assert.equal(op0.cmd_string, 'Save');
+    assert.instanceOf(op0.info, Array);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_view.css b/trace-viewer/trace_viewer/extras/cc/picture_view.css
new file mode 100644
index 0000000..b2623c0
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_view.css
@@ -0,0 +1,9 @@
+/* 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.
+ */
+
+.picture-snapshot-view {
+  -webkit-flex: 0 1 auto !important;
+  display: -webkit-flex;
+}
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_view.html b/trace-viewer/trace_viewer/extras/cc/picture_view.html
new file mode 100644
index 0000000..0d8f307
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_view.html
@@ -0,0 +1,54 @@
+<!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="/extras/cc/picture_view.css">
+
+<link rel="import" href="/extras/cc/picture.html">
+<link rel="import" href="/extras/cc/picture_debugger.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /*
+   * Displays a picture snapshot in a human readable form.
+   * @constructor
+   */
+  var PictureSnapshotView = tv.b.ui.define(
+      'picture-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  PictureSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('picture-snapshot-view');
+      this.pictureDebugger_ = new tv.e.cc.PictureDebugger();
+      this.appendChild(this.pictureDebugger_);
+    },
+
+    updateContents: function() {
+      if (this.objectSnapshot_ && this.pictureDebugger_)
+        this.pictureDebugger_.picture = this.objectSnapshot_;
+    }
+  };
+
+  tv.c.analysis.ObjectSnapshotView.register(
+      PictureSnapshotView,
+      {
+        typeNames: ['cc::Picture', 'cc::LayeredPicture'],
+        showInstances: false
+      });
+
+  return {
+    PictureSnapshotView: PictureSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/picture_view_test_data.js b/trace-viewer/trace_viewer/extras/cc/picture_view_test_data.js
new file mode 100644
index 0000000..457586d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/picture_view_test_data.js
@@ -0,0 +1,34 @@
+// 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.
+
+'use strict';
+
+var g_picture_trace = {
+  'cat': 'disabled-by-default-cc.debug',
+  'pid': 23969,
+  'tid': 1799,
+  'ts': 1427012847340,
+  'ph': 'O',
+  'name': 'cc::Picture',
+  'args': {
+    'snapshot': {
+      'params': {
+        'layer_rect': [
+          557,
+          194,
+          33,
+          31
+        ],
+        'opaque_rect': [
+          0,
+          0,
+          0,
+          0
+        ]
+      },
+      'skp64': 'AQAAAJABAACQAAAAIQAAAB8AAACQAQAAkAAAAAAAAAAAAAAACwAAACEAAAAfAAAAAwAAAAFkYWVyTAEAAAgAAB4DAAAACAAAHgMAAAAMAAAjAADIwwAAEMMcAAADAADIQwAAEEMAgNhDAAAvQwEAAABEAQAAGAAAFQEAAAAAAMhDAAAQQwCA2EMAAC9DDAAAIwAAAAAAAAAADAAAIwAAAAAAAAAAGAAAFQIAAAAAAMhDAAAQQwCA2EMAAC9DGAAAFQIAAAAAAMhDAAAQQwCA2EMAAC9DCAAAHgMAAAAcAAADAADQQwAAIEMAgAFEAAAxQwEAAADUAAAAGAAAFQMAAAAAANBDAAAgQwCAAUQAADFDBAAAHGwAABQEAAAAGgAAADMASwBMAE8ATABTAAMANQBSAEoASABVAFYAAAANAAAAANYdQwAGNEMAAC5DAADQQwCA1UMAgNpDAIDcQwCA3kMAgOBDAIDlQwAA50MAAO1DAADyQwAA90MAgPtDAID+QwQAABwEAAAcdGNhZgIAAAAPU2tDbGVhclhmZXJtb2RlDVNrU3JjWGZlcm1vZGVjZnB0AQAAAAEBBUFyaWFsBApBcmlhbCBCb2xkBgxBcmlhbC1Cb2xkTVT+\/wAAeWFyYfQAAAAgdG5wBAAAAAAAQEEAAIA\/AAAAAAAAgD8AAAAAAACAQAAAAP8CMAAAAAAAAAAAAAAAAAAAAQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQQAAgD8AAAAAAACAPwAAAAAAAIBA\/\/\/\/\/wIwAQAAAAAAAAAAAAAAAAACAAAABAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBAACAPwAAAAAAAIA\/AAAAAAAAgED+uYKZADABAAAAAAAAAIBBAACAPwAAAAAAAIA\/AAAAAAAAgEAzMzP\/ARCBAAMAAAABAAAAIGZvZQ==' // @suppress longLineCheck
+    }
+  },
+  'id': '0x7d229bc0'
+};
diff --git a/trace-viewer/trace_viewer/extras/cc/raster_task.html b/trace-viewer/trace_viewer/extras/cc/raster_task.html
new file mode 100644
index 0000000..c93e27c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/raster_task.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="/extras/cc/tile.html">
+<link rel="import" href="/extras/cc/tile_view.html">
+<link rel="import" href="/extras/cc/layer_tree_host_impl_view.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+
+  var knownRasterTaskNames = [
+      'TileManager::RunRasterTask',
+      'RasterWorkerPoolTaskImpl::RunRasterOnThread',
+      'RasterWorkerPoolTaskImpl::Raster',
+      'RasterTaskImpl::Raster',
+      'cc::RasterTask',
+      'RasterTask'
+  ];
+
+  var knownAnalysisTaskNames = [
+      'TileManager::RunAnalyzeTask',
+      'RasterWorkerPoolTaskImpl::RunAnalysisOnThread',
+      'RasterWorkerPoolTaskImpl::Analyze',
+      'RasterTaskImpl::Analyze',
+      'cc::AnalyzeTask',
+      'AnalyzeTask'
+  ];
+
+  function getTileFromRasterTaskSlice(slice) {
+    if (!(isSliceDoingRasterization(slice) || isSliceDoingAnalysis(slice)))
+      return undefined;
+
+    var tileData;
+    if (slice.args.data)
+      tileData = slice.args.data;
+    else
+      tileData = slice.args.tileData;
+    if (tileData === undefined)
+      return undefined;
+    if (tileData.tile_id)
+      return tileData.tile_id;
+
+    var tile = tileData.tileId;
+    if (!(tile instanceof tv.e.cc.TileSnapshot))
+      return undefined;
+    return tileData.tileId;
+  }
+
+  function isSliceDoingRasterization(slice) {
+    if (knownRasterTaskNames.indexOf(slice.title) !== -1)
+      return true;
+    return false;
+  }
+
+  function isSliceDoingAnalysis(slice) {
+    if (knownAnalysisTaskNames.indexOf(slice.title) !== -1)
+      return true;
+    return false;
+  }
+
+  return {
+    getTileFromRasterTaskSlice: getTileFromRasterTaskSlice,
+    isSliceDoingRasterization: isSliceDoingRasterization,
+    isSliceDoingAnalysis: isSliceDoingAnalysis
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/raster_task_selection.html b/trace-viewer/trace_viewer/extras/cc/raster_task_selection.html
new file mode 100644
index 0000000..9a8c83f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/raster_task_selection.html
@@ -0,0 +1,128 @@
+<!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="/extras/cc/raster_task.html">
+<link rel="import" href="/extras/cc/raster_task_view.html">
+<link rel="import" href="/extras/cc/selection.html">
+<link rel="import" href="/core/analysis/single_slice_sub_view.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /**
+   * @constructor
+   */
+  function RasterTaskSelection(selection) {
+    tv.e.cc.Selection.call(this);
+    var whySupported = RasterTaskSelection.whySuported(selection);
+    if (!whySupported.ok)
+      throw new Error('Fail: ' + whySupported.why);
+    this.slices_ = tv.b.asArray(selection);
+    this.tiles_ = this.slices_.map(function(slice) {
+      var tile = tv.e.cc.getTileFromRasterTaskSlice(slice);
+      if (tile === undefined)
+        throw new Error('This should never happen due to .supports check.');
+      return tile;
+    });
+  }
+  RasterTaskSelection.whySuported = function(selection) {
+    if (!(selection instanceof tv.c.Selection))
+      return {ok: false, why: 'Must be selection'};
+
+    if (selection.length === 0)
+      return {ok: false, why: 'Selection must be non empty'};
+
+    var tile0;
+    for (var i = 0; i < selection.length; i++) {
+      var event = selection[i];
+      if (!(event instanceof tv.c.trace_model.Slice))
+        return {ok: false, why: 'Not a slice'};
+
+      var tile = tv.e.cc.getTileFromRasterTaskSlice(selection[i]);
+      if (tile === undefined)
+        return {ok: false, why: 'No tile found'};
+
+      if (i === 0) {
+        tile0 = tile;
+      } else {
+        if (tile.containingSnapshot != tile0.containingSnapshot) {
+          return {
+            ok: false,
+            why: 'Raster tasks are from different compositor instances'
+          };
+        }
+      }
+    }
+    return {ok: true};
+  }
+
+  RasterTaskSelection.supports = function(selection) {
+    return RasterTaskSelection.whySuported(selection).ok;
+  };
+
+  RasterTaskSelection.prototype = {
+    __proto__: tv.e.cc.Selection.prototype,
+
+    get specicifity() {
+      return 3;
+    },
+
+    get associatedLayerId() {
+      var tile0 = this.tiles_[0];
+      var allSameLayer = this.tiles_.every(function(tile) {
+        tile.layerId == tile0.layerId;
+      });
+      if (allSameLayer)
+        return tile0.layerId;
+      return undefined;
+    },
+
+    get extraHighlightsByLayerId() {
+      var highlights = {};
+      this.tiles_.forEach(function(tile, i) {
+        if (highlights[tile.layerId] === undefined)
+          highlights[tile.layerId] = [];
+        var slice = this.slices_[i];
+        highlights[tile.layerId].push({
+          colorKey: slice.title,
+          rect: tile.layerRect
+        });
+      }, this);
+      return highlights;
+    },
+
+    createAnalysis: function() {
+      var sel = new tv.c.Selection();
+      this.slices_.forEach(function(slice) {
+        sel.push(slice);
+      });
+
+      var analysis;
+      if (sel.length == 1)
+        analysis = document.createElement('tv-c-single-slice-sub-view');
+      else
+        analysis = new RasterTaskView();
+      analysis.selection = sel;
+      return analysis;
+    },
+
+    findEquivalent: function(lthi) {
+      // Raster tasks are only valid in one LTHI.
+      return undefined;
+    },
+
+    // RasterTaskSelection specific stuff follows.
+    get containingSnapshot() {
+      return this.tiles_[0].containingSnapshot;
+    }
+  };
+
+  return {
+    RasterTaskSelection: RasterTaskSelection
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/raster_task_selection_test.html b/trace-viewer/trace_viewer/extras/cc/raster_task_selection_test.html
new file mode 100644
index 0000000..a9181cd
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/raster_task_selection_test.html
@@ -0,0 +1,33 @@
+<!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="/extras/cc/raster_task_selection.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('basic', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = m.processes[1];
+    var rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) {
+      return slice.title == 'RasterTask';
+    });
+
+    var selection = new tv.c.Selection();
+    selection.push(rasterTasks[0]);
+    selection.push(rasterTasks[1]);
+
+    assert.isTrue(tv.e.cc.RasterTaskSelection.supports(selection));
+    var selection = new tv.e.cc.RasterTaskSelection(selection);
+    var highlights = selection.extraHighlightsByLayerId;
+    assert.equal(tv.b.dictionaryLength(highlights), 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/raster_task_view.html b/trace-viewer/trace_viewer/extras/cc/raster_task_view.html
new file mode 100644
index 0000000..4bdcdf4
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/raster_task_view.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="/extras/cc/raster_task.html">
+<link rel="import" href="/extras/cc/selection.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/util.html">
+<link rel="import" href="/base/ui/sortable_table.html">
+
+<polymer-element name="tv-e-cc-raster-task-view"
+    constructor="RasterTaskView">
+  <script>
+  'use strict';
+  Polymer({
+    created: function() {
+      this.selection_ = undefined;
+    },
+
+    set selection(selection) {
+      this.selection_ = selection;
+
+      this.updateContents_();
+    },
+
+    updateContents_: function() {
+      this.textContent = '';
+
+      if (this.selection_.length === 0)
+        return;
+
+      var results = new tv.c.analysis.AnalysisResults();
+      this.appendChild(results);
+
+      var headerDiv = results.appendHeader('Rasterization costs in ');
+
+      // LTHI link.
+      var lthi = tv.e.cc.getTileFromRasterTaskSlice(
+          this.selection_[0]).containingSnapshot;
+      headerDiv.appendChild(results.createSelectionChangingLink(
+        lthi.userFriendlyName,
+        function() {
+          return new tv.c.Selection([lthi]);
+        }));
+
+      // Get costs by layer.
+      var costsByLayerId = {};
+      function getCurrentCostsForLayerId(tile) {
+        var layerId = tile.layerId;
+        var lthi = tile.containingSnapshot;
+        var layer;
+        if (lthi.activeTree)
+          layer = lthi.activeTree.findLayerWithId(layerId);
+        if (layer === undefined && lthi.pendingTree)
+          layer = lthi.pendingTree.findLayerWithId(layerId);
+        if (costsByLayerId[layerId] === undefined) {
+          costsByLayerId[layerId] = {
+            layerId: layerId,
+            layer: layer,
+            numTiles: 0,
+            numAnalysisTasks: 0,
+            numRasterTasks: 0,
+            duration: 0,
+            cpuDuration: 0
+          };
+        }
+        return costsByLayerId[layerId];
+      }
+
+      var totalDuration = 0;
+      var totalCpuDuration = 0;
+      var totalNumAnalyzeTasks = 0;
+      var totalNumRasterizeTasks = 0;
+      var hadCpuDurations = false;
+
+      var tilesThatWeHaveSeen = {};
+
+      this.selection_.forEach(function(slice) {
+        var tile = tv.e.cc.getTileFromRasterTaskSlice(slice);
+        var curCosts = getCurrentCostsForLayerId(tile);
+
+        if (!tilesThatWeHaveSeen[tile.objectInstance.id]) {
+          tilesThatWeHaveSeen[tile.objectInstance.id] = true;
+          curCosts.numTiles += 1;
+        }
+
+        if (tv.e.cc.isSliceDoingAnalysis(slice)) {
+          curCosts.numAnalysisTasks += 1;
+          totalNumAnalyzeTasks += 1;
+        } else {
+          curCosts.numRasterTasks += 1;
+          totalNumRasterizeTasks += 1;
+        }
+        curCosts.duration += slice.duration;
+        totalDuration += slice.duration;
+        if (slice.cpuDuration !== undefined) {
+          curCosts.cpuDuration += slice.cpuDuration;
+          totalCpuDuration += slice.cpuDuration;
+          hadCpuDurations = true;
+        }
+      });
+
+      // Initial sort.
+      var costs = tv.b.dictionaryValues(costsByLayerId);
+      costs.sort(function(x, y) {
+        if (hadCpuDurations)
+          return y.cpuDuration - x.cpuDuration;
+        return y.duration - x.duration;
+      });
+
+      // Output.
+      var table = results.appendTable(
+          'analyze-rasterizatin-costs-table', hadCpuDurations ? 6 : 5);
+      var hrow = results.appendHeadRow(table);
+      results.appendTableCell(table, hrow, 'Layer');
+      results.appendTableCell(table, hrow, 'Num Tiles');
+      results.appendTableCell(table, hrow, 'Num Analysis Tasks');
+      results.appendTableCell(table, hrow, 'Num Rasterized Tiles');
+      results.appendTableCell(table, hrow, 'Wall Duration (ms)');
+      if (hadCpuDurations)
+        results.appendTableCell(table, hrow, 'CPU Duration (ms)');
+
+
+      // Body.
+      costs.forEach(function(costs) {
+        var layerId = costs.layerId;
+        var row = results.appendBodyRow(table);
+        var layerEl = results.appendTableCell(table, row, 'Layer ' + layerId);
+        if (costs.layer) {
+          layerEl.textContent = '';
+          layerEl.appendChild(results.createSelectionChangingLink(
+              'Layer ' + layerId,
+              function() {
+                return new tv.e.cc.LayerSelection(costs.layer);
+              }));
+        }
+        results.appendTableCell(table, row, costs.numTiles);
+        results.appendTableCell(table, row, costs.numAnalysisTasks);
+        results.appendTableCell(table, row, costs.numRasterTasks);
+        results.appendTableCell(
+            table, row, tv.c.analysis.tsString(costs.duration));
+        if (hadCpuDurations) {
+          results.appendTableCell(
+              table, row, tv.c.analysis.tsString(costs.cpuDuration));
+        }
+      });
+
+      // Footer.
+      var frow = results.appendFootRow(table);
+      results.appendTableCell(table, frow, 'Totals');
+      results.appendTableCell(table, frow, tv.b.dictionaryLength(
+          tilesThatWeHaveSeen));
+      results.appendTableCell(table, frow, totalNumAnalyzeTasks);
+      results.appendTableCell(table, frow, totalNumRasterizeTasks);
+      results.appendTableCell(
+          table, frow, tv.c.analysis.tsString(totalDuration));
+      if (hadCpuDurations) {
+        results.appendTableCell(
+            table, frow, tv.c.analysis.tsString(totalCpuDuration));
+      }
+
+      tv.b.ui.SortableTable.decorate(table);
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/cc/raster_task_view_test.html b/trace-viewer/trace_viewer/extras/cc/raster_task_view_test.html
new file mode 100644
index 0000000..5f4caa9
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/raster_task_view_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="/base/utils.html">
+<link rel="import" href="/core/analysis/analysis_view.html">
+<link rel="import" href="/extras/cc/raster_task_view.html">
+<link rel="import" href="/extras/cc/raster_task_selection.html">
+<link rel="import" href="/extras/cc/layer_tree_host_impl_view.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function createSelection() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = m.processes[1];
+    var rasterTasks = p.threads[1].sliceGroup.slices.filter(function(slice) {
+      return slice.title == 'RasterTask' || slice.title == 'AnalyzeTask';
+    });
+
+    var selection = new tv.c.Selection();
+    selection.push(rasterTasks[0]);
+    selection.push(rasterTasks[1]);
+    return selection;
+  }
+
+  test('basic', function() {
+    var selection = createSelection();
+    var view = new RasterTaskView();
+    view.selection = selection;
+    this.addHTMLOutput(view);
+  });
+
+  test('analysisViewIntegration', function() {
+    var selection = createSelection();
+    var view = new TracingAnalysisView();
+    view.selection = selection;
+    assert.isDefined(view.querySelector('RasterTaskView'));
+    this.addHTMLOutput(view);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/region.html b/trace-viewer/trace_viewer/extras/cc/region.html
new file mode 100644
index 0000000..1683ee9
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/region.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/rect.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /**
+   * @constructor
+   */
+  function Region() {
+    this.rects = [];
+  }
+
+  Region.fromArray = function(array) {
+    if (array.length % 4 != 0)
+      throw new Error('Array must consist be a multiple of 4 in length');
+
+    var r = new Region();
+    for (var i = 0; i < array.length; i += 4) {
+      r.rects.push(tv.b.Rect.fromXYWH(array[i], array[i + 1],
+                                      array[i + 2], array[i + 3]));
+    }
+    return r;
+  }
+
+  /**
+   * @return {Region} If array is undefined, returns an empty region. Otherwise
+   * returns Region.fromArray(array).
+   */
+  Region.fromArrayOrUndefined = function(array) {
+    if (array === undefined)
+      return new Region();
+    return Region.fromArray(array);
+  };
+
+  Region.prototype = {
+    __proto__: Region.prototype,
+
+    rectIntersects: function(r) {
+      for (var i = 0; i < this.rects.length; i++) {
+        if (this.rects[i].intersects(r))
+          return true;
+      }
+      return false;
+    },
+
+    addRect: function(r) {
+      this.rects.push(r);
+    }
+  };
+
+  return {
+    Region: Region
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/render_pass.html b/trace-viewer/trace_viewer/extras/cc/render_pass.html
new file mode 100644
index 0000000..4158d54
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/render_pass.html
@@ -0,0 +1,44 @@
+<!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/cc/util.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function RenderPassSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  RenderPassSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+    },
+
+    initialize: function() {
+      tv.e.cc.moveRequiredFieldsFromArgsToToplevel(
+          this, ['quadList']);
+    }
+  };
+
+  ObjectSnapshot.register(RenderPassSnapshot, {typeName: 'cc::RenderPass'});
+
+  return {
+    RenderPassSnapshot: RenderPassSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/selection.html b/trace-viewer/trace_viewer/extras/cc/selection.html
new file mode 100644
index 0000000..7035309
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/selection.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="/core/analysis/generic_object_view.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  function Selection() {
+    this.selectionToSetIfClicked = undefined;
+  };
+  Selection.prototype = {
+    /**
+     * When two things are picked in the UI, one must occasionally tie-break
+     * between them to decide what was really clicked. Things with higher
+     * specicifity will win.
+     */
+    get specicifity() {
+      throw new Error('Not implemented');
+    },
+
+    /**
+     * If a selection is related to a specific layer, then this returns the
+     * layerId of that layer. If the selection is not related to a layer, for
+     * example if the device viewport is selected, then this returns undefined.
+     */
+    get associatedLayerId() {
+      throw new Error('Not implemented');
+    },
+
+    /**
+     * If a selection is related to a specific render pass, then this returns
+     * the layerId of that layer. If the selection is not related to a layer,
+     * for example if the device viewport is selected, then this returns
+     * undefined.
+     */
+    get associatedRenderPassId() {
+      throw new Error('Not implemented');
+    },
+
+
+    get highlightsByLayerId() {
+      return {};
+    },
+
+    /**
+     * Called when the selection is made active in the layer view. Must return
+     * an HTMLElement that explains this selection in detail.
+     */
+    createAnalysis: function() {
+      throw new Error('Not implemented');
+    },
+
+    /**
+     * Should try to create the equivalent selection in the provided LTHI,
+     * or undefined if it can't be done.
+     */
+    findEquivalent: function(lthi) {
+      throw new Error('Not implemented');
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function RenderPassSelection(renderPass, renderPassId) {
+    if (!renderPass || (renderPassId === undefined))
+      throw new Error('Render pass (with id) is required');
+    this.renderPass_ = renderPass;
+    this.renderPassId_ = renderPassId;
+  }
+
+  RenderPassSelection.prototype = {
+    __proto__: Selection.prototype,
+
+    get specicifity() {
+      return 1;
+    },
+
+    get associatedLayerId() {
+      return undefined;
+    },
+
+    get associatedRenderPassId() {
+      return this.renderPassId_;
+    },
+
+    get renderPass() {
+      return this.renderPass_;
+    },
+
+    createAnalysis: function() {
+      var dataView = document.createElement(
+          'tv-c-analysis-generic-object-view-with-label');
+      dataView.label = 'RenderPass ' + this.renderPassId_;
+      dataView.object = this.renderPass_.args;
+      return dataView;
+    },
+
+    get title() {
+      return this.renderPass_.objectInstance.typeName;
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function LayerSelection(layer) {
+    if (!layer)
+      throw new Error('Layer is required');
+    this.layer_ = layer;
+  }
+
+  LayerSelection.prototype = {
+    __proto__: Selection.prototype,
+
+    get specicifity() {
+      return 1;
+    },
+
+    get associatedLayerId() {
+      return this.layer_.layerId;
+    },
+
+    get associatedRenderPassId() {
+      return undefined;
+    },
+
+    get layer() {
+      return this.layer_;
+    },
+
+    createAnalysis: function() {
+      var dataView = document.createElement(
+          'tv-c-analysis-generic-object-view-with-label');
+      dataView.label = 'Layer ' + this.layer_.layerId;
+      if (this.layer_.usingGpuRasterization)
+        dataView.label += ' (GPU-rasterized)';
+      dataView.object = this.layer_.args;
+      return dataView;
+    },
+
+    get title() {
+      return this.layer_.objectInstance.typeName;
+    },
+
+    findEquivalent: function(lthi) {
+      var layer = lthi.activeTree.findLayerWithId(this.layer_.layerId) ||
+          lthi.pendingTree.findLayerWithId(this.layer_.layerId);
+      if (!layer)
+        return undefined;
+      return new LayerSelection(layer);
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function TileSelection(tile, opt_data) {
+    this.tile_ = tile;
+    this.data_ = opt_data || {};
+  }
+
+  TileSelection.prototype = {
+    __proto__: Selection.prototype,
+
+    get specicifity() {
+      return 2;
+    },
+
+    get associatedLayerId() {
+      return this.tile_.layerId;
+    },
+
+    get highlightsByLayerId() {
+      var highlights = {};
+      highlights[this.tile_.layerId] = [
+        {
+          colorKey: this.tile_.objectInstance.typeName,
+          rect: this.tile_.layerRect
+        }
+      ];
+      return highlights;
+    },
+
+    createAnalysis: function() {
+      var analysis = document.createElement(
+          'tv-c-analysis-generic-object-view-with-label');
+      analysis.label = 'Tile ' + this.tile_.objectInstance.id + ' on layer ' +
+          this.tile_.layerId;
+      if (this.data_) {
+        analysis.object = {
+          moreInfo: this.data_,
+          tileArgs: this.tile_.args
+        };
+      } else {
+        analysis.object = this.tile_.args;
+      }
+      return analysis;
+    },
+
+    findEquivalent: function(lthi) {
+      var tileInstance = this.tile_.tileInstance;
+      if (lthi.ts < tileInstance.creationTs ||
+          lthi.ts >= tileInstance.deletionTs)
+        return undefined;
+      var tileSnapshot = tileInstance.getSnapshotAt(lthi.ts);
+      if (!tileSnapshot)
+        return undefined;
+      return new TileSelection(tileSnapshot);
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function LayerRectSelection(layer, rectType, rect, opt_data) {
+    this.layer_ = layer;
+    this.rectType_ = rectType;
+    this.rect_ = rect;
+    this.data_ = opt_data !== undefined ? opt_data : rect;
+  }
+
+  LayerRectSelection.prototype = {
+    __proto__: Selection.prototype,
+
+    get specicifity() {
+      return 2;
+    },
+
+    get associatedLayerId() {
+      return this.layer_.layerId;
+    },
+
+
+    get highlightsByLayerId() {
+      var highlights = {};
+      highlights[this.layer_.layerId] = [
+        {
+          colorKey: this.rectType_,
+          rect: this.rect_
+        }
+      ];
+      return highlights;
+    },
+
+    createAnalysis: function() {
+      var analysis = document.createElement(
+          'tv-c-analysis-generic-object-view-with-label');
+      analysis.label = this.rectType_ + ' on layer ' + this.layer_.layerId;
+      analysis.object = this.data_;
+      return analysis;
+    },
+
+    findEquivalent: function(lthi) {
+      return undefined;
+    }
+  };
+
+  /**
+   * @constructor
+   */
+  function AnimationRectSelection(layer, rect) {
+    this.layer_ = layer;
+    this.rect_ = rect;
+  }
+
+  AnimationRectSelection.prototype = {
+    __proto__: Selection.prototype,
+
+    get specicifity() {
+      return 0;
+    },
+
+    get associatedLayerId() {
+      return this.layer_.layerId;
+    },
+
+    createAnalysis: function() {
+      var analysis = document.createElement(
+          'tv-c-analysis-generic-object-view-with-label');
+      analysis.label = 'Animation Bounds of layer ' + this.layer_.layerId;
+      analysis.object = this.rect_;
+      return analysis;
+    }
+  };
+
+  return {
+    Selection: Selection,
+    RenderPassSelection: RenderPassSelection,
+    LayerSelection: LayerSelection,
+    TileSelection: TileSelection,
+    LayerRectSelection: LayerRectSelection,
+    AnimationRectSelection: AnimationRectSelection
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/tile.html b/trace-viewer/trace_viewer/extras/cc/tile.html
new file mode 100644
index 0000000..3dc021b
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/tile.html
@@ -0,0 +1,88 @@
+<!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="/extras/cc/util.html">
+<link rel="import" href="/extras/cc/debug_colors.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function TileSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  TileSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+    },
+
+    initialize: function() {
+      tv.e.cc.moveOptionalFieldsFromArgsToToplevel(
+          this, ['layerId', 'contentsScale', 'contentRect']);
+      if (this.args.managedState) {
+        this.resolution = this.args.managedState.resolution;
+        this.isSolidColor = this.args.managedState.isSolidColor;
+        this.isUsingGpuMemory = this.args.managedState.isUsingGpuMemory;
+        this.hasResource = this.args.managedState.hasResource;
+        this.scheduledPriority = this.args.scheduledPriority;
+        this.gpuMemoryUsageInBytes = this.args.gpuMemoryUsage;
+      } else {
+        this.resolution = this.args.resolution;
+        this.isSolidColor = this.args.drawInfo.isSolidColor;
+        this.isUsingGpuMemory = this.args.isUsingGpuMemory;
+        this.hasResource = this.args.hasResource;
+        this.scheduledPriority = this.args.scheduledPriority;
+        this.gpuMemoryUsageInBytes = this.args.gpuMemoryUsage;
+      }
+
+      // This check is for backward compatability. It can probably
+      // be removed once we're confident that most traces contain
+      // content_rect.
+      if (this.contentRect)
+        this.layerRect = this.contentRect.scale(1.0 / this.contentsScale);
+
+      if (this.isSolidColor)
+        this.type_ = tv.e.cc.tileTypes.solidColor;
+      else if (!this.hasResource)
+        this.type_ = tv.e.cc.tileTypes.missing;
+      else if (this.resolution === 'HIGH_RESOLUTION')
+        this.type_ = tv.e.cc.tileTypes.highRes;
+      else if (this.resolution === 'LOW_RESOLUTION')
+        this.type_ = tv.e.cc.tileTypes.lowRes;
+      else
+        this.type_ = tv.e.cc.tileTypes.unknown;
+    },
+
+    getTypeForLayer: function(layer) {
+      var type = this.type_;
+      if (type == tv.e.cc.tileTypes.unknown) {
+        if (this.contentsScale < layer.idealContentsScale)
+          type = tv.e.cc.tileTypes.extraLowRes;
+        else if (this.contentsScale > layer.idealContentsScale)
+          type = tv.e.cc.tileTypes.extraHighRes;
+      }
+      return type;
+    }
+  };
+
+  ObjectSnapshot.register(TileSnapshot, {typeName: 'cc::Tile'});
+
+  return {
+    TileSnapshot: TileSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/tile_coverage_rect.html b/trace-viewer/trace_viewer/extras/cc/tile_coverage_rect.html
new file mode 100644
index 0000000..c951cde
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/tile_coverage_rect.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';
+
+tv.exportTo('tv.e.cc', function() {
+  /**
+   * This class represents a tile (from impl side) and its final rect on the
+   * layer. Note that the rect is determined by what is needed to cover all
+   * of the layer without overlap.
+   * @constructor
+   */
+  function TileCoverageRect(rect, tile) {
+    this.geometryRect = rect;
+    this.tile = tile;
+  }
+
+  return {
+    TileCoverageRect: TileCoverageRect
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/tile_test.html b/trace-viewer/trace_viewer/extras/cc/tile_test.html
new file mode 100644
index 0000000..67662e9
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/tile_test.html
@@ -0,0 +1,29 @@
+<!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/cc/tile.html">
+<link rel="import" href="/extras/cc/tile_view.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/cc/layer_tree_host_impl_test_data.js"></script>
+
+<script>
+
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('basic', function() {
+    var m = new tv.c.TraceModel(g_catLTHIEvents);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+    var instance = p.objects.getAllInstancesNamed('cc::Tile')[0];
+    var snapshot = instance.snapshots[0];
+
+    assert.instanceOf(snapshot, tv.e.cc.TileSnapshot);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/tile_view.html b/trace-viewer/trace_viewer/extras/cc/tile_view.html
new file mode 100644
index 0000000..46fd92c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/tile_view.html
@@ -0,0 +1,57 @@
+<!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/cc/tile.html">
+<link rel="import" href="/core/analysis/generic_object_view.html">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  /*
+   * Displays a tile in a human readable form.
+   * @constructor
+   */
+  var TileSnapshotView = tv.b.ui.define(
+      'tile-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  TileSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('tile-snapshot-view');
+      this.layerTreeView_ = new tv.e.cc.LayerTreeHostImplSnapshotView();
+      this.appendChild(this.layerTreeView_);
+    },
+
+    updateContents: function() {
+      var tile = this.objectSnapshot_;
+      var layerTreeHostImpl = tile.containingSnapshot;
+      if (!layerTreeHostImpl)
+        return;
+
+      this.layerTreeView_.objectSnapshot = layerTreeHostImpl;
+      this.layerTreeView_.selection = new tv.e.cc.TileSelection(tile);
+    }
+  };
+
+  tv.c.analysis.ObjectSnapshotView.register(
+      TileSnapshotView,
+      {
+        typeName: 'cc::Tile',
+        showInTrackView: false
+      });
+
+  return {
+    TileSnapshotView: TileSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/util.html b/trace-viewer/trace_viewer/extras/cc/util.html
new file mode 100644
index 0000000..a743522
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/util.html
@@ -0,0 +1,153 @@
+<!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/quad.html">
+<link rel="import" href="/base/rect.html">
+<link rel="import" href="/core/trace_model/object_instance.html">
+<script>
+
+'use strict';
+
+tv.exportTo('tv.e.cc', function() {
+  var convertedNameCache = {};
+  function convertNameToJSConvention(name) {
+    if (name in convertedNameCache)
+      return convertedNameCache[name];
+
+    if (name[0] == '_' ||
+        name[name.length - 1] == '_') {
+      convertedNameCache[name] = name;
+      return name;
+    }
+
+    var words = name.split('_');
+    if (words.length == 1) {
+      convertedNameCache[name] = words[0];
+      return words[0];
+    }
+
+    for (var i = 1; i < words.length; i++)
+      words[i] = words[i][0].toUpperCase() + words[i].substring(1);
+
+    convertedNameCache[name] = words.join('');
+    return convertedNameCache[name];
+  }
+
+  function convertObjectFieldNamesToJSConventions(object) {
+    tv.b.iterObjectFieldsRecursively(
+        object,
+        function(object, fieldName, fieldValue) {
+          delete object[fieldName];
+          object[newFieldName] = fieldValue;
+          return newFieldName;
+        });
+  }
+
+  function convertQuadSuffixedTypesToQuads(object) {
+    tv.b.iterObjectFieldsRecursively(
+        object,
+        function(object, fieldName, fieldValue) {
+        });
+  }
+
+  function convertObject(object) {
+    convertObjectFieldNamesToJSConventions(object);
+    convertQuadSuffixedTypesToQuads(object);
+  }
+
+  function moveRequiredFieldsFromArgsToToplevel(object, fields) {
+    for (var i = 0; i < fields.length; i++) {
+      var key = fields[i];
+      if (object.args[key] === undefined)
+        throw Error('Expected field ' + key + ' not found in args');
+      if (object[key] !== undefined)
+        throw Error('Field ' + key + ' already in object');
+      object[key] = object.args[key];
+      delete object.args[key];
+    }
+  }
+
+  function moveOptionalFieldsFromArgsToToplevel(object, fields) {
+    for (var i = 0; i < fields.length; i++) {
+      var key = fields[i];
+      if (object.args[key] === undefined)
+        continue;
+      if (object[key] !== undefined)
+        throw Error('Field ' + key + ' already in object');
+      object[key] = object.args[key];
+      delete object.args[key];
+    }
+  }
+
+  function preInitializeObject(object) {
+    preInitializeObjectInner(object.args, false);
+  }
+
+  function preInitializeObjectInner(object, hasRecursed) {
+    if (!(object instanceof Object))
+      return;
+
+    if (object instanceof Array) {
+      for (var i = 0; i < object.length; i++)
+        preInitializeObjectInner(object[i], true);
+      return;
+    }
+
+    if (hasRecursed &&
+        (object instanceof tv.c.trace_model.ObjectSnapshot ||
+         object instanceof tv.c.trace_model.ObjectInstance))
+      return;
+
+    for (var key in object) {
+      var newKey = convertNameToJSConvention(key);
+      if (newKey != key) {
+        var value = object[key];
+        delete object[key];
+        object[newKey] = value;
+        key = newKey;
+      }
+
+      // Convert objects with keys ending with Quad to tv.b.Quad type.
+      if (/Quad$/.test(key) && !(object[key] instanceof tv.b.Quad)) {
+        var q;
+        try {
+          q = tv.b.Quad.from8Array(object[key]);
+        } catch (e) {
+          console.log(e);
+        }
+        object[key] = q;
+        continue;
+      }
+
+      // Convert objects with keys ending with Rect to tv.b.Rect type.
+      if (/Rect$/.test(key) && !(object[key] instanceof tv.b.Rect)) {
+        var r;
+        try {
+          r = tv.b.Rect.fromArray(object[key]);
+        } catch (e) {
+          console.log(e);
+        }
+        object[key] = r;
+      }
+
+      preInitializeObjectInner(object[key], true);
+    }
+  }
+
+  function bytesToRoundedMegabytes(bytes) {
+    return Math.round(bytes / 100000.0) / 10.0;
+  }
+
+  return {
+    preInitializeObject: preInitializeObject,
+    convertNameToJSConvention: convertNameToJSConvention,
+    moveRequiredFieldsFromArgsToToplevel: moveRequiredFieldsFromArgsToToplevel,
+    moveOptionalFieldsFromArgsToToplevel: moveOptionalFieldsFromArgsToToplevel,
+    bytesToRoundedMegabytes: bytesToRoundedMegabytes
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/cc/util_test.html b/trace-viewer/trace_viewer/extras/cc/util_test.html
new file mode 100644
index 0000000..bce1341
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/cc/util_test.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="/extras/cc/util.html">
+<link rel="import" href="/base/quad.html">
+<link rel="import" href="/base/rect.html">
+
+<script>
+
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('nameConvert', function() {
+    assert.equal(tv.e.cc.convertNameToJSConvention('_foo'), '_foo');
+    assert.equal(tv.e.cc.convertNameToJSConvention('foo_'), 'foo_');
+    assert.equal(tv.e.cc.convertNameToJSConvention('foo'), 'foo');
+    assert.equal(tv.e.cc.convertNameToJSConvention('foo_bar'), 'fooBar');
+    assert.equal(tv.e.cc.convertNameToJSConvention('foo_bar_baz'),
+                 'fooBarBaz');
+  });
+
+  test('objectConvertNested', function() {
+    var object = {
+      un_disturbed: true,
+      args: {
+        foo_bar: {
+          a_field: 7
+        }
+      }
+    };
+    var expected = {
+      un_disturbed: true,
+      args: {
+        fooBar: {
+          aField: 7
+        }
+      }
+    };
+    tv.e.cc.preInitializeObject(object);
+    assert.deepEqual(object, expected);
+  });
+
+  test('arrayConvert', function() {
+    var object = {
+      un_disturbed: true,
+      args: [
+        {foo_bar: 7},
+        {foo_bar: 8}
+      ]
+    };
+    var expected = {
+      un_disturbed: true,
+      args: [
+        {fooBar: 7},
+        {fooBar: 8}
+      ]
+    };
+    tv.e.cc.preInitializeObject(object);
+    assert.deepEqual(object, expected);
+  });
+
+  test('quadCoversion', function() {
+    var object = {
+      args: {
+        some_quad: [1, 2, 3, 4, 5, 6, 7, 8]
+      }
+    };
+    tv.e.cc.preInitializeObject(object);
+    assert.instanceOf(object.args.someQuad, tv.b.Quad);
+  });
+
+  test('quadConversionNested', function() {
+    var object = {
+      args: {
+        nested_field: {
+          a_quad: [1, 2, 3, 4, 5, 6, 7, 8]
+        },
+        non_nested_quad: [1, 2, 3, 4, 5, 6, 7, 8]
+      }
+    };
+    tv.e.cc.preInitializeObject(object);
+    assert.instanceOf(object.args.nestedField.aQuad, tv.b.Quad);
+    assert.instanceOf(object.args.nonNestedQuad, tv.b.Quad);
+  });
+
+  test('rectCoversion', function() {
+    var object = {
+      args: {
+        some_rect: [1, 2, 3, 4]
+      }
+    };
+    tv.e.cc.preInitializeObject(object);
+    assert.instanceOf(object.args.someRect, tv.b.Rect);
+  });
+
+  test('rectCoversionNested', function() {
+    var object = {
+      args: {
+        nested_field: {
+          a_rect: [1, 2, 3, 4]
+        },
+        non_nested_rect: [1, 2, 3, 4]
+      }
+    };
+    tv.e.cc.preInitializeObject(object);
+    assert.instanceOf(object.args.nestedField.aRect, tv.b.Rect);
+    assert.instanceOf(object.args.nonNestedRect, tv.b.Rect);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/chrome_config.html b/trace-viewer/trace_viewer/extras/chrome_config.html
new file mode 100644
index 0000000..eb443f1
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/chrome_config.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.
+-->
+
+<!--
+The chrome config is heavily used:
+  - chrome://tracing,
+  - trace2html, which in turn implies
+    - adb_profile_chrome
+    - telemetry
+-->
+
+<!-- Chrome also supports systrace & lean config -->
+<link rel="import" href="/extras/systrace_config.html">
+<link rel="import" href="/extras/lean_config.html">
+
+<!-- General importers -->
+<link rel="import" href="/extras/importer/gzip_importer.html">
+<link rel="import" href="/extras/importer/zip_importer.html">
+
+<!--- Domain specific importers -->
+<link rel="import" href="/extras/importer/v8/v8_log_importer.html">
+<link rel="import" href="/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/extras/importer/trace2html_importer.html">
+
+<!-- Lots of chrome-specific extras -->
+<link rel="import" href="/extras/cc/cc.html">
+<link rel="import" href="/extras/tcmalloc/tcmalloc.html">
+<link rel="import" href="/extras/net/net.html">
+<link rel="import" href="/extras/system_stats/system_stats.html">
+<link rel="import" href="/extras/gpu/gpu.html">
+
+<!-- Side panels are chrome-only for now -->
+<link rel="import" href="/extras/side_panel/input_latency.html">
+<link rel="import" href="/extras/side_panel/time_summary.html">
+
+<!-- Sample analysis is chrome-only because it brings in D3 which is HUGE -->
+<link rel="import" href="/extras/analysis/sampling_summary.html">
+
+<!-- Auditors are fun -->
+<link rel="import" href="/extras/audits/chrome_auditor.html">
+<link rel="import" href="/extras/audits/android_auditor.html">
diff --git a/trace-viewer/trace_viewer/extras/full_config.html b/trace-viewer/trace_viewer/extras/full_config.html
new file mode 100644
index 0000000..cb7b716
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/full_config.html
@@ -0,0 +1,11 @@
+<!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.
+-->
+
+<!-- The full config is all the configs slammed together. -->
+<link rel="import" href="/extras/chrome_config.html">
+<link rel="import" href="/extras/systrace_config.html">
+<link rel="import" href="/extras/lean_config.html">
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/extras/gpu/gpu.html b/trace-viewer/trace_viewer/extras/gpu/gpu.html
new file mode 100644
index 0000000..ded4a48
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/gpu.html
@@ -0,0 +1,9 @@
+<!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/gpu/state.html">
+<link rel="import" href="/extras/gpu/state_view.html">
diff --git a/trace-viewer/trace_viewer/extras/gpu/images/checkerboard.png b/trace-viewer/trace_viewer/extras/gpu/images/checkerboard.png
new file mode 100644
index 0000000..8ea9bc7
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/images/checkerboard.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/extras/gpu/state.html b/trace-viewer/trace_viewer/extras/gpu/state.html
new file mode 100644
index 0000000..e32555c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/state.html
@@ -0,0 +1,52 @@
+<!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/trace_model/object_instance.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.gpu', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function StateSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  StateSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      this.screenshot_ = undefined;
+    },
+
+    initialize: function() {
+      if (this.args.screenshot)
+        this.screenshot_ = this.args.screenshot;
+    },
+
+    /**
+     * @return {String} a base64 encoded screenshot if available.
+     */
+    get screenshot() {
+      return this.screenshot_;
+    }
+  };
+
+  ObjectSnapshot.register(
+    StateSnapshot,
+    {typeName: 'gpu::State'});
+
+  return {
+    StateSnapshot: StateSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/gpu/state_test.html b/trace-viewer/trace_viewer/extras/gpu/state_test.html
new file mode 100644
index 0000000..f43ca56
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/state_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="/extras/gpu/gpu.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script src="/extras/gpu/state_test_data.js"></script>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('basic', function() {
+    var m = new tv.c.TraceModel(g_gpu_state_trace);
+    var p = tv.b.dictionaryValues(m.processes)[0];
+
+    var instance = p.objects.getAllInstancesNamed('gpu::State')[0];
+    var snapshot = instance.snapshots[0];
+
+    assert.instanceOf(snapshot, tv.e.gpu.StateSnapshot);
+    assert.typeOf(snapshot.screenshot, 'string');
+    instance.wasDeleted(150);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/gpu/state_test_data.js b/trace-viewer/trace_viewer/extras/gpu/state_test_data.js
new file mode 100644
index 0000000..39af04d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/state_test_data.js
@@ -0,0 +1,22 @@
+// 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.
+
+'use strict';
+
+var g_gpu_state_trace = [
+  {
+    'cat': 'disabled-by-default-gpu.debug',
+    'pid': 23969,
+    'tid': 1799,
+    'ts': 1427012847340,
+    'ph': 'O',
+    'name': 'gpu::State',
+    'args': {
+      'snapshot': {
+        'screenshot': 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90JCREbHlyxtxQAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAqUlEQVRIx+2WiwrAIAhF/f9vHmywQTMft8w2CJKIoPDozR50fmy0AcsAiMjs9UDOJgG4jwG8SMuUXoMAZcVYBmgPqDYNwLo3JHqcHufbRETZKireOSbDQAA+zgKE7lyiCQDtcQygS6PKYIp3vZ5MvgB0mhmQu8kcgAXE6bYBoZYFmPnhgg5n4En/h0RmvdmX3eKAMYZ3HtGD0+NU3w2BR795dPe/aANuuwDyW5SiCgNBiQAAAABJRU5ErkJggg==' // @suppress longLineCheck
+      }
+    },
+    'id': '0x7d229bc0'
+  }
+];
diff --git a/trace-viewer/trace_viewer/extras/gpu/state_view.css b/trace-viewer/trace_viewer/extras/gpu/state_view.css
new file mode 100644
index 0000000..1b3c2b3
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/state_view.css
@@ -0,0 +1,15 @@
+/* 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.
+ */
+
+.gpu-state-snapshot-view {
+  background: url('./images/checkerboard.png');
+  display: -webkit-flex;
+  overflow: auto;
+}
+
+.gpu-state-snapshot-view img {
+  display: block;
+  margin: 16px auto 16px auto;
+}
diff --git a/trace-viewer/trace_viewer/extras/gpu/state_view.html b/trace-viewer/trace_viewer/extras/gpu/state_view.html
new file mode 100644
index 0000000..ddc4407
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/gpu/state_view.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="stylesheet" href="/extras/gpu/state_view.css">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.gpu', function() {
+  /*
+   * Displays a GPU state snapshot in a human readable form.
+   * @constructor
+   */
+  var StateSnapshotView = tv.b.ui.define(
+      'gpu-state-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  StateSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('gpu-state-snapshot-view');
+      this.screenshotImage_ = document.createElement('img');
+      this.appendChild(this.screenshotImage_);
+    },
+
+    updateContents: function() {
+      if (this.objectSnapshot_ && this.objectSnapshot_.screenshot) {
+        this.screenshotImage_.src = 'data:image/png;base64,' +
+            this.objectSnapshot_.screenshot;
+      }
+    }
+  };
+  tv.c.analysis.ObjectSnapshotView.register(
+    StateSnapshotView,
+    {typeName: 'gpu::State'});
+
+  return {
+    StateSnapshotView: StateSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/highlighter/vsync_highlighter.html b/trace-viewer/trace_viewer/extras/highlighter/vsync_highlighter.html
new file mode 100644
index 0000000..4a4a050
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/highlighter/vsync_highlighter.html
@@ -0,0 +1,198 @@
+<!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/tracks/highlighter.html">
+<link rel="import" href="/core/timeline_track_view.html">
+<link rel="import" href="/core/timeline_viewport.html">
+<link rel="import" href="/core/tracks/trace_model_track.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Provides the VSyncHighlighter class.
+ */
+tv.exportTo('tv.e.highlighter', function() {
+
+  var Highlighter = tv.c.tracks.Highlighter;
+
+  /**
+   * Highlights VSync events on the model track (using "zebra" striping).
+   * @constructor
+   */
+  function VSyncHighlighter(viewport) {
+    Highlighter.call(this, viewport);
+    this.times_ = [];
+  }
+
+  VSyncHighlighter.VSYNC_HIGHLIGHT_COLOR = {r: 0, g: 0, b: 255};
+  VSyncHighlighter.VSYNC_HIGHLIGHT_ALPHA = 0.1;
+
+  VSyncHighlighter.VSYNC_DENSITY_TRANSPARENT = 0.20;
+  VSyncHighlighter.VSYNC_DENSITY_OPAQUE = 0.10;
+  VSyncHighlighter.VSYNC_DENSITY_RANGE =
+      VSyncHighlighter.VSYNC_DENSITY_TRANSPARENT -
+      VSyncHighlighter.VSYNC_DENSITY_OPAQUE;
+
+  VSyncHighlighter.VSYNC_COUNTER_PRECISIONS = {
+    // Android. Seems to appear in sample systrace only.
+    'android.VSYNC': 15
+  };
+
+  VSyncHighlighter.VSYNC_SLICE_PRECISIONS = {
+    // Android.
+    'RenderWidgetHostViewAndroid::OnVSync': 5,
+
+    // Android. Very precise. Requires "gfx" systrace category to be enabled.
+    'VSYNC': 10,
+
+    // Linux. Very precise. Requires "gpu" tracing category to be enabled.
+    'vblank': 10,
+
+    // Mac. Derived from a Mac callback (CVDisplayLinkSetOutputCallback).
+    'DisplayLinkMac::GetVSyncParameters': 5
+  };
+
+  /**
+   * Find the most precise VSync event times in a model.
+   */
+  VSyncHighlighter.findVSyncTimes = function(model) {
+    var times = [];
+
+    // Only keep the most precise VSync data.
+    var maxPrecision = Number.NEGATIVE_INFINITY;
+    var maxTitle = undefined;
+    var useInstead = function(title, precisions) {
+      if (title != maxTitle) {
+        var precision = precisions[title];
+        if (precision === undefined || precision <= maxPrecision) {
+          if (precision === maxPrecision) {
+            console.warn('Encountered two different VSync events (' +
+                maxTitle + ', ' + title + ') with the same precision, ' +
+                'ignoring the newer one (' + title + ')');
+          }
+          return false;
+        }
+        maxPrecision = precision;
+        maxTitle = title;
+        times = [];
+      }
+      return true;
+    }
+
+    for (var pid in model.processes) {
+      var process = model.processes[pid];
+
+      // Traverse process counters.
+      for (var cid in process.counters) {
+        if (useInstead(cid, VSyncHighlighter.VSYNC_COUNTER_PRECISIONS)) {
+          var counter = process.counters[cid];
+          for (var i = 0; i < counter.series.length; i++) {
+            var series = counter.series[i];
+            Array.prototype.push.apply(times, series.timestamps);
+          }
+        }
+      }
+
+      // Traverse thread slices.
+      for (var tid in process.threads) {
+        var thread = process.threads[tid];
+        for (var i = 0; i < thread.sliceGroup.slices.length; i++) {
+          var slice = thread.sliceGroup.slices[i];
+          if (useInstead(slice.title,
+                         VSyncHighlighter.VSYNC_SLICE_PRECISIONS)) {
+            times.push(slice.start);
+          }
+        }
+      }
+    }
+
+    times.sort(function(x, y) { return x - y; });
+    return times;
+  };
+
+  /**
+   * Generate a zebra striping from a list of times.
+   */
+  VSyncHighlighter.generateStripes = function(times, minTime, maxTime) {
+    var stripes = [];
+
+    // Find the lowest and highest index within the viewport.
+    var lowIndex = tv.b.findLowIndexInSortedArray(
+        times,
+        function(time) { return time; },
+        minTime);
+    if (lowIndex > times.length) {
+      lowIndex = times.length;
+    }
+    var highIndex = lowIndex - 1;
+    while (times[highIndex + 1] <= maxTime) {
+      highIndex++;
+    }
+
+    // Must start at an even index and end at an odd index.
+    for (var i = lowIndex - (lowIndex % 2); i <= highIndex; i += 2) {
+      var left = i < lowIndex ? minTime : times[i];
+      var right = i + 1 > highIndex ? maxTime : times[i + 1];
+      stripes.push([left, right]);
+    }
+
+    return stripes;
+  }
+
+  VSyncHighlighter.prototype = {
+    __proto__: Highlighter.prototype,
+
+    processModel: function(model) {
+      this.times_ = VSyncHighlighter.findVSyncTimes(model);
+    },
+
+    drawHighlight: function(ctx, dt, viewLWorld, viewRWorld, viewHeight) {
+      if (!this.viewport_.highlightVSync) {
+        return;
+      }
+
+      var stripes = VSyncHighlighter.generateStripes(
+          this.times_, viewLWorld, viewRWorld);
+      if (stripes.length == 0) {
+        return;
+      }
+
+      var stripeRange = stripes[stripes.length - 1][1] - stripes[0][0];
+      var stripeDensity = stripes.length / (dt.scaleX * stripeRange);
+      var clampedStripeDensity = tv.b.clamp(stripeDensity,
+          VSyncHighlighter.VSYNC_DENSITY_OPAQUE,
+          VSyncHighlighter.VSYNC_DENSITY_TRANSPARENT);
+      var opacity =
+          (VSyncHighlighter.VSYNC_DENSITY_TRANSPARENT - clampedStripeDensity) /
+          VSyncHighlighter.VSYNC_DENSITY_RANGE;
+      if (opacity == 0) {
+        return;
+      }
+
+      var pixelRatio = window.devicePixelRatio || 1;
+      var height = viewHeight * pixelRatio;
+      ctx.fillStyle = tv.b.ui.colorToRGBAString(
+          VSyncHighlighter.VSYNC_HIGHLIGHT_COLOR,
+          VSyncHighlighter.VSYNC_HIGHLIGHT_ALPHA * opacity);
+
+      for (var i = 0; i < stripes.length; i++) {
+        var xLeftView = dt.xWorldToView(stripes[i][0]);
+        var xRightView = dt.xWorldToView(stripes[i][1]);
+        ctx.fillRect(xLeftView, 0, xRightView - xLeftView, height);
+      }
+    }
+  };
+
+  // Register the highlighter.
+  tv.c.tracks.Highlighter.register(VSyncHighlighter);
+
+  return {
+    VSyncHighlighter: VSyncHighlighter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/highlighter/vsync_highlighter_test.html b/trace-viewer/trace_viewer/extras/highlighter/vsync_highlighter_test.html
new file mode 100644
index 0000000..dc55c54
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/highlighter/vsync_highlighter_test.html
@@ -0,0 +1,220 @@
+<!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/trace_model/thread.html">
+<link rel="import" href="/extras/highlighter/vsync_highlighter.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+  var VSyncHighlighter = tv.e.highlighter.VSyncHighlighter;
+
+  var VIEW_L_WORLD = 100;
+  var VIEW_R_WORLD = 1000;
+
+  function buildModel(slices) {
+    var model = new tv.c.TraceModel();
+    var process = model.getOrCreateProcess(1);
+    for (var i = 0; i < slices.length; i++) {
+      var thread = process.getOrCreateThread(i);
+      for (var j = 0; j < slices[i].length; j++) {
+        thread.sliceGroup.pushSlice(slices[i][j]);
+      }
+    }
+    return model;
+  }
+
+  function buildSlice(title, time) {
+    return new tv.c.trace_model.ThreadSlice('', title, 0, time, {});
+  }
+
+  function testFindVSyncTimes(slices, expectedTimes) {
+    assert.deepEqual(
+        expectedTimes, VSyncHighlighter.findVSyncTimes(buildModel(slices)));
+  }
+
+  function testGenerateStripes(times, expectedRanges) {
+    var ranges = VSyncHighlighter.generateStripes(
+        times, VIEW_L_WORLD, VIEW_R_WORLD);
+    ranges.sort();
+    expectedRanges.sort();
+
+    assert.equal(ranges.length, expectedRanges.length);
+    for (var i = 0; i < expectedRanges.length; i++) {
+      assert.deepEqual(ranges[i], expectedRanges[i]);
+    }
+  }
+
+  test('findEmpty', function() {
+    testFindVSyncTimes([], []);
+  });
+
+  test('findNoVsync', function() {
+    testFindVSyncTimes([
+        [buildSlice('MessageLoop::RunTask', 10),
+         buildSlice('MessageLoop::RunTask', 20)],
+        [buildSlice('MessageLoop::RunTask', 15)]
+    ], []);
+  });
+
+  test('findOneVsync', function() {
+    testFindVSyncTimes([[buildSlice('vblank', 42)]], [42]);
+  });
+
+  test('findMultipleVsyncs', function() {
+    testFindVSyncTimes([
+        [buildSlice('VSYNC', 1), buildSlice('MessageLoop::RunTask', 2)],
+        [buildSlice('MessageLoop::RunTask', 3)],
+        [buildSlice('MessageLoop::RunTask', 4), buildSlice('VSYNC', 5)],
+        [buildSlice('VSYNC', 6), buildSlice('VSYNC', 7)]
+    ], [1, 5, 6, 7]);
+  });
+
+  test('findUnsorted', function() {
+    testFindVSyncTimes([
+        [buildSlice('RenderWidgetHostViewAndroid::OnVSync', 4),
+         buildSlice('MessageLoop::RunTask', 2)],
+        [buildSlice('RenderWidgetHostViewAndroid::OnVSync', 1),
+         buildSlice('RenderWidgetHostViewAndroid::OnVSync', 3)]
+    ], [1, 3, 4]);
+  });
+
+  test('findDifferentPrecisions', function() {
+    // vblank has higher precision than RenderWidgetHostViewAndroid::OnVSync.
+    testFindVSyncTimes([
+        [buildSlice('RenderWidgetHostViewAndroid::OnVSync', 1),
+         buildSlice('vblank', 2),
+         buildSlice('RenderWidgetHostViewAndroid::OnVSync', 3)]
+    ], [2]);
+  });
+
+  test('generateInside', function() {
+    testGenerateStripes([], []);
+    testGenerateStripes([200, 500], [[200, 500]]);
+    testGenerateStripes([200, 500, 800, 900], [[200, 500], [800, 900]]);
+    testGenerateStripes(
+        [200, 500, 800, 900, 998, 999],
+        [[200, 500], [800, 900], [998, 999]]);
+  });
+
+  test('generateOutside', function() {
+    // Far left.
+    testGenerateStripes([0, 99], []);
+    testGenerateStripes([0, 10, 50, 99], []);
+    testGenerateStripes([0, 99, 101, 999], [[101, 999]]);
+    testGenerateStripes([0, 10, 50, 99, 101, 999], [[101, 999]]);
+
+    // Far right.
+    testGenerateStripes([1001, 2000], []);
+    testGenerateStripes([1001, 2000, 3000, 4000], []);
+    testGenerateStripes([101, 999, 1001, 2000], [[101, 999]]);
+    testGenerateStripes([101, 999, 1001, 2000, 3000, 4000], [[101, 999]]);
+
+    // Far both.
+    testGenerateStripes([0, 99, 1001, 2000], []);
+    testGenerateStripes([0, 10, 50, 99, 1001, 2000], []);
+    testGenerateStripes([0, 10, 50, 99, 1001, 2000, 3000, 4000], []);
+    testGenerateStripes([0, 99, 101, 999, 1001, 2000], [[101, 999]]);
+  });
+
+  test('generateOverlap', function() {
+    // Left overlap.
+    testGenerateStripes([0, 101], [[VIEW_L_WORLD, 101]]);
+    testGenerateStripes([0, 1, 2, 101], [[VIEW_L_WORLD, 101]]);
+    testGenerateStripes(
+        [2, 101, 102, 103],
+        [[VIEW_L_WORLD, 101], [102, 103]]);
+    testGenerateStripes(
+        [0, 1, 2, 101, 102, 103],
+        [[VIEW_L_WORLD, 101], [102, 103]]);
+    testGenerateStripes(
+        [0, 1, 2, 101, 102, 103, 1001, 3000],
+        [[VIEW_L_WORLD, 101], [102, 103]]);
+
+    // Right overlap.
+    testGenerateStripes([999, 2000], [[999, VIEW_R_WORLD]]);
+    testGenerateStripes([999, 2000, 3000, 4000], [[999, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [997, 998, 999, 2000],
+        [[997, 998], [999, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [997, 998, 999, 2000, 3000, 4000],
+        [[997, 998], [999, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 10, 997, 998, 999, 2000, 3000, 4000],
+        [[997, 998], [999, VIEW_R_WORLD]]);
+
+    // Both overlap.
+    testGenerateStripes([0, 2000], [[VIEW_L_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 101, 999, 2000],
+        [[VIEW_L_WORLD, 101], [999, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 101, 200, 900, 999, 2000],
+        [[VIEW_L_WORLD, 101], [200, 900], [999, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 10, 90, 101, 999, 2000, 3000, 4000],
+        [[VIEW_L_WORLD, 101], [999, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 10, 90, 101, 200, 900, 999, 2000, 3000, 4000],
+        [[VIEW_L_WORLD, 101], [200, 900], [999, VIEW_R_WORLD]]);
+  });
+
+  test('generateOdd', function() {
+    // One VSync.
+    testGenerateStripes([0], [[VIEW_L_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes([500], [[500, VIEW_R_WORLD]]);
+    testGenerateStripes([1500], []);
+
+    // Multiple VSyncs.
+    testGenerateStripes([0, 10, 20], [[VIEW_L_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes([0, 500, 2000], [[VIEW_L_WORLD, 500]]);
+    testGenerateStripes([0, 10, 500], [[500, VIEW_R_WORLD]]);
+    testGenerateStripes([0, 10, 2000], []);
+    testGenerateStripes(
+        [0, 200, 500],
+        [[VIEW_L_WORLD, 200], [500, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 200, 500, 900],
+        [[VIEW_L_WORLD, 200], [500, 900]]);
+  });
+
+  test('generateBorder', function() {
+    testGenerateStripes([0, VIEW_L_WORLD], [[VIEW_L_WORLD, VIEW_L_WORLD]]);
+    testGenerateStripes(
+        [VIEW_L_WORLD, VIEW_L_WORLD],
+        [[VIEW_L_WORLD, VIEW_L_WORLD]]);
+    testGenerateStripes(
+        [VIEW_R_WORLD, 2000],
+        [[VIEW_R_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [VIEW_R_WORLD, VIEW_R_WORLD],
+        [[VIEW_R_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [VIEW_L_WORLD, VIEW_R_WORLD],
+        [[VIEW_L_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [VIEW_L_WORLD, 200, 500, VIEW_R_WORLD],
+        [[VIEW_L_WORLD, 200], [500, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+        [[VIEW_L_WORLD, VIEW_L_WORLD], [VIEW_R_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, VIEW_L_WORLD, VIEW_R_WORLD, 2000],
+        [[VIEW_L_WORLD, VIEW_L_WORLD], [VIEW_R_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, VIEW_L_WORLD, 200, 500, VIEW_R_WORLD, 2000],
+        [[VIEW_L_WORLD, VIEW_L_WORLD], [200, 500],
+         [VIEW_R_WORLD, VIEW_R_WORLD]]);
+    testGenerateStripes(
+        [0, 10, VIEW_L_WORLD, VIEW_R_WORLD, 2000, 3000],
+        [[VIEW_L_WORLD, VIEW_R_WORLD]]);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/ddms_importer.html b/trace-viewer/trace_viewer/extras/importer/ddms_importer.html
new file mode 100644
index 0000000..4962dce
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/ddms_importer.html
@@ -0,0 +1,219 @@
+<!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="/extras/importer/jszip.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/importer/importer.html">
+
+<script>
+/**
+ * @fileoverview Blah
+ */
+'use strict';
+
+tv.exportTo('tv.e.importer.ddms', function() {
+  var Importer = tv.c.importer.Importer;
+
+  var kPid = 0;
+  var kCategory = 'java';
+  var kMethodLutEndMarker = '\n*end\n';
+  var kThreadsStart = '\n*threads\n';
+  var kMethodsStart = '\n*methods\n';
+
+  var kTraceMethodEnter = 0x00;       // method entry
+  var kTraceMethodExit = 0x01;        // method exit
+  var kTraceUnroll = 0x02;            // method exited by exception unrolling
+  // 0x03 currently unused
+  var kTraceMethodActionMask = 0x03;  // two bits
+
+  var kTraceHeaderLength = 32;
+  var kTraceMagicValue = 0x574f4c53;
+  var kTraceVersionSingleClock = 2;
+  var kTraceVersionDualClock = 3;
+  var kTraceRecordSizeSingleClock = 10;  // using v2
+  var kTraceRecordSizeDualClock = 14;  // using v3 with two timestamps
+
+  function Reader(string_payload) {
+    this.position_ = 0;
+    this.data_ = JSZip.utils.transformTo('uint8array', string_payload);
+  }
+
+  Reader.prototype = {
+    __proto__: Object.prototype,
+
+    uint8: function() {
+      var result = this.data_[this.position_];
+      this.position_ += 1;
+      return result;
+    },
+
+    uint16: function() {
+      var result = 0;
+      result += this.uint8();
+      result += this.uint8() << 8;
+      return result;
+    },
+
+    uint32: function() {
+      var result = 0;
+      result += this.uint8();
+      result += this.uint8() << 8;
+      result += this.uint8() << 16;
+      result += this.uint8() << 24;
+      return result;
+    },
+
+    uint64: function() {
+      // Javascript isn't able to manage 64-bit numeric values.
+      var low = this.uint32();
+      var high = this.uint32();
+      var low_str = ('0000000' + low.toString(16)).substr(-8);
+      var high_str = ('0000000' + high.toString(16)).substr(-8);
+      var result = high_str + low_str;
+      return result;
+    },
+
+    seekTo: function(position) {
+      this.position_ = position;
+    },
+
+    hasMore: function() {
+      return this.position_ < this.data_.length;
+    }
+  };
+
+  /**
+   * Imports DDMS method tracing events into a specified model.
+   * @constructor
+   */
+  function DdmsImporter(model, data) {
+    this.importPriority = 3;
+    this.model_ = model;
+    this.data_ = data;
+  }
+
+  /**
+   * Guesses whether the provided events is from a DDMS method trace.
+   * @return {boolean} True when events is a DDMS method trace.
+   */
+  DdmsImporter.canImport = function(data) {
+    if (typeof(data) === 'string' || data instanceof String) {
+      var header = data.slice(0, 1000);
+      return header.startsWith('*version\n') &&
+        header.indexOf('\nvm=') >= 0 &&
+        header.indexOf(kThreadsStart) >= 0;
+    }
+    /* key bit */
+    return false;
+  };
+
+  DdmsImporter.prototype = {
+    __proto__: Importer.prototype,
+
+    get model() {
+      return this.model_;
+    },
+
+    /**
+     * Imports the data in this.data_ into this.model_.
+     */
+    importEvents: function(isSecondaryImport) {
+      var divider = this.data_.indexOf(kMethodLutEndMarker) +
+          kMethodLutEndMarker.length;
+      this.metadata_ = this.data_.slice(0, divider);
+      this.methods_ = {};
+      this.parseThreads();
+      this.parseMethods();
+
+      var traceReader = new Reader(this.data_.slice(divider));
+      var magic = traceReader.uint32();
+      if (magic != kTraceMagicValue) {
+        throw Error('Failed to match magic value');
+      }
+      this.version_ = traceReader.uint16();
+      if (this.version_ != kTraceVersionDualClock) {
+        throw Error('Unknown version');
+      }
+      var dataOffest = traceReader.uint16();
+      var startDateTime = traceReader.uint64();
+      var recordSize = traceReader.uint16();
+
+      traceReader.seekTo(dataOffest);
+
+      while (traceReader.hasMore()) {
+        this.parseTraceEntry(traceReader);
+      }
+    },
+
+    parseTraceEntry: function(reader) {
+      var tid = reader.uint16();
+      var methodPacked = reader.uint32();
+      var cpuSinceStart = reader.uint32();
+      var wallClockSinceStart = reader.uint32();
+      var method = methodPacked & ~kTraceMethodActionMask;
+      var action = methodPacked & kTraceMethodActionMask;
+      var thread = this.getTid(tid);
+      method = this.getMethodName(method);
+      if (action == kTraceMethodEnter) {
+        thread.sliceGroup.beginSlice(kCategory, method, wallClockSinceStart,
+            undefined, cpuSinceStart);
+      } else if (thread.sliceGroup.openSliceCount) {
+        thread.sliceGroup.endSlice(wallClockSinceStart, cpuSinceStart);
+      }
+    },
+
+    parseThreads: function() {
+      var threads = this.metadata_.slice(this.metadata_.indexOf(kThreadsStart) +
+          kThreadsStart.length);
+      threads = threads.slice(0, threads.indexOf('\n*'));
+      threads = threads.split('\n');
+      threads.forEach(this.parseThread.bind(this));
+    },
+
+    parseThread: function(thread_line) {
+      var tid = thread_line.slice(0, thread_line.indexOf('\t'));
+      var thread = this.getTid(parseInt(tid));
+      thread.name = thread_line.slice(thread_line.indexOf('\t') + 1);
+    },
+
+    getTid: function(tid) {
+      return this.model_.getOrCreateProcess(kPid)
+        .getOrCreateThread(tid);
+    },
+
+    parseMethods: function() {
+      var methods = this.metadata_.slice(this.metadata_.indexOf(kMethodsStart) +
+          kMethodsStart.length);
+      methods = methods.slice(0, methods.indexOf('\n*'));
+      methods = methods.split('\n');
+      methods.forEach(this.parseMethod.bind(this));
+    },
+
+    parseMethod: function(method_line) {
+      var data = method_line.split('\t');
+      var methodId = parseInt(data[0]);
+      var methodName = data[1] + '.' + data[2] + data[3];
+      this.addMethod(methodId, methodName);
+    },
+
+    addMethod: function(methodId, methodName) {
+      this.methods_[methodId] = methodName;
+    },
+
+    getMethodName: function(methodId) {
+      return this.methods_[methodId];
+    }
+  };
+
+  // Register the DdmsImporter to the Importer.
+  tv.c.importer.Importer.register(DdmsImporter);
+
+  return {
+    DdmsImporter: DdmsImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/ddms_importer_test.html b/trace-viewer/trace_viewer/extras/importer/ddms_importer_test.html
new file mode 100644
index 0000000..6f019cf
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/ddms_importer_test.html
@@ -0,0 +1,180 @@
+<!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="/extras/importer/ddms_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('canImport', function() {
+    assert.isFalse(tv.e.importer.ddms.DdmsImporter.canImport('string'));
+    assert.isFalse(tv.e.importer.ddms.DdmsImporter.canImport([]));
+    assert.isTrue(tv.e.importer.ddms.DdmsImporter.canImport(TEST_DATA));
+  });
+
+  test('parseThreads', function() {
+    var m = new tv.c.TraceModel(TEST_DATA, false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 2);
+    threads = m.findAllThreadsNamed('main');
+    assert.equal(threads.length, 1);
+    var thread = threads[0];
+    assert.equal(thread.tid, 2703);
+  });
+
+  test('parseMethods', function() {
+    var m = new tv.c.TraceModel(TEST_DATA, false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.findAllThreadsNamed('Binder_1');
+    assert.equal(threads.length, 1);
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.length, 22);
+    assert.equal('android.os.Binder.execTransact(IJJI)Z',
+                 thread.sliceGroup.slices[0].title);
+  });
+
+  var TEST_DATA = atob('KnZlcnNpb24KMwpkYXRhLWZpbGUtb3ZlcmZsb3c9ZmFsc2UKY2' +
+      'xvY2s9ZHVhbAplbGFwc2VkLXRpbWUtdXNlYz02MzMwNzc5Cm51' +
+      'bS1tZXRob2QtY2FsbHM9NzYKY2xvY2stY2FsbC1vdmVyaGVhZC' +
+      '1uc2VjPTMzNDMKdm09YXJ0Cip0aHJlYWRzCjI3MDMJbWFpbgoy' +
+      'NzEwCUhlYXAgdGhyZWFkIHBvb2wgd29ya2VyIHRocmVhZCAxCj' +
+      'I3MDkJSGVhcCB0aHJlYWQgcG9vbCB3b3JrZXIgdGhyZWFkIDAK' +
+      'MjcxMQlIZWFwIHRocmVhZCBwb29sIHdvcmtlciB0aHJlYWQgMg' +
+      'oyNzEyCVNpZ25hbCBDYXRjaGVyCjI3MTMJSkRXUAoyNzE0CVJl' +
+      'ZmVyZW5jZVF1ZXVlRGFlbW9uCjI3MTUJRmluYWxpemVyRGFlbW' +
+      '9uCjI3MTYJRmluYWxpemVyV2F0Y2hkb2dEYWVtb24KMjcxNwlI' +
+      'ZWFwVHJpbW1lckRhZW1vbgoyNzE4CUdDRGFlbW9uCjI3MTkJQm' +
+      'luZGVyXzEKMjcyMAlCaW5kZXJfMgoyNzI3CVJlbmRlclRocmVh' +
+      'ZAoyNzI4CUFzeW5jVGFzayAjMQoyNzI5CUFzeW5jVGFzayAjMg' +
+      'oyNzMwCUJpbmRlcl8zCjExNTk4CWh3dWlUYXNrMQoxMTU5OQlo' +
+      'd3VpVGFzazIKMTE2MDAJQXN5bmNUYXNrICMzCjExNjAxCUFzeW' +
+      '5jVGFzayAjNAoxMTY3MwlBc3luY1Rhc2sgIzUKKm1ldGhvZHMK' +
+      'MHg3MGZiNzc1OAlkYWx2aWsuc3lzdGVtLkNsb3NlR3VhcmQJY2' +
+      'xvc2UJKClWCUNsb3NlR3VhcmQuamF2YQoweDcwZmI4NDE4CWRh' +
+      'bHZpay5zeXN0ZW0uVk1EZWJ1ZwlzdGFydE1ldGhvZFRyYWNpbm' +
+      'cJKExqYXZhL2xhbmcvU3RyaW5nO0xqYXZhL2lvL0ZpbGVEZXNj' +
+      'cmlwdG9yO0lJWkkpVglWTURlYnVnLmphdmEKMHg3MGZlYTcyOA' +
+      'lqYXZhLnV0aWwuQXJyYXlMaXN0CXNpemUJKClJCUFycmF5TGlz' +
+      'dC5qYXZhCjB4NzEwMTNmYTgJbGliY29yZS5pby5CbG9ja0d1YX' +
+      'JkT3MJY2xvc2UJKExqYXZhL2lvL0ZpbGVEZXNjcmlwdG9yOylW' +
+      'CUJsb2NrR3VhcmRPcy5qYXZhCjB4NzEwNTc3NDgJYW5kcm9pZC' +
+      '5vcy5CaW5kZXIJZXhlY1RyYW5zYWN0CShJSkpJKVoJQmluZGVy' +
+      'LmphdmEKMHg3MTA3MTdlOAlhbmRyb2lkLmFwcC5BY3Rpdml0eV' +
+      'RocmVhZCRBcHBsaWNhdGlvblRocmVhZAlwcm9maWxlckNvbnRy' +
+      'b2wJKFpMYW5kcm9pZC9hcHAvUHJvZmlsZXJJbmZvO0kpVglBY3' +
+      'Rpdml0eVRocmVhZC5qYXZhCjB4NzEwNzJhNDgJYW5kcm9pZC5h' +
+      'cHAuQXBwbGljYXRpb25UaHJlYWROYXRpdmUJb25UcmFuc2FjdA' +
+      'koSUxhbmRyb2lkL29zL1BhcmNlbDtMYW5kcm9pZC9vcy9QYXJj' +
+      'ZWw7SSlaCUFwcGxpY2F0aW9uVGhyZWFkTmF0aXZlLmphdmEKMH' +
+      'g3MTA3MmYyOAlhbmRyb2lkLm9zLkhhbmRsZXIJZW5xdWV1ZU1l' +
+      'c3NhZ2UJKExhbmRyb2lkL29zL01lc3NhZ2VRdWV1ZTtMYW5kcm' +
+      '9pZC9vcy9NZXNzYWdlO0opWglIYW5kbGVyLmphdmEKMHg3MTA3' +
+      'MmZlOAlhbmRyb2lkLm9zLkhhbmRsZXIJZGlzcGF0Y2hNZXNzYW' +
+      'dlCShMYW5kcm9pZC9vcy9NZXNzYWdlOylWCUhhbmRsZXIuamF2' +
+      'YQoweDcxMDczNTI4CWFuZHJvaWQub3MuSGFuZGxlcglzZW5kTW' +
+      'Vzc2FnZQkoTGFuZHJvaWQvb3MvTWVzc2FnZTspWglIYW5kbGVy' +
+      'LmphdmEKMHg3MTA3MzU4OAlhbmRyb2lkLm9zLkhhbmRsZXIJc2' +
+      'VuZE1lc3NhZ2VBdFRpbWUJKExhbmRyb2lkL29zL01lc3NhZ2U7' +
+      'SilaCUhhbmRsZXIuamF2YQoweDcxMDczNWI4CWFuZHJvaWQub3' +
+      'MuSGFuZGxlcglzZW5kTWVzc2FnZURlbGF5ZWQJKExhbmRyb2lk' +
+      'L29zL01lc3NhZ2U7SilaCUhhbmRsZXIuamF2YQoweDcxMDczNj' +
+      'Q4CWFuZHJvaWQuYXBwLkFjdGl2aXR5VGhyZWFkJEgJaGFuZGxl' +
+      'TWVzc2FnZQkoTGFuZHJvaWQvb3MvTWVzc2FnZTspVglBY3Rpdm' +
+      'l0eVRocmVhZC5qYXZhCjB4NzEwNzM3NjgJYW5kcm9pZC5hcHAu' +
+      'QWN0aXZpdHlUaHJlYWQkUHJvZmlsZXIJc3RhcnRQcm9maWxpbm' +
+      'cJKClWCUFjdGl2aXR5VGhyZWFkLmphdmEKMHg3MTA3Mzc5OAlh' +
+      'bmRyb2lkLmFwcC5BY3Rpdml0eVRocmVhZCRQcm9maWxlcglzdG' +
+      '9wUHJvZmlsaW5nCSgpVglBY3Rpdml0eVRocmVhZC5qYXZhCjB4' +
+      'NzEwYzQ2ZDgJYW5kcm9pZC5vcy5NZXNzYWdlCW9idGFpbgkoKU' +
+      'xhbmRyb2lkL29zL01lc3NhZ2U7CU1lc3NhZ2UuamF2YQoweDcx' +
+      'MGM0YTM4CWFuZHJvaWQub3MuTWVzc2FnZQlpc0luVXNlCSgpWg' +
+      'lNZXNzYWdlLmphdmEKMHg3MTBjNGE2OAlhbmRyb2lkLm9zLk1l' +
+      'c3NhZ2UJbWFya0luVXNlCSgpVglNZXNzYWdlLmphdmEKMHg3MT' +
+      'BjNGFmOAlhbmRyb2lkLm9zLk1lc3NhZ2UJcmVjeWNsZVVuY2hl' +
+      'Y2tlZAkoKVYJTWVzc2FnZS5qYXZhCjB4NzEwYzRmMTgJYW5kcm' +
+      '9pZC5vcy5QYXJjZWwJaW5pdAkoSilWCVBhcmNlbC5qYXZhCjB4' +
+      'NzEwYzRmYTgJYW5kcm9pZC5vcy5QYXJjZWwJb2J0YWluCShKKU' +
+      'xhbmRyb2lkL29zL1BhcmNlbDsJUGFyY2VsLmphdmEKMHg3MTBj' +
+      'NTQyOAlhbmRyb2lkLm9zLlBhcmNlbAllbmZvcmNlSW50ZXJmYW' +
+      'NlCShMamF2YS9sYW5nL1N0cmluZzspVglQYXJjZWwuamF2YQow' +
+      'eDcxMGM1OWY4CWFuZHJvaWQub3MuUGFyY2VsCXJlYWRJbnQJKC' +
+      'lJCVBhcmNlbC5qYXZhCjB4NzEwYzZhNDgJYW5kcm9pZC5vcy5Q' +
+      'YXJjZWxGaWxlRGVzY3JpcHRvcgljbG9zZVdpdGhTdGF0dXMJKE' +
+      'lMamF2YS9sYW5nL1N0cmluZzspVglQYXJjZWxGaWxlRGVzY3Jp' +
+      'cHRvci5qYXZhCjB4NzEwYzZkNzgJYW5kcm9pZC5vcy5QYXJjZW' +
+      'xGaWxlRGVzY3JpcHRvcgl3cml0ZUNvbW1TdGF0dXNBbmRDbG9z' +
+      'ZQkoSUxqYXZhL2xhbmcvU3RyaW5nOylWCVBhcmNlbEZpbGVEZX' +
+      'NjcmlwdG9yLmphdmEKMHg3MTBjNmUwOAlhbmRyb2lkLm9zLlBh' +
+      'cmNlbEZpbGVEZXNjcmlwdG9yCWNsb3NlCSgpVglQYXJjZWxGaW' +
+      'xlRGVzY3JpcHRvci5qYXZhCjB4NzEwYzZmZTgJYW5kcm9pZC5v' +
+      'cy5QYXJjZWxGaWxlRGVzY3JpcHRvcglyZWxlYXNlUmVzb3VyY2' +
+      'VzCSgpVglQYXJjZWxGaWxlRGVzY3JpcHRvci5qYXZhCjB4NzEx' +
+      'NWIwZjgJYW5kcm9pZC5vcy5NZXNzYWdlUXVldWUJZW5xdWV1ZU' +
+      '1lc3NhZ2UJKExhbmRyb2lkL29zL01lc3NhZ2U7SilaCU1lc3Nh' +
+      'Z2VRdWV1ZS5qYXZhCjB4NzExNWIyMTgJYW5kcm9pZC5vcy5NZX' +
+      'NzYWdlUXVldWUJbmV4dAkoKUxhbmRyb2lkL29zL01lc3NhZ2U7' +
+      'CU1lc3NhZ2VRdWV1ZS5qYXZhCjB4NzE3ZTM2MzAJZGFsdmlrLn' +
+      'N5c3RlbS5WTURlYnVnCXN0YXJ0TWV0aG9kVHJhY2luZ0ZkCShM' +
+      'amF2YS9sYW5nL1N0cmluZztMamF2YS9pby9GaWxlRGVzY3JpcH' +
+      'RvcjtJSVpJKVYJVk1EZWJ1Zy5qYXZhCjB4NzE4MTBhMjAJamF2' +
+      'YS5pby5GaWxlRGVzY3JpcHRvcglpc1NvY2tldAkoKVoJRmlsZU' +
+      'Rlc2NyaXB0b3IuamF2YQoweDcxODEwYWUwCWphdmEuaW8uRmls' +
+      'ZURlc2NyaXB0b3IJdmFsaWQJKClaCUZpbGVEZXNjcmlwdG9yLm' +
+      'phdmEKMHg3MTgxYmI4MAlsaWJjb3JlLmlvLklvVXRpbHMJY2xv' +
+      'c2UJKExqYXZhL2lvL0ZpbGVEZXNjcmlwdG9yOylWCUlvVXRpbH' +
+      'MuamF2YQoweDcxODFiYmIwCWxpYmNvcmUuaW8uSW9VdGlscwlj' +
+      'bG9zZVF1aWV0bHkJKExqYXZhL2lvL0ZpbGVEZXNjcmlwdG9yOy' +
+      'lWCUlvVXRpbHMuamF2YQoweDcxODIxZWUwCWFuZHJvaWQuYXBw' +
+      'LkFjdGl2aXR5VGhyZWFkCWFjY2VzcyQzMDAJKExhbmRyb2lkL2' +
+      'FwcC9BY3Rpdml0eVRocmVhZDtJTGphdmEvbGFuZy9PYmplY3Q7' +
+      'SUkpVglBY3Rpdml0eVRocmVhZC5qYXZhCjB4NzE4MjJhZTAJYW' +
+      '5kcm9pZC5hcHAuQWN0aXZpdHlUaHJlYWQJc2VuZE1lc3NhZ2UJ' +
+      'KElMamF2YS9sYW5nL09iamVjdDtJSSlWCUFjdGl2aXR5VGhyZW' +
+      'FkLmphdmEKMHg3MTgyMmIxMAlhbmRyb2lkLmFwcC5BY3Rpdml0' +
+      'eVRocmVhZAlzZW5kTWVzc2FnZQkoSUxqYXZhL2xhbmcvT2JqZW' +
+      'N0O0lJWilWCUFjdGl2aXR5VGhyZWFkLmphdmEKMHg3MTgyMzIz' +
+      'MAlhbmRyb2lkLmFwcC5BY3Rpdml0eVRocmVhZAloYW5kbGVQcm' +
+      '9maWxlckNvbnRyb2wJKFpMYW5kcm9pZC9hcHAvUHJvZmlsZXJJ' +
+      'bmZvO0kpVglBY3Rpdml0eVRocmVhZC5qYXZhCjB4NzE4MzY3MD' +
+      'AJYW5kcm9pZC5vcy5EZWJ1ZwlzdG9wTWV0aG9kVHJhY2luZwko' +
+      'KVYJRGVidWcuamF2YQoqZW5kClNMT1cDACAASxycoWkAAAAOAA' +
+      'AAAAAAAAAAAAAAAAAAjwoxNn5xxtotAKISAgCPChmE+3CT4y0A' +
+      '/hsCAI8KaTcHcbbkLQAtNQIAjwoIbgxx0eQtAEc1AgCPCkhqDH' +
+      'Hk5C0AWjUCAI8KWHf7cPLkLQBoNQIAjwpZd/tw/eQtAHM1AgCP' +
+      'CnhtDHED5S0AeTUCAI8KeW0McQjlLQB+NQIAjwqwu4FxFOUtAI' +
+      'o1AgCPCoC7gXEa5S0AkDUCAI8K4AqBcSPlLQCaNQIAjwrhCoFx' +
+      'KuUtAKE1AgCPCqg/AXE65S0AsTUCAI8KIAqBcUDlLQC2NQIAjw' +
+      'ohCoFxTuUtAMU1AgCPCqk/AXFn5S0A3jUCAI8KgbuBcWzlLQDi' +
+      'NQIAjwqxu4Fxb+UtAOU1AgCPCuhvDHF15S0A6zUCAI8K6W8McX' +
+      'nlLQDvNQIAjwpJagxxfOUtAPI1AgCPCgluDHGB5S0A9zUCAI8K' +
+      'MTKCcYXlLQD6NQIAjwpJNgdxjOUtAAM2AgCPCukvB3GS5S0ACT' +
+      'YCAI8K+EoMcaTlLQAbNgIAjwr5SgxxwOUtADY2AgCPChiyFXHH' +
+      '5S0APTYCAI8KKKf+cPHlLQBnNgIAjwopp/5w++UtAHE2AgCfCk' +
+      'h3BXHy2wAAu5RgAJ8KqE8McSvcAADwlGAAnwoYTwxxaNwAACyV' +
+      'YACfChlPDHGD3AAARpVgAJ8KqU8McZbcAABZlWAAnwqoTwxxrN' +
+      'wAAG6VYACfChhPDHHL3AAAjpVgAJ8KGU8MceDcAACjlWAAnwqp' +
+      'Twxx8twAALSVYACfCkgqB3Ea3QAA3ZVgAJ8KKFQMcUHdAAAFlm' +
+      'AAnwopVAxxjN0AAFGWYACfCvhZDHHD3QAAiJZgAJ8K+VkMcejd' +
+      'AACslmAAnwr4WQxxAN4AAMOWYACfCvlZDHEa3gAA3ZZgAJ8K+F' +
+      'kMcS/eAADylmAAnwr5WQxxR94AAAqXYACfCugXB3Fv3gAAM5dg' +
+      'AJ8K4B6CcYveAABPl2AAnwrgKoJxrN4AAHCXYACfChArgnHQ3g' +
+      'AAkpdgAJ8K2EYMceTeAACml2AAnwrZRgxxDd8AANCXYACfCig1' +
+      'B3Er3wAA7pdgAJ8KuDUHcT7fAAAAmGAAnwqINQdxa98AAC+YYA' +
+      'CfCigvB3GD3wAASJhgAJ8K+LAVcaDfAABjmGAAnwo4Sgxxtt8A' +
+      'AHiYYACfCjlKDHHL3wAAkJhgAJ8KaEoMce3fAACxmGAAnwppSg' +
+      'xxAuAAAMWYYACfCvmwFXFz4AAAOJlgAI8KGbIVcU3mLQBAmWAA' +
+      'jwroLwdxVuYtAEmZYACfCikvB3GG4AAASZlgAI8KSDYHcVzmLQ' +
+      'BPmWAAjwowMoJxZuYtAFmZYACfCok1B3GW4AAAWJlgAI8KmDcH' +
+      'cXTmLQBnmWAAnwq5NQdxr+AAAHGZYACPCgBng3GC5i0AdplgAJ' +
+      '8KKTUHcb3gAAB/mWAAnwoRK4JxzOAAAI6ZYAA=');
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/etw_importer.html b/trace-viewer/trace_viewer/extras/importer/etw/etw_importer.html
new file mode 100644
index 0000000..842018e
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/etw_importer.html
@@ -0,0 +1,475 @@
+<!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="/extras/importer/etw/eventtrace_parser.html">
+<link rel="import" href="/extras/importer/etw/process_parser.html">
+<link rel="import" href="/extras/importer/etw/thread_parser.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/base/base64.html">
+
+<script>
+/**
+ * @fileoverview Imports JSON file with the raw payloads from a Windows event
+ * trace into the Tracemodel. This format is outputted by Chrome running
+ * on a Windows system.
+ *
+ * This importer assumes the events arrived as a JSON file and the payloads are
+ * undecoded sequence of bytes in hex format string. The unit tests provide
+ * examples of the trace format.
+ *
+ * The format of the system trace is
+ *     {
+ *       name: 'ETW',
+ *       content: [ <events> ]
+ *     }
+  *
+ * where the <events> are dictionary values with fields.
+ *
+ *     {
+ *       guid: "1234-...",    // The unique GUID for the event.
+ *       op: 12,              // The opcode of the event.
+ *       ver: 1,              // The encoding version of the event.
+ *       cpu: 0,              // The cpu id on which the event was captured.
+ *       ts: 1092,            // The thread id on which the event was captured.
+ *       payload: "aaaa"      // A base64 encoded string of the raw payload.
+ *     }
+ *
+ * The payload is an undecoded version of the raw event sent by ETW.
+ * This importer uses specific parsers to decode recognized events.
+ * A parser need to register the recognized event by calling
+ * registerEventHandler(guid, opcode, handler). The parser is responsible to
+ * decode the payload and update the TraceModel.
+ *
+ * The payload formats are described there:
+ *   http://msdn.microsoft.com/en-us/library/windows/desktop/aa364085(v=vs.85).aspx
+ *
+ */
+'use strict';
+
+tv.exportTo('tv.e.importer.etw', function() {
+  var Importer = tv.c.importer.Importer;
+
+  // GUID and opcode of a Thread DCStart event, as defined at the link above.
+  var kThreadGuid = '3D6FA8D1-FE05-11D0-9DDA-00C04FD7BA7C';
+  var kThreadDCStartOpcode = 3;
+
+  /**
+   * Represents the raw bytes payload decoder.
+   * @constructor
+   */
+  function Decoder() {
+    this.payload_ = new DataView(new ArrayBuffer(256));
+  };
+
+  Decoder.prototype = {
+    __proto__: Object.prototype,
+
+    reset: function(base64_payload) {
+      var decoded_size = tv.b.Base64.getDecodedBufferLength(base64_payload);
+      if (decoded_size > this.payload_.byteLength)
+        this.payload_ = new DataView(new ArrayBuffer(decoded_size));
+
+      tv.b.Base64.DecodeToTypedArray(base64_payload, this.payload_);
+      this.position_ = 0;
+    },
+
+    skip: function(length) {
+      this.position_ += length;
+    },
+
+    decodeUInt8: function() {
+      var result = this.payload_.getUint8(this.position_, true);
+      this.position_ += 1;
+      return result;
+    },
+
+    decodeUInt16: function() {
+      var result = this.payload_.getUint16(this.position_, true);
+      this.position_ += 2;
+      return result;
+    },
+
+    decodeUInt32: function() {
+      var result = this.payload_.getUint32(this.position_, true);
+      this.position_ += 4;
+      return result;
+    },
+
+    decodeUInt64ToString: function() {
+      // Javascript isn't able to manage 64-bit numeric values.
+      var low = this.decodeUInt32();
+      var high = this.decodeUInt32();
+      var low_str = ('0000000' + low.toString(16)).substr(-8);
+      var high_str = ('0000000' + high.toString(16)).substr(-8);
+      var result = high_str + low_str;
+      return result;
+    },
+
+    decodeInt8: function() {
+      var result = this.payload_.getInt8(this.position_, true);
+      this.position_ += 1;
+      return result;
+    },
+
+    decodeInt16: function() {
+      var result = this.payload_.getInt16(this.position_, true);
+      this.position_ += 2;
+      return result;
+    },
+
+    decodeInt32: function() {
+      var result = this.payload_.getInt32(this.position_, true);
+      this.position_ += 4;
+      return result;
+    },
+
+    decodeInt64ToString: function() {
+      // Javascript isn't able to manage 64-bit numeric values.
+      // Fallback to unsigned 64-bit hexa value.
+      return this.decodeUInt64ToString();
+    },
+
+    decodeUInteger: function(is64) {
+      if (is64)
+        return this.decodeUInt64ToString();
+      return this.decodeUInt32();
+    },
+
+    decodeString: function() {
+      var str = '';
+      while (true) {
+        var c = this.decodeUInt8();
+        if (!c)
+          return str;
+        str = str + String.fromCharCode(c);
+      }
+    },
+
+    decodeW16String: function() {
+      var str = '';
+      while (true) {
+        var c = this.decodeUInt16();
+        if (!c)
+          return str;
+        str = str + String.fromCharCode(c);
+      }
+    },
+
+    decodeFixedW16String: function(length) {
+      var old_position = this.position_;
+      var str = '';
+      for (var i = 0; i < length; i++) {
+        var c = this.decodeUInt16();
+        if (!c)
+          break;
+        str = str + String.fromCharCode(c);
+      }
+
+      // Move the position after the fixed buffer (i.e. wchar[length]).
+      this.position_ = old_position + 2 * length;
+      return str;
+    },
+
+    decodeBytes: function(length) {
+      var bytes = [];
+      for (var i = 0; i < length; ++i) {
+        var c = this.decodeUInt8();
+        bytes.push(c);
+      }
+      return bytes;
+    },
+
+    decodeSID: function(is64) {
+      // Decode the TOKEN_USER structure.
+      var pSid = this.decodeUInteger(is64);
+      var attributes = this.decodeUInt32();
+
+      // Skip padding.
+      if (is64)
+        this.decodeUInt32();
+
+      // Decode the SID structure.
+      var revision = this.decodeUInt8();
+      var subAuthorityCount = this.decodeUInt8();
+      this.decodeUInt16();
+      this.decodeUInt32();
+
+      if (revision != 1)
+        throw 'Invalid SID revision: could not decode the SID structure.';
+
+      var sid = this.decodeBytes(4 * subAuthorityCount);
+
+      return {
+        pSid: pSid,
+        attributes: attributes,
+        sid: sid
+      };
+    },
+
+    decodeSystemTime: function() {
+      // Decode the SystemTime structure.
+      var wYear = this.decodeInt16();
+      var wMonth = this.decodeInt16();
+      var wDayOfWeek = this.decodeInt16();
+      var wDay = this.decodeInt16();
+      var wHour = this.decodeInt16();
+      var wMinute = this.decodeInt16();
+      var wSecond = this.decodeInt16();
+      var wMilliseconds = this.decodeInt16();
+      return {
+        wYear: wYear,
+        wMonth: wMonth,
+        wDayOfWeek: wDayOfWeek,
+        wDay: wDay,
+        wHour: wHour,
+        wMinute: wMinute,
+        wSecond: wSecond,
+        wMilliseconds: wMilliseconds
+      };
+    },
+
+    decodeTimeZoneInformation: function() {
+      // Decode the TimeZoneInformation structure.
+      var bias = this.decodeUInt32();
+      var standardName = this.decodeFixedW16String(32);
+      var standardDate = this.decodeSystemTime();
+      var standardBias = this.decodeUInt32();
+      var daylightName = this.decodeFixedW16String(32);
+      var daylightDate = this.decodeSystemTime();
+      var daylightBias = this.decodeUInt32();
+      return {
+        bias: bias,
+        standardName: standardName,
+        standardDate: standardDate,
+        standardBias: standardBias,
+        daylightName: daylightName,
+        daylightDate: daylightDate,
+        daylightBias: daylightBias
+      };
+    }
+
+  };
+
+  /**
+   * Imports Windows ETW kernel events into a specified model.
+   * @constructor
+   */
+  function EtwImporter(model, events) {
+    this.importPriority = 3;
+    this.model_ = model;
+    this.events_ = events;
+    this.handlers_ = {};
+    this.decoder_ = new Decoder();
+    this.walltime_ = undefined;
+    this.ticks_ = undefined;
+    this.is64bit_ = undefined;
+
+    // A map of tids to their process pid. On Windows, the tid is global to
+    // the system and doesn't need to belong to a process. As many events
+    // only provide tid, this map allows to retrieve the parent process.
+    this.tidsToPid_ = {};
+
+    // Instantiate the parsers; this will register handlers for known events.
+    var allTypeInfos = tv.e.importer.etw.Parser.getAllRegisteredTypeInfos();
+    this.parsers_ = allTypeInfos.map(
+        function(typeInfo) {
+          return new typeInfo.constructor(this);
+        }, this);
+  }
+
+  /**
+   * Guesses whether the provided events is from a Windows ETW trace.
+   * The object must has a property named 'name' with the value 'ETW' and
+   * a property 'content' with all the undecoded events.
+   *
+   * @return {boolean} True when events is a Windows ETW array.
+   */
+  EtwImporter.canImport = function(events) {
+    if (!events.hasOwnProperty('name') ||
+        !events.hasOwnProperty('content') ||
+        events.name !== 'ETW') {
+      return false;
+    }
+
+    return true;
+  };
+
+  EtwImporter.prototype = {
+    __proto__: Importer.prototype,
+
+    get model() {
+      return this.model_;
+    },
+
+    createThreadIfNeeded: function(pid, tid) {
+      this.tidsToPid_[tid] = pid;
+    },
+
+    removeThreadIfPresent: function(tid) {
+      this.tidsToPid_[tid] = undefined;
+    },
+
+    getPidFromWindowsTid: function(tid) {
+      if (tid == 0)
+        return 0;
+      var pid = this.tidsToPid_[tid];
+      if (pid == undefined) {
+        // Kernel threads are not defined.
+        return 0;
+      }
+      return pid;
+    },
+
+    getThreadFromWindowsTid: function(tid) {
+      var pid = this.getPidFromWindowsTid(tid);
+      var process = this.model_.getProcess(pid);
+      if (!process)
+        return undefined;
+      return process.getThread(tid);
+    },
+
+    /*
+     * Retrieve the Cpu for a given cpuNumber.
+     * @return {Cpu} A Cpu corresponding to the given cpuNumber.
+     */
+    getOrCreateCpu: function(cpuNumber) {
+      var cpu = this.model_.kernel.getOrCreateCpu(cpuNumber);
+      return cpu;
+    },
+
+    /**
+     * Imports the data in this.events_ into this.model_.
+     */
+    importEvents: function(isSecondaryImport) {
+      this.events_.content.forEach(this.parseInfo.bind(this));
+
+      if (this.walltime_ == undefined || this.ticks_ == undefined)
+        throw Error('Cannot find clock sync information in the system trace.');
+
+      if (this.is64bit_ == undefined)
+        throw Error('Cannot determine pointer size of the system trace.');
+
+      this.events_.content.forEach(this.parseEvent.bind(this));
+    },
+
+    importTimestamp: function(timestamp) {
+      var ts = parseInt(timestamp, 16);
+      return (ts - this.walltime_ + this.ticks_) / 1000.;
+    },
+
+    parseInfo: function(event) {
+      // Retrieve clock sync information.
+      if (event.hasOwnProperty('guid') &&
+          event.hasOwnProperty('walltime') &&
+          event.hasOwnProperty('tick') &&
+          event.guid === 'ClockSync') {
+        this.walltime_ = parseInt(event.walltime, 16);
+        this.ticks_ = parseInt(event.tick, 16);
+      }
+
+      // Retrieve pointer size information from a Thread.DCStart event.
+      if (this.is64bit_ == undefined &&
+          event.hasOwnProperty('guid') &&
+          event.hasOwnProperty('op') &&
+          event.hasOwnProperty('ver') &&
+          event.hasOwnProperty('payload') &&
+          event.guid === kThreadGuid &&
+          event.op == kThreadDCStartOpcode) {
+        var decoded_size = tv.b.Base64.getDecodedBufferLength(event.payload);
+
+        if (event.ver == 1) {
+          if (decoded_size >= 52)
+            this.is64bit_ = true;
+          else
+            this.is64bit_ = false;
+        } else if (event.ver == 2) {
+          if (decoded_size >= 64)
+            this.is64bit_ = true;
+          else
+            this.is64bit_ = false;
+        } else if (event.ver == 3) {
+          if (decoded_size >= 60)
+            this.is64bit_ = true;
+          else
+            this.is64bit_ = false;
+        }
+      }
+
+      return true;
+    },
+
+    parseEvent: function(event) {
+      if (!event.hasOwnProperty('guid') ||
+          !event.hasOwnProperty('op') ||
+          !event.hasOwnProperty('ver') ||
+          !event.hasOwnProperty('cpu') ||
+          !event.hasOwnProperty('ts') ||
+          !event.hasOwnProperty('payload')) {
+        return false;
+      }
+
+      var timestamp = this.importTimestamp(event.ts);
+
+      // Create the event header.
+      var header = {
+        guid: event.guid,
+        opcode: event.op,
+        version: event.ver,
+        cpu: event.cpu,
+        timestamp: timestamp,
+        is64: this.is64bit_
+      };
+
+      // Set the payload to decode.
+      var decoder = this.decoder_;
+      decoder.reset(event.payload);
+
+      // Retrieve the handler to decode the payload.
+      var handler = this.getEventHandler(header.guid, header.opcode);
+      if (!handler)
+        return false;
+
+      if (!handler(header, decoder)) {
+        this.model_.importWarning({
+          type: 'parse_error',
+          message: 'Malformed ' + header.guid + ' event (' + text + ')'
+        });
+        return false;
+      }
+
+      return true;
+    },
+
+    /**
+     * Registers a windows ETW event handler used by parseEvent().
+     */
+    registerEventHandler: function(guid, opcode, handler) {
+      if (this.handlers_[guid] == undefined)
+        this.handlers_[guid] = [];
+      this.handlers_[guid][opcode] = handler;
+    },
+
+    /**
+     * Retrieves a registered event handler.
+     */
+    getEventHandler: function(guid, opcode) {
+      if (this.handlers_[guid] == undefined)
+        return undefined;
+      return this.handlers_[guid][opcode];
+    }
+
+  };
+
+  // Register the EtwImporter to the Importer.
+  tv.c.importer.Importer.register(EtwImporter);
+
+  return {
+    EtwImporter: EtwImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/etw_importer_test.html b/trace-viewer/trace_viewer/extras/importer/etw/etw_importer_test.html
new file mode 100644
index 0000000..b82c208
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/etw_importer_test.html
@@ -0,0 +1,287 @@
+<!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/test_utils.html">
+<link rel="import" href="/extras/importer/etw/etw_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('canImport', function() {
+    assert.isFalse(tv.e.importer.etw.EtwImporter.canImport('string'));
+    assert.isFalse(tv.e.importer.etw.EtwImporter.canImport([]));
+
+    // Must not parse an invalid name.
+    var dummy = { name: 'dummy', content: [] };
+    assert.isFalse(tv.e.importer.etw.EtwImporter.canImport(dummy));
+
+    // Must parse  an empty valid trace.
+    var valid = { name: 'ETW', content: [] };
+    assert.isTrue(tv.e.importer.etw.EtwImporter.canImport(valid));
+  });
+
+  test('getModel', function() {
+    var model = 'dummy';
+    var events = [];
+    var importer = new tv.e.importer.etw.EtwImporter(model, events);
+    assert.strictEqual(importer.model, model);
+  });
+
+  test('registerEventHandler', function() {
+    // Create a dummy EtwImporter.
+    var model = 'dummy';
+    var events = ['events'];
+    var importer = new tv.e.importer.etw.EtwImporter(model, events);
+    var dummy_handler = function() {};
+
+    // The handler must not exists.
+    assert.isUndefined(importer.getEventHandler('ABCDEF', 2));
+
+    // Register an event handler for guid: ABCDEF and opcode: 2.
+    importer.registerEventHandler('ABCDEF', 2, dummy_handler);
+
+    // The handler exists now, must find it.
+    assert.isDefined(importer.getEventHandler('ABCDEF', 2));
+
+    // Must be able to manage an invalid handler.
+    assert.isUndefined(importer.getEventHandler('zzzzzz', 2));
+  });
+
+  test('parseEvent', function() {
+    var model = 'dummy';
+    var events = [];
+    var importer = new tv.e.importer.etw.EtwImporter(model, events);
+    var handler_called = false;
+    var dummy_handler = function() { handler_called = true; return true; };
+
+    // Register a valid handler.
+    importer.registerEventHandler('aaaa', 42, dummy_handler);
+
+    // Try to parse an invalid event with missing fields.
+    var incomplet_event = { guid: 'aaaa', 'op': 42, 'ver': 0 };
+    assert.isFalse(importer.parseEvent(incomplet_event));
+    assert.isFalse(handler_called);
+
+    // Try to parse a valid event.
+    var valid_event = {
+      guid: 'aaaa', 'op': 42, 'ver': 0, 'cpu': 0, 'ts': 0, 'payload': btoa('0')
+    };
+    assert.isTrue(importer.parseEvent(valid_event));
+    assert.isTrue(handler_called);
+  });
+
+  test('resetTooSmall', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+
+    var oldByteLength = decoder.payload_.byteLength;
+    // Decode a payload too big for the actual buffer.
+    decoder.reset('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==');
+    var newByteLength = decoder.payload_.byteLength;
+
+    // Validate the buffer has been resized.
+    assert.isBelow(oldByteLength, newByteLength);
+  });
+
+  test('decode', function() {
+    var model = 'dummy';
+    var events = [];
+    var importer = new tv.e.importer.etw.EtwImporter(model, events);
+
+    var decoder = importer.decoder_;
+
+    decoder.reset('YQBiYw==');
+    assert.equal(decoder.decodeInt32(), 0x63620061);
+
+    // Decode unsigned numbers.
+    decoder.reset('AQ==');
+    assert.equal(decoder.decodeUInt8(), 0x01);
+
+    decoder.reset('AQI=');
+    assert.equal(decoder.decodeUInt16(), 0x0201);
+
+    decoder.reset('AQIDBA==');
+    assert.equal(decoder.decodeUInt32(), 0x04030201);
+
+    decoder.reset('AQIDBAUGBwg=');
+    assert.strictEqual(decoder.decodeUInt64ToString(), '0807060504030201');
+
+    // Decode signed numbers.
+    decoder.reset('AQ==');
+    assert.equal(decoder.decodeInt8(), 0x01);
+
+    decoder.reset('AQI=');
+    assert.equal(decoder.decodeInt16(), 0x0201);
+
+    decoder.reset('AQIDBA==');
+    assert.equal(decoder.decodeInt32(), 0x04030201);
+
+    decoder.reset('AQIDBAUGBwg=');
+    assert.strictEqual(decoder.decodeInt64ToString(), '0807060504030201');
+
+    // Last value before being a signed number.
+    decoder.reset('fw==');
+    assert.equal(decoder.decodeInt8(), 127);
+
+    // Decode negative numbers.
+    decoder.reset('1g==');
+    assert.equal(decoder.decodeInt8(), -42);
+
+    decoder.reset('gA==');
+    assert.equal(decoder.decodeInt8(), -128);
+
+    decoder.reset('hYI=');
+    assert.equal(decoder.decodeInt16(), -32123);
+
+    decoder.reset('hYL//w==');
+    assert.equal(decoder.decodeInt32(), -32123);
+
+    decoder.reset('Lv1ptv////8=');
+    assert.equal(decoder.decodeInt32(), -1234567890);
+
+    // Decode number with zero (nul) in the middle of the string.
+    decoder.reset('YQBiYw==');
+    assert.equal(decoder.decodeInt32(), 0x63620061);
+  });
+
+  test('decodeUInteger', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+
+    decoder.reset('AQIDBAUGBwg=');
+    assert.equal(decoder.decodeUInteger(false), 0x04030201);
+
+    decoder.reset('AQIDBAUGBwg=');
+    assert.strictEqual(decoder.decodeUInteger(true), '0807060504030201');
+  });
+
+  test('decodeString', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+
+    decoder.reset('dGVzdAA=');
+    assert.strictEqual(decoder.decodeString(), 'test');
+
+    decoder.reset('VGhpcyBpcyBhIHRlc3Qu');
+    assert.strictEqual(decoder.decodeString(), 'This is a test.');
+  });
+
+  test('decodeW16String', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    decoder.reset('dABlAHMAdAAAAA==');
+    assert.strictEqual(decoder.decodeW16String(), 'test');
+  });
+
+  test('decodeFixedW16String', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    decoder.reset('dABlAHMAdAAAAA==');
+    assert.strictEqual(decoder.decodeFixedW16String(32), 'test');
+    assert.equal(decoder.position_, 64);
+
+    decoder.reset('dABlAHMAdAAAAA==');
+    assert.strictEqual(decoder.decodeFixedW16String(1), 't');
+    assert.equal(decoder.position_, 2);
+  });
+
+  test('decodeBytes', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    decoder.reset('AAECAwQFBgc=');
+    var bytes = decoder.decodeBytes(8);
+    for (var i = 0; i < bytes.length; ++i)
+      assert.equal(bytes[i], i);
+  });
+
+  test('decodeSID', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+
+    // Decode a SID structure with 64-bit pointer.
+    decoder.reset(
+        'AQIDBAECAwQFBAMCAAAAAAEFAAAAAAAFFQAAAAECAwQFBgcICQoLDA0DAAA=');
+    var sid = decoder.decodeSID(true);
+
+    assert.strictEqual(sid.pSid, '0403020104030201');
+    assert.equal(sid.attributes, 0x02030405);
+    assert.equal(sid.sid.length, 20);
+  });
+
+  test('decodeSystemTime', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+
+    // Decode a SystemTime structure.
+    decoder.reset('AQACAAMABAAFAAYABwAIAA==');
+    var time = decoder.decodeSystemTime();
+    assert.equal(time.wYear, 1);
+    assert.equal(time.wMonth, 2);
+    assert.equal(time.wDayOfWeek, 3);
+    assert.equal(time.wDay, 4);
+    assert.equal(time.wHour, 5);
+    assert.equal(time.wMinute, 6);
+    assert.equal(time.wSecond, 7);
+    assert.equal(time.wMilliseconds, 8);
+  });
+
+  test('decodeTimeZoneInformation', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+
+    // Decode a TimeZoneInformation structure.
+    decoder.reset('AQIDBGEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIAAwAEAAUABgAHAAgABA' +
+                  'MCAWIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+                  'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIAAwAEAAUABgAHAAgACAgI' +
+                  'CA==');
+    var time = decoder.decodeTimeZoneInformation();
+
+    assert.equal(time.bias, 0x04030201);
+    assert.equal(time.standardBias, 0x01020304);
+    assert.equal(time.daylightBias, 0x08080808);
+    assert.strictEqual(time.standardName, 'a');
+    assert.strictEqual(time.daylightName, 'b');
+  });
+
+  test('manageThreads', function() {
+    var events = [];
+    var model = 'dummy';
+    var importer = new tv.e.importer.etw.EtwImporter(model, events);
+
+    // After initialisation, no threads must exists.
+    assert.equal(Object.getOwnPropertyNames(importer.tidsToPid_).length, 0);
+
+    // Add some threads.
+    var thread10 = importer.createThreadIfNeeded(1, 10);
+    var thread11 = importer.createThreadIfNeeded(1, 11);
+    var thread20 = importer.createThreadIfNeeded(2, 20);
+
+    assert.equal(Object.getOwnPropertyNames(importer.tidsToPid_).length, 3);
+    assert.isTrue(importer.tidsToPid_.hasOwnProperty(10));
+    assert.isTrue(importer.tidsToPid_.hasOwnProperty(11));
+    assert.isTrue(importer.tidsToPid_.hasOwnProperty(20));
+
+    // Retrieve existing threads and processes.
+    var pid10 = importer.getPidFromWindowsTid(10);
+    var pid11 = importer.getPidFromWindowsTid(11);
+    var pid20 = importer.getPidFromWindowsTid(20);
+
+    assert.equal(pid10, 1);
+    assert.equal(pid11, 1);
+    assert.equal(pid20, 2);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/eventtrace_parser.html b/trace-viewer/trace_viewer/extras/importer/etw/eventtrace_parser.html
new file mode 100644
index 0000000..d023c9c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/eventtrace_parser.html
@@ -0,0 +1,110 @@
+<!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="/extras/importer/etw/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses EventTrace events in the Windows event trace format.
+ */
+
+tv.exportTo('tv.e.importer.etw', function() {
+  var Parser = tv.e.importer.etw.Parser;
+
+  // Constants for EventTrace events.
+  var guid = '68FDD900-4A3E-11D1-84F4-0000F80464E3';
+  var kEventTraceHeaderOpcode = 0;
+
+  /**
+   * Parses Windows EventTrace trace events.
+   * @constructor
+   */
+  function EventTraceParser(importer) {
+    Parser.call(this, importer);
+
+    // Register handlers.
+    importer.registerEventHandler(guid, kEventTraceHeaderOpcode,
+        EventTraceParser.prototype.decodeHeader.bind(this));
+  }
+
+  EventTraceParser.prototype = {
+    __proto__: Parser.prototype,
+
+    decodeFields: function(header, decoder) {
+      if (header.version != 2)
+        throw new Error('Incompatible EventTrace event version.');
+
+      var bufferSize = decoder.decodeUInt32();
+      var version = decoder.decodeUInt32();
+      var providerVersion = decoder.decodeUInt32();
+      var numberOfProcessors = decoder.decodeUInt32();
+      var endTime = decoder.decodeUInt64ToString();
+      var timerResolution = decoder.decodeUInt32();
+      var maxFileSize = decoder.decodeUInt32();
+      var logFileMode = decoder.decodeUInt32();
+      var buffersWritten = decoder.decodeUInt32();
+      var startBuffers = decoder.decodeUInt32();
+      var pointerSize = decoder.decodeUInt32();
+      var eventsLost = decoder.decodeUInt32();
+      var cpuSpeed = decoder.decodeUInt32();
+      var loggerName = decoder.decodeUInteger(header.is64);
+      var logFileName = decoder.decodeUInteger(header.is64);
+      var timeZoneInformation = decoder.decodeTimeZoneInformation();
+      var padding = decoder.decodeUInt32();
+      var bootTime = decoder.decodeUInt64ToString();
+      var perfFreq = decoder.decodeUInt64ToString();
+      var startTime = decoder.decodeUInt64ToString();
+      var reservedFlags = decoder.decodeUInt32();
+      var buffersLost = decoder.decodeUInt32();
+      var sessionNameString = decoder.decodeW16String();
+      var logFileNameString = decoder.decodeW16String();
+
+      return {
+        bufferSize: bufferSize,
+        version: version,
+        providerVersion: providerVersion,
+        numberOfProcessors: numberOfProcessors,
+        endTime: endTime,
+        timerResolution: timerResolution,
+        maxFileSize: maxFileSize,
+        logFileMode: logFileMode,
+        buffersWritten: buffersWritten,
+        startBuffers: startBuffers,
+        pointerSize: pointerSize,
+        eventsLost: eventsLost,
+        cpuSpeed: cpuSpeed,
+        loggerName: loggerName,
+        logFileName: logFileName,
+        timeZoneInformation: timeZoneInformation,
+        bootTime: bootTime,
+        perfFreq: perfFreq,
+        startTime: startTime,
+        reservedFlags: reservedFlags,
+        buffersLost: buffersLost,
+        sessionNameString: sessionNameString,
+        logFileNameString: logFileNameString
+      };
+    },
+
+    decodeHeader: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      // TODO(etienneb): Update the TraceModel with |fields|.
+      return true;
+    }
+
+  };
+
+  Parser.register(EventTraceParser);
+
+  return {
+    EventTraceParser: EventTraceParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/eventtrace_parser_test.html b/trace-viewer/trace_viewer/extras/importer/etw/eventtrace_parser_test.html
new file mode 100644
index 0000000..ec31a9d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/eventtrace_parser_test.html
@@ -0,0 +1,120 @@
+<!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/test_utils.html">
+<link rel="import" href="/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/extras/importer/etw/eventtrace_parser.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  // Constants for EventTrace events.
+  var guid = '68FDD900-4A3E-11D1-84F4-0000F80464E3';
+  var kEventTraceHeaderOpcode = 0;
+
+  var kEventTraceHeaderPayload32bitV2 =
+      'AAABAAYBAQWwHQAAEAAAABEs1WHICMwBYWECAGQAAAABAAAAAwAAAAEAAAAEAAAAAAAAA' +
+      'FoJAAAFAAAABgAAACwBAABAAHQAegByAGUAcwAuAGQAbABsACwALQAxADEAMgAAAAAAAA' +
+      'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAQACAAAAAAAAAAAAAABAAHQ' +
+      'AegByAGUAcwAuAGQAbABsACwALQAxADEAMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
+      'AAAAAAAAAAAAAAADAAAAAgACAAAAAAAAAMT///8AAAAAf0Ob368FzAGdrCMAAAAAACw0o' +
+      '2DICMwBAQAAAAAAAABNAGEAawBlACAAVABlAHMAdAAgAEQAYQB0AGEAIABTAGUAcwBzAG' +
+      'kAbwBuAAAAYwA6AFwAcwByAGMAXABzAGEAdwBiAHUAYwBrAFwAdAByAHUAbgBrAFwAcwB' +
+      'yAGMAXABzAGEAdwBiAHUAYwBrAFwAbABvAGcAXwBsAGkAYgBcAHQAZQBzAHQAXwBkAGEA' +
+      'dABhAFwAaQBtAGEAZwBlAF8AZABhAHQAYQBfADMAMgBfAHYAMAAuAGUAdABsAAAA';
+
+  var kEventTraceHeaderPayload64bitV2 =
+      'AAABAAYBAQWxHQAABAAAADsuzRRYLM8BYWECAAAAAAABAAEAtgEAAAEAAAAIAAAAHwAAA' +
+      'KAGAAAAAAAAAAAAAAAAAAAAAAAALAEAAEAAdAB6AHIAZQBzAC4AZABsAGwALAAtADEAMQ' +
+      'AyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAA' +
+      'AAAAAAEAAdAB6AHIAZQBzAC4AZABsAGwALAAtADEAMQAxAAAAAAAAAAAAAAAAAAAAAAAA' +
+      'AAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///wAAAABZQyWiwCvPAX1GG' +
+      'QAAAAAALWSZBFgszwEBAAAAAAAAAFIAZQBsAG8AZwBnAGUAcgAAAEMAOgBcAGsAZQByAG' +
+      '4AZQBsAC4AZQB0AGwAAAA=';
+
+  test('DecodeFields', function() {
+
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    var parser = new tv.e.importer.etw.EventTraceParser(importer);
+    var header;
+    var fields;
+
+    // Validate a version 2 32-bit payload.
+    header = {
+      guid: guid, opcode: kEventTraceHeaderOpcode, version: 2, is64: 0
+    };
+    decoder.reset(kEventTraceHeaderPayload32bitV2);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.bufferSize, 65536);
+    assert.equal(fields.version, 83951878);
+    assert.equal(fields.providerVersion, 7600);
+    assert.equal(fields.numberOfProcessors, 16);
+    assert.strictEqual(fields.endTime, '01cc08c861d52c11');
+    assert.equal(fields.timerResolution, 156001);
+    assert.equal(fields.maxFileSize, 100);
+    assert.equal(fields.logFileMode, 1);
+    assert.equal(fields.buffersWritten, 3);
+    assert.equal(fields.startBuffers, 1);
+    assert.equal(fields.pointerSize, 4);
+    assert.equal(fields.eventsLost, 0);
+    assert.equal(fields.cpuSpeed, 2394);
+    assert.equal(fields.loggerName, 5);
+    assert.equal(fields.logFileName, 6);
+    assert.strictEqual(fields.timeZoneInformation.standardName,
+                       '@tzres.dll,-112');
+    assert.strictEqual(fields.timeZoneInformation.daylightName,
+                       '@tzres.dll,-111');
+    assert.strictEqual(fields.bootTime, '01cc05afdf9b437f');
+    assert.strictEqual(fields.perfFreq, '000000000023ac9d');
+    assert.strictEqual(fields.startTime, '01cc08c860a3342c');
+    assert.equal(fields.reservedFlags, 1);
+    assert.equal(fields.buffersLost, 0);
+    assert.strictEqual(fields.sessionNameString, 'Make Test Data Session');
+    assert.strictEqual(fields.logFileNameString,
+                       'c:\\src\\sawbuck\\trunk\\src\\sawbuck\\log_lib\\' +
+                       'test_data\\image_data_32_v0.etl');
+
+    // Validate a version 2 64-bit payload.
+    header = {
+      guid: guid, opcode: kEventTraceHeaderOpcode, version: 2, is64: 1
+    };
+    decoder.reset(kEventTraceHeaderPayload64bitV2);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.bufferSize, 65536);
+    assert.equal(fields.version, 83951878);
+    assert.equal(fields.providerVersion, 7601);
+    assert.equal(fields.numberOfProcessors, 4);
+    assert.strictEqual(fields.endTime, '01cf2c5814cd2e3b');
+    assert.equal(fields.timerResolution, 156001);
+    assert.equal(fields.maxFileSize, 0);
+    assert.equal(fields.logFileMode, 0x10001);
+    assert.equal(fields.buffersWritten, 438);
+    assert.equal(fields.startBuffers, 1);
+    assert.equal(fields.pointerSize, 8);
+    assert.equal(fields.eventsLost, 31);
+    assert.equal(fields.cpuSpeed, 1696);
+    assert.equal(fields.loggerName, 0);
+    assert.equal(fields.logFileName, 0);
+    assert.strictEqual(fields.timeZoneInformation.standardName,
+        '@tzres.dll,-112');
+    assert.strictEqual(fields.timeZoneInformation.daylightName,
+        '@tzres.dll,-111');
+    assert.strictEqual(fields.bootTime, '01cf2bc0a2254359');
+    assert.strictEqual(fields.perfFreq, '000000000019467d');
+    assert.strictEqual(fields.startTime, '01cf2c580499642d');
+    assert.equal(fields.reservedFlags, 1);
+    assert.equal(fields.buffersLost, 0);
+    assert.strictEqual(fields.sessionNameString, 'Relogger');
+    assert.strictEqual(fields.logFileNameString, 'C:\\kernel.etl');
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/parser.html b/trace-viewer/trace_viewer/extras/importer/etw/parser.html
new file mode 100644
index 0000000..626b58a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/parser.html
@@ -0,0 +1,73 @@
+<!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 Base class for Windows ETW event parsers.
+ *
+ * The ETW trace event importer depends on subclasses of
+ * Parser to parse event data.  Each subclass corresponds
+ * to a group of trace events; e.g. Thread and Process implements
+ * decoding of scheduling events.  Parser subclasses must
+ * call Parser.register to arrange to be instantiated
+ * and their constructor must register their event handlers with the
+ * importer.  For example,
+ *
+ * var Parser = tv.e.importer.etw.Parser;
+ *
+ * function ThreadParser(importer) {
+ *   Parser.call(this, importer);
+ *
+ *   importer.registerEventHandler(guid, kThreadStartOpcode,
+ *       ThreadParser.prototype.decodeStart.bind(this));
+ *   importer.registerEventHandler(guid, kThreadEndOpcode,
+ *       ThreadParser.prototype.decodeEnd.bind(this));
+ * }
+ *
+ * Parser.register(ThreadParser);
+ *
+ * When a registered event is found, the associated event handler is invoked:
+ *
+ *   decodeStart: function(header, decoder) {
+ *     [...]
+ *     return true;
+ *   },
+ *
+ * If the routine returns false the caller will generate an import error
+ * saying there was a problem parsing it.  Handlers can also emit import
+ * messages using this.importer.model.importWarning.  If this is done in lieu of
+ * the generic import error it may be desirable for the handler to return
+ * true.
+ *
+ */
+tv.exportTo('tv.e.importer.etw', function() {
+  /**
+   * Parses Windows ETW events.
+   * @constructor
+   */
+  function Parser(importer) {
+    this.importer = importer;
+    this.model = importer.model;
+  }
+
+  Parser.prototype = {
+    __proto__: Object.prototype
+  };
+
+  var options = new tv.b.ExtensionRegistryOptions(tv.b.BASIC_REGISTRY_MODE);
+  options.mandatoryBaseClass = Parser;
+  tv.b.decorateExtensionRegistry(Parser, options);
+
+
+  return {
+    Parser: Parser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/process_parser.html b/trace-viewer/trace_viewer/extras/importer/etw/process_parser.html
new file mode 100644
index 0000000..cd489a2
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/process_parser.html
@@ -0,0 +1,181 @@
+<!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="/extras/importer/etw/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses processes events in the Windows event trace format.
+ *
+ * The Windows process events are:
+ *
+ * - DCStart: Describes a process that was already running when the trace
+ *    started. ETW automatically generates these events for all running
+ *    processes at the beginning of the trace.
+ * - Start: Describes a process launched during the tracing session.
+ * - End: Describes a process that ended during the tracing session.
+ * - DCEnd: Describes a process that was still running when the trace ended.
+ *
+ * See http://msdn.microsoft.com/library/windows/desktop/aa364092.aspx
+ */
+tv.exportTo('tv.e.importer.etw', function() {
+  var Parser = tv.e.importer.etw.Parser;
+
+  // Constants for Process events.
+  var guid = '3D6FA8D0-FE05-11D0-9DDA-00C04FD7BA7C';
+  var kProcessStartOpcode = 1;
+  var kProcessEndOpcode = 2;
+  var kProcessDCStartOpcode = 3;
+  var kProcessDCEndOpcode = 4;
+  var kProcessDefunctOpcode = 39;
+
+  /**
+   * Parses Windows process trace events.
+   * @constructor
+   */
+  function ProcessParser(importer) {
+    Parser.call(this, importer);
+
+    // Register handlers.
+    importer.registerEventHandler(guid, kProcessStartOpcode,
+        ProcessParser.prototype.decodeStart.bind(this));
+    importer.registerEventHandler(guid, kProcessEndOpcode,
+        ProcessParser.prototype.decodeEnd.bind(this));
+    importer.registerEventHandler(guid, kProcessDCStartOpcode,
+        ProcessParser.prototype.decodeDCStart.bind(this));
+    importer.registerEventHandler(guid, kProcessDCEndOpcode,
+        ProcessParser.prototype.decodeDCEnd.bind(this));
+    importer.registerEventHandler(guid, kProcessDefunctOpcode,
+        ProcessParser.prototype.decodeDefunct.bind(this));
+  }
+
+  ProcessParser.prototype = {
+    __proto__: Parser.prototype,
+
+    decodeFields: function(header, decoder) {
+      if (header.version > 5)
+        throw new Error('Incompatible Process event version.');
+
+      var pageDirectoryBase;
+      if (header.version == 1)
+        pageDirectoryBase = decoder.decodeUInteger(header.is64);
+
+      var uniqueProcessKey;
+      if (header.version >= 2)
+        uniqueProcessKey = decoder.decodeUInteger(header.is64);
+
+      var processId = decoder.decodeUInt32();
+      var parentId = decoder.decodeUInt32();
+
+      var sessionId;
+      var exitStatus;
+      if (header.version >= 1) {
+        sessionId = decoder.decodeUInt32();
+        exitStatus = decoder.decodeInt32();
+      }
+
+      var directoryTableBase;
+      if (header.version >= 3)
+        directoryTableBase = decoder.decodeUInteger(header.is64);
+
+      var flags;
+      if (header.version >= 4)
+        flags = decoder.decodeUInt32();
+
+      var userSID = decoder.decodeSID(header.is64);
+
+      var imageFileName;
+      if (header.version >= 1)
+        imageFileName = decoder.decodeString();
+
+      var commandLine;
+      if (header.version >= 2)
+        commandLine = decoder.decodeW16String();
+
+      var packageFullName;
+      var applicationId;
+      if (header.version >= 4) {
+        packageFullName = decoder.decodeW16String();
+        applicationId = decoder.decodeW16String();
+      }
+
+      var exitTime;
+      if (header.version == 5 && header.opcode == kProcessDefunctOpcode)
+        exitTime = decoder.decodeUInt64ToString();
+
+      return {
+        pageDirectoryBase: pageDirectoryBase,
+        uniqueProcessKey: uniqueProcessKey,
+        processId: processId,
+        parentId: parentId,
+        sessionId: sessionId,
+        exitStatus: exitStatus,
+        directoryTableBase: directoryTableBase,
+        flags: flags,
+        userSID: userSID,
+        imageFileName: imageFileName,
+        commandLine: commandLine,
+        packageFullName: packageFullName,
+        applicationId: applicationId,
+        exitTime: exitTime
+      };
+    },
+
+    decodeStart: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      var process = this.model.getOrCreateProcess(fields.processId);
+      if (process.hasOwnProperty('has_ended')) {
+        // On Windows, a process ID used by a process could be reused as soon as
+        // the process ends (there is no pid cycling like on Linux). However, in
+        // a short trace, this is unlikely to happen.
+        throw new Error('Process clash detected.');
+      }
+      process.name = fields.imageFileName;
+      return true;
+    },
+
+    decodeEnd: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      var process = this.model.getOrCreateProcess(fields.processId);
+      process.has_ended = true;
+      return true;
+    },
+
+    decodeDCStart: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      var process = this.model.getOrCreateProcess(fields.processId);
+      if (process.hasOwnProperty('has_ended'))
+        throw new Error('Process clash detected.');
+      process.name = fields.imageFileName;
+      return true;
+    },
+
+    decodeDCEnd: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      var process = this.model.getOrCreateProcess(fields.processId);
+      process.has_ended = true;
+      return true;
+    },
+
+    decodeDefunct: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      // TODO(etienneb): Update the TraceModel with |fields|.
+      return true;
+    }
+
+  };
+
+  Parser.register(ProcessParser);
+
+  return {
+    ProcessParser: ProcessParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/process_parser_test.html b/trace-viewer/trace_viewer/extras/importer/etw/process_parser_test.html
new file mode 100644
index 0000000..fabcf62
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/process_parser_test.html
@@ -0,0 +1,150 @@
+<!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/test_utils.html">
+<link rel="import" href="/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/extras/importer/etw/process_parser.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  // Constants for Process events.
+  var guid = '3D6FA8D0-FE05-11D0-9DDA-00C04FD7BA7C';
+  var kProcessStartOpcode = 1;
+  var kProcessDefunctOpcode = 39;
+
+  var kProcessStartPayload32bitV1 =
+      'AAAAAPAGAADcAwAAAQAAAAMBAAAAAAAAAAAAAAEFAAAAAAAFFQAAAJYs7Cxo/TEG8dyk0' +
+      '+gDAABub3RlcGFkLmV4ZQA=';
+
+  var kProcessStartPayload32bitV2 =
+      'AAAAAPAGAADcAwAAAQAAAAMBAAAAAAAAAAAAAAEFAAAAAAAFFQAAAJYs7Cxo/TEG8dyk0' +
+      '+gDAABub3RlcGFkLmV4ZQAiAEMAOgBcAFcAaQBuAGQAbwB3AHMAXABzAHkAcwB0AGUAbQ' +
+      'AzADIAXABuAG8AdABlAHAAYQBkAC4AZQB4AGUAIgAgAAAA';
+
+  var kProcessStartPayload32bitV3 =
+      'AAAAAPAGAADcAwAAAQAAAAMBAAAAAAAAAAAAAAAAAAABBQAAAAAABRUAAACWLOwsaP0xB' +
+      'vHcpNPoAwAAbm90ZXBhZC5leGUAIgBDADoAXABXAGkAbgBkAG8AdwBzAFwAcwB5AHMAdA' +
+      'BlAG0AMwAyAFwAbgBvAHQAZQBwAGEAZAAuAGUAeABlACIAIAAAAA==';
+
+  var kProcessStartPayload64bitV3 =
+      'YIBiD4D6//8AGgAAoBwAAAEAAAADAQAAAPBDHQEAAAAwVlMVoPj//wAAAACg+P//AQUAA' +
+      'AAAAAUVAAAAAgMBAgMEBQYHCAkKCwwAAHhwZXJmLmV4ZQB4AHAAZQByAGYAIAAgAC0AZA' +
+      'AgAG8AdQB0AC4AZQB0AGwAAAA=';
+
+  var kProcessStartPayload64bitV4 =
+      'gED8GgDg//+MCgAACBcAAAUAAAADAQAAALCiowAAAAAAAAAAkPBXBADA//8AAAAAAAAAA' +
+      'AEFAAAAAAAFFQAAAAECAwQFBgcICQoLBukDAAB4cGVyZi5leGUAeABwAGUAcgBmACAAIA' +
+      'AtAHMAdABvAHAAAAAAAAAA';
+
+  var kProcessDefunctPayload64bitV5 =
+      'wMXyBgDg//9IGQAAEAgAAAEAAAAAAAAAAGDLTwAAAAAAAAAA8OU7AwDA//8AAAAAAAAMA' +
+      'AEFAAAAAAAFFQAAAMDBwsPExcbH0NHS09QDAABjaHJvbWUuZXhlAAAAAAAAAI1Jovns+s' +
+      '4B';
+
+  test('DecodeFields', function() {
+
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    var parser = new tv.e.importer.etw.ProcessParser(importer);
+    var header;
+    var fields;
+
+    // Validate a version 1 32-bit payload.
+    header = { guid: guid, opcode: kProcessStartOpcode, version: 1, is64: 0 };
+    decoder.reset(kProcessStartPayload32bitV1);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.pageDirectoryBase, 0);
+    assert.equal(fields.processId, 1776);
+    assert.equal(fields.parentId, 988);
+    assert.equal(fields.sessionId, 1);
+    assert.equal(fields.exitStatus, 259);
+    assert.strictEqual(fields.imageFileName, 'notepad.exe');
+
+    // Validate a version 2 32-bit payload.
+    header = { guid: guid, opcode: kProcessStartOpcode, version: 2, is64: 0 };
+    decoder.reset(kProcessStartPayload32bitV2);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.uniqueProcessKey, 0);
+    assert.equal(fields.processId, 1776);
+    assert.equal(fields.parentId, 988);
+    assert.equal(fields.sessionId, 1);
+    assert.equal(fields.exitStatus, 259);
+    assert.strictEqual(fields.imageFileName, 'notepad.exe');
+    assert.strictEqual(fields.commandLine,
+               '\"C:\\Windows\\system32\\notepad.exe\" ');
+
+    // Validate a version 3 32-bit payload.
+    header = { guid: guid, opcode: kProcessStartOpcode, version: 3, is64: 0 };
+    decoder.reset(kProcessStartPayload32bitV3);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.uniqueProcessKey, 0);
+    assert.equal(fields.processId, 1776);
+    assert.equal(fields.parentId, 988);
+    assert.equal(fields.sessionId, 1);
+    assert.equal(fields.exitStatus, 259);
+    assert.equal(fields.directoryTableBase, 0);
+    assert.strictEqual(fields.imageFileName, 'notepad.exe');
+    assert.strictEqual(fields.commandLine,
+               '\"C:\\Windows\\system32\\notepad.exe\" ');
+
+    // Validate a version 3 64-bit payload.
+    header = { guid: guid, opcode: kProcessStartOpcode, version: 3, is64: 1 };
+    decoder.reset(kProcessStartPayload64bitV3);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.strictEqual(fields.uniqueProcessKey, 'fffffa800f628060');
+    assert.equal(fields.processId, 6656);
+    assert.equal(fields.parentId, 7328);
+    assert.equal(fields.sessionId, 1);
+    assert.equal(fields.exitStatus, 259);
+    assert.strictEqual(fields.directoryTableBase, '000000011d43f000');
+    assert.strictEqual(fields.imageFileName, 'xperf.exe');
+    assert.strictEqual(fields.commandLine, 'xperf  -d out.etl');
+
+    // Validate a version 4 64-bit payload.
+    header = { guid: guid, opcode: kProcessStartOpcode, version: 4, is64: 1 };
+    decoder.reset(kProcessStartPayload64bitV4);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.uniqueProcessKey, 'ffffe0001afc4080');
+    assert.equal(fields.processId, 2700);
+    assert.equal(fields.parentId, 5896);
+    assert.equal(fields.sessionId, 5);
+    assert.equal(fields.exitStatus, 259);
+    assert.equal(fields.directoryTableBase, '00000000a3a2b000');
+    assert.equal(fields.flags, 0);
+    assert.strictEqual(fields.imageFileName, 'xperf.exe');
+    assert.strictEqual(fields.commandLine, 'xperf  -stop');
+    assert.strictEqual(fields.packageFullName, '');
+    assert.strictEqual(fields.applicationId, '');
+
+    // Validate a version 5 64-bit payload.
+    header = { guid: guid, opcode: kProcessDefunctOpcode, version: 5, is64: 1 };
+    decoder.reset(kProcessDefunctPayload64bitV5);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.strictEqual(fields.uniqueProcessKey, 'ffffe00006f2c5c0');
+    assert.equal(fields.processId, 6472);
+    assert.equal(fields.parentId, 2064);
+    assert.equal(fields.sessionId, 1);
+    assert.equal(fields.exitStatus, 0);
+    assert.strictEqual(fields.directoryTableBase, '000000004fcb6000');
+    assert.equal(fields.flags, 0);
+    assert.strictEqual(fields.imageFileName, 'chrome.exe');
+    assert.strictEqual(fields.commandLine, '');
+    assert.strictEqual(fields.packageFullName, '');
+    assert.strictEqual(fields.applicationId, '');
+    assert.strictEqual(fields.exitTime, '01cefaecf9a2498d');
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/thread_parser.html b/trace-viewer/trace_viewer/extras/importer/etw/thread_parser.html
new file mode 100644
index 0000000..659e5fa
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/thread_parser.html
@@ -0,0 +1,243 @@
+<!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="/extras/importer/etw/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses threads events in the Windows event trace format.
+ *
+ * The Windows thread events are:
+ *
+ * - DCStart: Describes a thread that was already running when the trace
+ *    started. ETW automatically generates these events for all running
+ *    threads at the beginning of the trace.
+ * - Start: Describes a thread that started during the tracing session.
+ * - End: Describes a thread that ended during the tracing session.
+ * - DCEnd: Describes a thread that was still alive when the trace ended.
+ *
+ * See http://msdn.microsoft.com/library/windows/desktop/aa364132.aspx
+ */
+tv.exportTo('tv.e.importer.etw', function() {
+  var Parser = tv.e.importer.etw.Parser;
+
+  // Constants for Thread events.
+  var guid = '3D6FA8D1-FE05-11D0-9DDA-00C04FD7BA7C';
+  var kThreadStartOpcode = 1;
+  var kThreadEndOpcode = 2;
+  var kThreadDCStartOpcode = 3;
+  var kThreadDCEndOpcode = 4;
+  var kThreadCSwitchOpcode = 36;
+
+  /**
+   * Parses Windows threads trace events.
+   * @constructor
+   */
+  function ThreadParser(importer) {
+    Parser.call(this, importer);
+
+    // Register handlers.
+    importer.registerEventHandler(guid, kThreadStartOpcode,
+        ThreadParser.prototype.decodeStart.bind(this));
+    importer.registerEventHandler(guid, kThreadEndOpcode,
+        ThreadParser.prototype.decodeEnd.bind(this));
+    importer.registerEventHandler(guid, kThreadDCStartOpcode,
+        ThreadParser.prototype.decodeDCStart.bind(this));
+    importer.registerEventHandler(guid, kThreadDCEndOpcode,
+        ThreadParser.prototype.decodeDCEnd.bind(this));
+    importer.registerEventHandler(guid, kThreadCSwitchOpcode,
+        ThreadParser.prototype.decodeCSwitch.bind(this));
+  }
+
+  ThreadParser.prototype = {
+    __proto__: Parser.prototype,
+
+    decodeFields: function(header, decoder) {
+      if (header.version > 3)
+        throw new Error('Incompatible Thread event version.');
+
+      // Common fields to all Thread events.
+      var processId = decoder.decodeUInt32();
+      var threadId = decoder.decodeUInt32();
+
+      // Extended fields.
+      var stackBase;
+      var stackLimit;
+      var userStackBase;
+      var userStackLimit;
+      var affinity;
+      var startAddr;
+      var win32StartAddr;
+      var tebBase;
+      var subProcessTag;
+      var basePriority;
+      var pagePriority;
+      var ioPriority;
+      var threadFlags;
+      var waitMode;
+
+      if (header.version == 1) {
+        // On version 1, only start events have extended information.
+        if (header.opcode == kThreadStartOpcode ||
+            header.opcode == kThreadDCStartOpcode) {
+          stackBase = decoder.decodeUInteger(header.is64);
+          stackLimit = decoder.decodeUInteger(header.is64);
+          userStackBase = decoder.decodeUInteger(header.is64);
+          userStackLimit = decoder.decodeUInteger(header.is64);
+          startAddr = decoder.decodeUInteger(header.is64);
+          win32StartAddr = decoder.decodeUInteger(header.is64);
+          waitMode = decoder.decodeInt8();
+          decoder.skip(3);
+        }
+      } else {
+        stackBase = decoder.decodeUInteger(header.is64);
+        stackLimit = decoder.decodeUInteger(header.is64);
+        userStackBase = decoder.decodeUInteger(header.is64);
+        userStackLimit = decoder.decodeUInteger(header.is64);
+
+        // Version 2 produces a field named 'startAddr'.
+        if (header.version == 2)
+          startAddr = decoder.decodeUInteger(header.is64);
+        else
+          affinity = decoder.decodeUInteger(header.is64);
+
+        win32StartAddr = decoder.decodeUInteger(header.is64);
+        tebBase = decoder.decodeUInteger(header.is64);
+        subProcessTag = decoder.decodeUInt32();
+
+        if (header.version == 3) {
+          basePriority = decoder.decodeUInt8();
+          pagePriority = decoder.decodeUInt8();
+          ioPriority = decoder.decodeUInt8();
+          threadFlags = decoder.decodeUInt8();
+        }
+      }
+
+      return {
+        processId: processId,
+        threadId: threadId,
+        stackBase: stackBase,
+        stackLimit: stackLimit,
+        userStackBase: userStackBase,
+        userStackLimit: userStackLimit,
+        affinity: affinity,
+        startAddr: startAddr,
+        win32StartAddr: win32StartAddr,
+        tebBase: tebBase,
+        subProcessTag: subProcessTag,
+        waitMode: waitMode,
+        basePriority: basePriority,
+        pagePriority: pagePriority,
+        ioPriority: ioPriority,
+        threadFlags: threadFlags
+      };
+    },
+
+    decodeCSwitchFields: function(header, decoder) {
+      if (header.version != 2)
+        throw new Error('Incompatible Thread event version.');
+
+      // Decode CSwitch payload.
+      var newThreadId = decoder.decodeUInt32();
+      var oldThreadId = decoder.decodeUInt32();
+      var newThreadPriority = decoder.decodeInt8();
+      var oldThreadPriority = decoder.decodeInt8();
+      var previousCState = decoder.decodeUInt8();
+      var spareByte = decoder.decodeInt8();
+      var oldThreadWaitReason = decoder.decodeInt8();
+      var oldThreadWaitMode = decoder.decodeInt8();
+      var oldThreadState = decoder.decodeInt8();
+      var oldThreadWaitIdealProcessor = decoder.decodeInt8();
+      var newThreadWaitTime = decoder.decodeUInt32();
+      var reserved = decoder.decodeUInt32();
+
+      return {
+        newThreadId: newThreadId,
+        oldThreadId: oldThreadId,
+        newThreadPriority: newThreadPriority,
+        oldThreadPriority: oldThreadPriority,
+        previousCState: previousCState,
+        spareByte: spareByte,
+        oldThreadWaitReason: oldThreadWaitReason,
+        oldThreadWaitMode: oldThreadWaitMode,
+        oldThreadState: oldThreadState,
+        oldThreadWaitIdealProcessor: oldThreadWaitIdealProcessor,
+        newThreadWaitTime: newThreadWaitTime,
+        reserved: reserved
+      };
+    },
+
+    decodeStart: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      this.importer.createThreadIfNeeded(fields.processId, fields.threadId);
+      return true;
+    },
+
+    decodeEnd: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      this.importer.removeThreadIfPresent(fields.threadId);
+      return true;
+    },
+
+    decodeDCStart: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      this.importer.createThreadIfNeeded(fields.processId, fields.threadId);
+      return true;
+    },
+
+    decodeDCEnd: function(header, decoder) {
+      var fields = this.decodeFields(header, decoder);
+      this.importer.removeThreadIfPresent(fields.threadId);
+      return true;
+    },
+
+    decodeCSwitch: function(header, decoder) {
+      var fields = this.decodeCSwitchFields(header, decoder);
+      var cpu = this.importer.getOrCreateCpu(header.cpu);
+      var new_thread =
+          this.importer.getThreadFromWindowsTid(fields.newThreadId);
+
+      // Generate the new thread name. If some events were lost, it's possible
+      // that information about the new thread or process is not available.
+      var new_thread_name;
+      if (new_thread && new_thread.userFriendlyName) {
+        new_thread_name = new_thread.userFriendlyName;
+      } else {
+        var new_process_id = this.importer.getPidFromWindowsTid(
+            fields.newThreadId);
+        var new_process = this.model.getProcess(new_process_id);
+        var new_process_name;
+        if (new_process)
+          new_process_name = new_process.name;
+        else
+          new_process_name = 'Unknown process';
+
+        new_thread_name =
+            new_process_name + ' (tid ' + fields.newThreadId + ')';
+      }
+
+      cpu.switchActiveThread(
+          header.timestamp,
+          {},
+          fields.newThreadId,
+          new_thread_name,
+          fields);
+      return true;
+    }
+
+  };
+
+  Parser.register(ThreadParser);
+
+  return {
+    ThreadParser: ThreadParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/etw/thread_parser_test.html b/trace-viewer/trace_viewer/extras/importer/etw/thread_parser_test.html
new file mode 100644
index 0000000..43e018a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/etw/thread_parser_test.html
@@ -0,0 +1,179 @@
+<!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/test_utils.html">
+<link rel="import" href="/extras/importer/etw/etw_importer.html">
+<link rel="import" href="/extras/importer/etw/thread_parser.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+
+  // Constants for Thread events.
+  var guid = '3D6FA8D1-FE05-11D0-9DDA-00C04FD7BA7C';
+  var kThreadStartOpcode = 1;
+  var kThreadEndOpcode = 2;
+  var kThreadDCStartOpcode = 3;
+  var kThreadCSwitchOpcode = 36;
+
+  var kThreadStartPayload32bitV1 =
+      'BAAAAEwHAAAAYLfzADC38wAAAAAAAAAAhdse9wAAAAD/AAAA';
+
+  var kThreadEndPayload32bitV1 = 'BAAAALQAAAA=';
+
+
+  var kThreadDCStartPayload64bitV2 =
+      'AAAAAAAAAAAAYPUCAPj//wAA9QIA+P//AAAAAAAAAAAAAAAAAAAAAIAlxwEA+P//gCXHA' +
+      'QD4//8AAAAAAAAAAAAAAAA=';
+
+  var kThreadStartPayload32bitV3 =
+      'LAIAACwTAAAAUJixACCYsQAA1QAAwNQAAwAAAOkDq3cA4P1/AAAAAAkFAgA=';
+
+  var kThreadStartPayload64bitV3 =
+      'eCEAAJQUAAAAMA4nAND//wDQDScA0P//MP0LBgAAAAAAgAsGAAAAAP8AAAAAAAAALP1YX' +
+      'AAAAAAAwBL/AAAAAAAAAAAIBQIA';
+
+  var kThreadCSwitchPayload32bitV2 = 'AAAAACwRAAAACQAAFwABABIAAAAmSAAA';
+  var kThreadCSwitchPayload64bitV2 = 'zAgAAAAAAAAIAAEAAAACBAEAAACHbYg0';
+
+  test('DecodeFields', function() {
+
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    var parser = new tv.e.importer.etw.ThreadParser(importer);
+    var header;
+    var fields;
+
+    // Validate a version 1 32-bit payload.
+    header = { guid: guid, opcode: kThreadStartOpcode, version: 1, is64: 0 };
+    decoder.reset(kThreadStartPayload32bitV1);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.processId, 4);
+    assert.equal(fields.threadId, 1868);
+    assert.equal(fields.stackBase, 4088881152);
+    assert.equal(fields.stackLimit, 4088868864);
+    assert.equal(fields.userStackBase, 0);
+    assert.equal(fields.userStackLimit, 0);
+    assert.equal(fields.startAddr, 4145994629);
+    assert.equal(fields.win32StartAddr, 0);
+    assert.equal(fields.waitMode, -1);
+
+    // Validate an End version 1 32-bit payload.
+    header = { guid: guid, opcode: kThreadEndOpcode, version: 1, is64: 0 };
+    decoder.reset(kThreadStartPayload32bitV1);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.processId, 4);
+    assert.equal(fields.threadId, 1868);
+
+    // Validate a version 2 64-bit payload.
+    header = { guid: guid, opcode: kThreadDCStartOpcode, version: 2, is64: 1 };
+    decoder.reset(kThreadDCStartPayload64bitV2);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.processId, 0);
+    assert.equal(fields.threadId, 0);
+    assert.strictEqual(fields.stackBase, 'fffff80002f56000');
+    assert.equal(fields.stackLimit, 'fffff80002f50000');
+    assert.strictEqual(fields.userStackBase, '0000000000000000');
+    assert.strictEqual(fields.userStackLimit, '0000000000000000');
+    assert.strictEqual(fields.startAddr, 'fffff80001c72580');
+    assert.strictEqual(fields.win32StartAddr, 'fffff80001c72580');
+    assert.strictEqual(fields.tebBase, '0000000000000000');
+    assert.equal(fields.subProcessTag, 0);
+
+    // Validate a version 3 32-bit payload.
+    header = { guid: guid, opcode: kThreadStartOpcode, version: 3, is64: 0 };
+    decoder.reset(kThreadStartPayload32bitV3);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.processId, 556);
+    assert.equal(fields.threadId, 4908);
+    assert.equal(fields.stackBase, 2979549184);
+    assert.equal(fields.stackLimit, 2979536896);
+    assert.equal(fields.userStackBase, 13959168);
+    assert.equal(fields.userStackLimit, 13942784);
+    assert.equal(fields.affinity, 3);
+    assert.equal(fields.win32StartAddr, 2007696361);
+    assert.equal(fields.tebBase, 2147344384);
+    assert.equal(fields.subProcessTag, 0);
+    assert.equal(fields.basePriority, 9);
+    assert.equal(fields.pagePriority, 5);
+    assert.equal(fields.ioPriority, 2);
+    assert.equal(fields.threadFlags, 0);
+
+    // Validate a version 3 64-bit payload.
+    header = { guid: guid, opcode: kThreadStartOpcode, version: 3, is64: 1 };
+    decoder.reset(kThreadStartPayload64bitV3);
+    fields = parser.decodeFields(header, decoder);
+
+    assert.equal(fields.processId, 8568);
+    assert.equal(fields.threadId, 5268);
+    assert.strictEqual(fields.stackBase, 'ffffd000270e3000');
+    assert.strictEqual(fields.stackLimit, 'ffffd000270dd000');
+    assert.strictEqual(fields.userStackBase, '00000000060bfd30');
+    assert.strictEqual(fields.userStackLimit, '00000000060b8000');
+    assert.strictEqual(fields.affinity, '00000000000000ff');
+    assert.strictEqual(fields.win32StartAddr, '000000005c58fd2c');
+    assert.strictEqual(fields.tebBase, '00000000ff12c000');
+    assert.equal(fields.subProcessTag, 0);
+    assert.equal(fields.basePriority, 8);
+    assert.equal(fields.pagePriority, 5);
+    assert.equal(fields.ioPriority, 2);
+    assert.equal(fields.threadFlags, 0);
+  });
+
+  test('DecodeCSwitchFields', function() {
+    var importer = new tv.e.importer.etw.EtwImporter('dummy', []);
+    var decoder = importer.decoder_;
+    var parser = new tv.e.importer.etw.ThreadParser(importer);
+    var header;
+    var fields;
+
+
+    // Validate a version 2 CSwitch 32-bit payload.
+    header = { guid: guid, opcode: kThreadCSwitchOpcode, version: 2, is64: 0 };
+    decoder.reset(kThreadCSwitchPayload32bitV2);
+    fields = parser.decodeCSwitchFields(header, decoder);
+
+    assert.equal(fields.newThreadId, 0);
+    assert.equal(fields.oldThreadId, 4396);
+    assert.equal(fields.newThreadPriority, 0);
+    assert.equal(fields.oldThreadPriority, 9);
+    assert.equal(fields.previousCState, 0);
+    assert.equal(fields.spareByte, 0);
+    assert.equal(fields.oldThreadWaitReason, 23);
+    assert.equal(fields.oldThreadWaitMode, 0);
+    assert.equal(fields.oldThreadState, 1);
+    assert.equal(fields.oldThreadWaitIdealProcessor, 0);
+    assert.equal(fields.newThreadWaitTime, 18);
+    assert.equal(fields.reserved, 18470);
+
+    // Validate a version 2 CSwitch 64-bit payload.
+    header = { guid: guid, opcode: kThreadCSwitchOpcode, version: 2, is64: 1 };
+    decoder.reset(kThreadCSwitchPayload64bitV2);
+    fields = parser.decodeCSwitchFields(header, decoder);
+
+    assert.equal(fields.newThreadId, 2252);
+    assert.equal(fields.oldThreadId, 0);
+    assert.equal(fields.newThreadPriority, 8);
+    assert.equal(fields.oldThreadPriority, 0);
+    assert.equal(fields.previousCState, 1);
+    assert.equal(fields.spareByte, 0);
+    assert.equal(fields.oldThreadWaitReason, 0);
+    assert.equal(fields.oldThreadWaitMode, 0);
+    assert.equal(fields.oldThreadState, 2);
+    assert.equal(fields.oldThreadWaitIdealProcessor, 4);
+    assert.equal(fields.newThreadWaitTime, 1);
+    assert.equal(fields.reserved, 881356167);
+
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/gzip_importer.html b/trace-viewer/trace_viewer/extras/importer/gzip_importer.html
new file mode 100644
index 0000000..d47b700
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/gzip_importer.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="/extras/importer/jszip.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview GzipImporter inflates gzip compressed data and passes it along
+ * to an actual importer.
+ */
+tv.exportTo('tv.e.importer', function() {
+  var Importer = tv.c.importer.Importer;
+
+  var GZIP_MEMBER_HEADER_ID_SIZE = 3;
+
+  var GZIP_HEADER_ID1 = 0x1f;
+  var GZIP_HEADER_ID2 = 0x8b;
+  var GZIP_DEFLATE_COMPRESSION = 8;
+
+  function GzipImporter(model, eventData) {
+    // Normalize the data into an Uint8Array.
+    if (typeof(eventData) === 'string' || eventData instanceof String) {
+      eventData = JSZip.utils.transformTo('uint8array', eventData);
+    } else if (eventData instanceof ArrayBuffer) {
+      eventData = new Uint8Array(eventData);
+    } else
+      throw new Error('Unknown gzip data format');
+    this.model_ = model;
+    this.gzipData_ = eventData;
+  }
+
+  /**
+   * @param {eventData} Possibly gzip compressed data as a string or an
+   *                    ArrayBuffer.
+   * @return {boolean} Whether obj looks like gzip compressed data.
+   */
+  GzipImporter.canImport = function(eventData) {
+    var header;
+    if (eventData instanceof ArrayBuffer)
+      header = new Uint8Array(eventData.slice(0, GZIP_MEMBER_HEADER_ID_SIZE));
+    else if (typeof(eventData) === 'string' || eventData instanceof String) {
+      header = eventData.substring(0, GZIP_MEMBER_HEADER_ID_SIZE);
+      // Convert the string to a byteArray for correct value comparison.
+      header = JSZip.utils.transformTo('uint8array', header);
+    } else
+      return false;
+    return header[0] == GZIP_HEADER_ID1 &&
+        header[1] == GZIP_HEADER_ID2 &&
+        header[2] == GZIP_DEFLATE_COMPRESSION;
+  };
+
+  /**
+   * Inflates (decompresses) the data stored in the given gzip bitstream.
+   * @return {string} Inflated data.
+   */
+  GzipImporter.inflateGzipData_ = function(data) {
+    var position = 0;
+
+    function getByte() {
+      if (position >= data.length)
+        throw new Error('Unexpected end of gzip data');
+      return data[position++];
+    }
+
+    function getWord() {
+      var low = getByte();
+      var high = getByte();
+      return (high << 8) + low;
+    }
+
+    function skipBytes(amount) {
+      position += amount;
+    }
+
+    function skipZeroTerminatedString() {
+      while (getByte() != 0) {}
+    }
+
+    var id1 = getByte();
+    var id2 = getByte();
+    if (id1 !== GZIP_HEADER_ID1 || id2 !== GZIP_HEADER_ID2)
+      throw new Error('Not gzip data');
+    var compression_method = getByte();
+    if (compression_method !== GZIP_DEFLATE_COMPRESSION)
+      throw new Error('Unsupported compression method: ' + compression_method);
+    var flags = getByte();
+    var have_header_crc = flags & (1 << 1);
+    var have_extra_fields = flags & (1 << 2);
+    var have_file_name = flags & (1 << 3);
+    var have_comment = flags & (1 << 4);
+
+    // Skip modification time, extra flags and OS.
+    skipBytes(4 + 1 + 1);
+
+    // Skip remaining fields before compressed data.
+    if (have_extra_fields) {
+      var bytes_to_skip = getWord();
+      skipBytes(bytes_to_skip);
+    }
+    if (have_file_name)
+      skipZeroTerminatedString();
+    if (have_comment)
+      skipZeroTerminatedString();
+    if (have_header_crc)
+      getWord();
+
+    // Inflate the data using jszip.
+    var inflated_data =
+        JSZip.compressions['DEFLATE'].uncompress(data.subarray(position));
+    return JSZip.utils.transformTo('string', inflated_data);
+  },
+
+  GzipImporter.prototype = {
+    __proto__: Importer.prototype,
+
+    /**
+     * Called by the Model to check whether the importer just encapsulates
+     * the actual trace data which needs to be imported by another importer.
+     */
+    isTraceDataContainer: function() {
+      return true;
+    },
+
+    /**
+     * Called by the Model to extract subtraces from the event data. The
+     * subtraces are passed on to other importers that can recognize them.
+     */
+    extractSubtraces: function() {
+      var eventData = GzipImporter.inflateGzipData_(this.gzipData_);
+      return eventData ? [eventData] : [];
+    }
+  };
+
+  tv.c.importer.Importer.register(GzipImporter);
+
+  return {
+    GzipImporter: GzipImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/gzip_importer_test.html b/trace-viewer/trace_viewer/extras/importer/gzip_importer_test.html
new file mode 100644
index 0000000..8af5081
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/gzip_importer_test.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="/extras/importer/gzip_importer.html">
+<link rel="import" href="/core/test_utils.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var findSliceNamed = tv.c.test_utils.findSliceNamed;
+  var original_data =
+      '[{"name":"a","args":{},"pid":52,"ts":520,"cat":"foo","tid":53,' +
+      '"ph":"B"},{"name":"a","args":{},"pid":52,"ts":520,"cat":"foo",' +
+      '"tid":53,"ph":"E"}]\n';
+  var gzip_data_base64 =
+      'H4sICHr4HVIAA3RyYWNlAIuuVspLzE1VslJKVNJRSixKL1ayqq7VUSrITFGy' +
+      'MjXSUSopBtEGOkrJiSVAVWn5+UB1JWBZY6CyDKCYk1KtDhWMcVWqjeUCALak' +
+      'EH+QAAAA';
+
+  test('failImportEmpty', function() {
+    assert.isFalse(tv.e.importer.GzipImporter.canImport([]));
+    assert.isFalse(tv.e.importer.GzipImporter.canImport(''));
+  });
+
+  test('inflateString', function() {
+    // Test inflating the data from a string.
+    var gzip_data = atob(gzip_data_base64);
+    var importer = new tv.e.importer.GzipImporter(null, gzip_data);
+    assert.isTrue(tv.e.importer.GzipImporter.canImport(gzip_data));
+    assert.equal(importer.extractSubtraces()[0], original_data);
+  });
+
+  test('inflateArrayBuffer', function() {
+    // Test inflating the data from an ArrayBuffer.
+    var gzip_data = atob(gzip_data_base64);
+    var buffer = new ArrayBuffer(gzip_data.length);
+    var view = new Uint8Array(buffer);
+    for (var i = 0; i < gzip_data.length; i++)
+      view[i] = gzip_data.charCodeAt(i);
+    var importer = new tv.e.importer.GzipImporter(null, buffer);
+    assert.isTrue(tv.e.importer.GzipImporter.canImport(buffer));
+    assert.equal(importer.extractSubtraces()[0], original_data);
+  });
+
+  test('import', function() {
+    var gzip_data = atob(gzip_data_base64);
+    assert.isTrue(tv.e.importer.GzipImporter.canImport(gzip_data));
+
+    var model = new tv.c.TraceModel(gzip_data);
+    var threads = model.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var slice = findSliceNamed(threads[0].sliceGroup, 'a');
+    assert.equal(slice.category, 'foo');
+  });
+
+  test('importXMLHttpRequest', function() {
+    var req = new XMLHttpRequest();
+    var url = '/test_data/simple_trace_gz.gz';
+    req.open('GET', url, false);
+    req.overrideMimeType('text/plain; charset=x-user-defined');
+    req.send(null);
+    var gzip_data = req.responseText;
+    assert.isTrue(tv.e.importer.GzipImporter.canImport(gzip_data));
+
+    var model = new tv.c.TraceModel(gzip_data);
+    var threads = model.getAllThreads();
+    assert.equal(threads.length, 2);
+
+    var slice = findSliceNamed(threads[0].sliceGroup, 'B');
+    assert.equal(slice.category, 'PERF');
+
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/jszip.html b/trace-viewer/trace_viewer/extras/importer/jszip.html
new file mode 100644
index 0000000..1b93f27
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/jszip.html
@@ -0,0 +1,8 @@
+<!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.
+-->
+
+<script src="/jszip.min.js"></script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/android_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/android_parser.html
new file mode 100644
index 0000000..730c52b
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/android_parser.html
@@ -0,0 +1,239 @@
+<!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="/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/core/trace_model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses trace_marker events that were inserted in the trace by
+ * userland.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux trace mark events that were inserted in the trace by userland.
+   * @constructor
+   */
+  function AndroidParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('tracing_mark_write:android',
+        AndroidParser.prototype.traceMarkWriteAndroidEvent.bind(this));
+    importer.registerEventHandler('0:android',
+        AndroidParser.prototype.traceMarkWriteAndroidEvent.bind(this));
+
+    this.model_ = importer.model_;
+    this.ppids_ = {};
+  }
+
+  function parseArgs(argsString) {
+    var args = {};
+    if (argsString) {
+      var argsArray = argsString.split(';');
+      for (var i = 0; i < argsArray.length; ++i) {
+        var parts = argsArray[i].split('=');
+        if (parts[0])
+          args[parts.shift()] = parts.join('=');
+      }
+    }
+    return args;
+  }
+
+  AndroidParser.prototype = {
+    __proto__: Parser.prototype,
+
+    openAsyncSlice: function(thread, category, name, cookie, ts, args) {
+      var asyncSliceConstructor =
+         tv.c.trace_model.AsyncSlice.getConstructor(
+            category, name);
+      var slice = new asyncSliceConstructor(
+          category, name,
+          tv.b.ui.getColorIdForGeneralPurposeString(name), ts, args);
+      var key = category + ':' + name + ':' + cookie;
+      slice.id = cookie;
+      slice.startThread = thread;
+
+      if (!this.openAsyncSlices) {
+        this.openAsyncSlices = { };
+      }
+      this.openAsyncSlices[key] = slice;
+    },
+
+    closeAsyncSlice: function(thread, category, name, cookie, ts, args) {
+      if (!this.openAsyncSlices) {
+        // No async slices have been started.
+        return;
+      }
+
+      var key = category + ':' + name + ':' + cookie;
+      var slice = this.openAsyncSlices[key];
+      if (!slice) {
+        // No async slices w/ this key have been started.
+        return;
+      }
+
+      for (var arg in args) {
+        if (slice.args[arg] !== undefined) {
+          this.model_.importWarning({
+            type: 'parse_error',
+            message: 'Both the S and F events of ' + slice.title +
+                ' provided values for argument ' + arg + '.' +
+                ' The value of the F event will be used.'
+          });
+        }
+        slice.args[arg] = args[arg];
+      }
+
+      slice.endThread = thread;
+      slice.duration = ts - slice.start;
+      slice.startThread.asyncSliceGroup.push(slice);
+      slice.subSlices = [new tv.c.trace_model.Slice(slice.category,
+          slice.title, slice.colorId, slice.start, slice.args, slice.duration)];
+      delete this.openAsyncSlices[key];
+    },
+
+    traceMarkWriteAndroidEvent: function(eventName, cpuNumber, pid, ts,
+                                  eventBase) {
+      var eventData = eventBase.details.split('|');
+      switch (eventData[0]) {
+        case 'B':
+          var ppid = parseInt(eventData[1]);
+          var title = eventData[2];
+          var args = parseArgs(eventData[3]);
+          var category = eventData[4];
+          if (category === undefined)
+            category = 'android';
+
+          var thread = this.model_.getOrCreateProcess(ppid)
+              .getOrCreateThread(pid);
+          thread.name = eventBase.threadName;
+          if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(ts)) {
+            this.model_.importWarning({
+              type: 'parse_error',
+              message: 'Timestamps are moving backward.'
+            });
+            return false;
+          }
+
+          this.ppids_[pid] = ppid;
+          thread.sliceGroup.beginSlice(category, title, ts, args);
+
+          break;
+
+        case 'E':
+          var ppid = this.ppids_[pid];
+          if (ppid === undefined) {
+            // Silently ignore unmatched E events.
+            break;
+          }
+
+          var thread = this.model_.getOrCreateProcess(ppid)
+              .getOrCreateThread(pid);
+          if (!thread.sliceGroup.openSliceCount) {
+            // Silently ignore unmatched E events.
+            break;
+          }
+
+          var slice = thread.sliceGroup.endSlice(ts);
+
+          var args = parseArgs(eventData[3]);
+          for (var arg in args) {
+            if (slice.args[arg] !== undefined) {
+              this.model_.importWarning({
+                type: 'parse_error',
+                message: 'Both the B and E events of ' + slice.title +
+                    ' provided values for argument ' + arg + '.' +
+                    ' The value of the E event will be used.'
+              });
+            }
+            slice.args[arg] = args[arg];
+          }
+
+          break;
+
+        case 'C':
+          var ppid = parseInt(eventData[1]);
+          var name = eventData[2];
+          var value = parseInt(eventData[3]);
+          var category = eventData[4];
+          if (category === undefined)
+            category = 'android';
+
+          var ctr = this.model_.getOrCreateProcess(ppid)
+              .getOrCreateCounter(category, name);
+          // Initialize the counter's series fields if needed.
+          if (ctr.numSeries === 0) {
+            ctr.addSeries(new tv.c.trace_model.CounterSeries(value,
+                tv.b.ui.getColorIdForGeneralPurposeString(
+                    ctr.name + '.' + 'value')));
+          }
+
+          ctr.series.forEach(function(series) {
+            series.addCounterSample(ts, value);
+          });
+
+          break;
+
+        case 'S':
+          var ppid = parseInt(eventData[1]);
+          var name = eventData[2];
+          var cookie = parseInt(eventData[3]);
+          var args = parseArgs(eventData[4]);
+          var category = eventData[5];
+          if (category === undefined)
+            category = 'android';
+
+
+          var thread = this.model_.getOrCreateProcess(ppid)
+            .getOrCreateThread(pid);
+          thread.name = eventBase.threadName;
+
+          this.ppids_[pid] = ppid;
+          this.openAsyncSlice(thread, category, name, cookie, ts, args);
+
+          break;
+
+        case 'F':
+          // Note: An async slice may end on a different thread from the one
+          // that started it so this thread may not have been seen yet.
+          var ppid = parseInt(eventData[1]);
+
+          var name = eventData[2];
+          var cookie = parseInt(eventData[3]);
+          var args = parseArgs(eventData[4]);
+          var category = eventData[5];
+          if (category === undefined)
+            category = 'android';
+
+          var thread = this.model_.getOrCreateProcess(ppid)
+            .getOrCreateThread(pid);
+          thread.name = eventBase.threadName;
+
+          this.ppids_[pid] = ppid;
+          this.closeAsyncSlice(thread, category, name, cookie, ts, args);
+
+          break;
+
+        default:
+          return false;
+      }
+
+      return true;
+    }
+  };
+
+  Parser.register(AndroidParser);
+
+  return {
+    AndroidParser: AndroidParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/android_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/android_parser_test.html
new file mode 100644
index 0000000..99e4740
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/android_parser_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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('androidUserlandImport', function() {
+    var lines = [
+      'SurfaceFlinger-4831  [001] ...1 80909.598554: tracing_mark_write: B|4829|onMessageReceived', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598572: tracing_mark_write: B|4829|handleMessageInvalidate', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598590: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598604: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598627: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598651: tracing_mark_write: B|4829|updateTexImage', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598675: tracing_mark_write: B|4829|acquireBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598695: tracing_mark_write: B|4829|' + // @suppress longLineCheck
+          'com.android.launcher/com.android.launcher2.Launcher: 0',
+      'SurfaceFlinger-4831  [001] ...1 80909.598709: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598733: tracing_mark_write: C|4829|' + // @suppress longLineCheck
+          'com.android.launcher/com.android.launcher2.Launcher|0',
+      'SurfaceFlinger-4831  [001] ...1 80909.598746: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598844: tracing_mark_write: B|4829|releaseBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598862: tracing_mark_write: B|4829|' + // @suppress longLineCheck
+          'com.android.launcher/com.android.launcher2.Launcher: 2',
+      'SurfaceFlinger-4831  [001] ...1 80909.598876: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598892: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598925: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598955: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598988: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.599001: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599021: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.599036: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599068: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599087: tracing_mark_write: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599104: tracing_mark_write: E'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.parent.pid, 4829);
+    assert.equal(thread.tid, 4831);
+    assert.equal(thread.name, 'SurfaceFlinger');
+    assert.equal(thread.sliceGroup.length, 11);
+  });
+
+  test('androidUserlandImportWithSpacesInThreadName', function() {
+    var lines = [
+      'Surface Flinger -4831  [001] ...1 80909.598590: tracing_mark_write: B|4829|latchBuffer', // @suppress longLineCheck
+      'Surface Flinger -4831  [001] ...1 80909.598604: tracing_mark_write: E' // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.parent.pid, 4829);
+    assert.equal(thread.tid, 4831);
+    assert.equal(thread.name, 'Surface Flinger ');
+    assert.equal(thread.sliceGroup.length, 1);
+  });
+
+  test('androidAsyncUserlandImport', function() {
+    var lines = [
+      'ndroid.launcher-9649  ( 9649) [000] ...1 1990280.663276: ' +
+          'tracing_mark_write: S|9649|animator:childrenOutlineAlpha|' +
+          '1113053968',
+      'ndroid.launcher-9649  ( 9649) [000] ...1 1990280.781445: ' +
+          'tracing_mark_write: F|9649|animator:childrenOutlineAlpha|' +
+          '1113053968'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.parent.pid, 9649);
+    assert.equal(thread.tid, 9649);
+    assert.equal(thread.name, 'ndroid.launcher');
+    assert.equal(thread.sliceGroup.length, 0);
+    assert.equal(thread.asyncSliceGroup.length, 1);
+
+    var slice = thread.asyncSliceGroup.slices[0];
+    assert.equal(slice.title, 'animator:childrenOutlineAlpha');
+    assert.closeTo(118.169, slice.duration, 1e-5);
+  });
+
+  test('androidUserlandLegacyKernelImport', function() {
+    var lines = [
+      'SurfaceFlinger-4831  [001] ...1 80909.598554: 0: B|4829|onMessageReceived', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598572: 0: B|4829|handleMessageInvalidate', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598590: 0: B|4829|latchBuffer',
+      'SurfaceFlinger-4831  [001] ...1 80909.598604: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598627: 0: B|4829|latchBuffer',
+      'SurfaceFlinger-4831  [001] ...1 80909.598651: 0: B|4829|updateTexImage', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598675: 0: B|4829|acquireBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598695: 0: B|4829|' +
+          'com.android.launcher/com.android.launcher2.Launcher: 0',
+      'SurfaceFlinger-4831  [001] ...1 80909.598709: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598733: 0: C|4829|' +
+          'com.android.launcher/com.android.launcher2.Launcher|0',
+      'SurfaceFlinger-4831  [001] ...1 80909.598746: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598844: 0: B|4829|releaseBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.598862: 0: B|4829|' +
+          'com.android.launcher/com.android.launcher2.Launcher: 2',
+      'SurfaceFlinger-4831  [001] ...1 80909.598876: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598892: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598925: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598955: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.598988: 0: B|4829|latchBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.599001: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599021: 0: B|4829|latchBuffer', // @suppress longLineCheck
+      'SurfaceFlinger-4831  [001] ...1 80909.599036: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599068: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599087: 0: E',
+      'SurfaceFlinger-4831  [001] ...1 80909.599104: 0: E'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.parent.pid, 4829);
+    assert.equal(thread.tid, 4831);
+    assert.equal(thread.name, 'SurfaceFlinger');
+    assert.equal(thread.sliceGroup.length, 11);
+  });
+
+  test('androidUserlandChromiumImport', function() {
+    var lines = [
+      'SandboxedProces-2894  [001] ...1   253.780659: tracing_mark_write: B|2867|DoWorkLoop|arg1=1|cat1', // @suppress longLineCheck
+      'SandboxedProces-2894  [001] ...1   253.780671: tracing_mark_write: B|2867|DeferOrRunPendingTask|source=test=test;task=xyz|cat2', // @suppress longLineCheck
+      'SandboxedProces-2894  [001] ...1   253.780671: tracing_mark_write: E|2867|DeferOrRunPendingTask||cat1', // @suppress longLineCheck
+      'SandboxedProces-2894  [001] ...1   253.780686: tracing_mark_write: B|2867|MessageLoop::RunTask|source=ipc/ipc_sync_message_filter.cc:Send|cat2', // @suppress longLineCheck
+      'SandboxedProces-2894  [001] ...1   253.780700: tracing_mark_write: E|2867|MessageLoop::RunTask||cat1', // @suppress longLineCheck
+      'SandboxedProces-2894  [001] ...1   253.780750: tracing_mark_write: C|2867|counter1|10|cat1', // @suppress longLineCheck
+      'SandboxedProces-2894  [001] ...1   253.780859: tracing_mark_write: E|2867|DoWorkLoop|arg2=2|cat2', // @suppress longLineCheck
+      'SandboxedProces-2894  [000] ...1   255.663276: tracing_mark_write: S|2867|async|1113053968|arg1=1;arg2=2|cat1', // @suppress longLineCheck
+      'SandboxedProces-2894  [000] ...1   255.663276: tracing_mark_write: F|2867|async|1113053968|arg3=3|cat1', // @suppress longLineCheck
+      'SandboxedProces-2894  [000] ...1   255.663276: tracing_mark_write: trace_event_clock_sync: parent_ts=128' // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.parent.pid, 2867);
+    assert.equal(thread.tid, 2894);
+    assert.equal(thread.name, 'SandboxedProces');
+    assert.equal(thread.sliceGroup.length, 3);
+
+    assert.equal(thread.sliceGroup.slices[0].args['arg1'], '1');
+    assert.equal(thread.sliceGroup.slices[0].args['arg2'], '2');
+
+    assert.equal(thread.sliceGroup.slices[1].args['source'], 'test=test');
+    assert.equal(thread.sliceGroup.slices[1].category, 'cat2');
+    assert.equal('DeferOrRunPendingTask',
+                 thread.sliceGroup.slices[1].title);
+    assert.equal(thread.sliceGroup.slices[1].args['task'], 'xyz');
+
+    assert.equal('ipc/ipc_sync_message_filter.cc:Send',
+                 thread.sliceGroup.slices[2].args['source']);
+
+    assert.equal(thread.asyncSliceGroup.length, 1);
+    assert.equal(thread.asyncSliceGroup.slices[0].args['arg1'], '1');
+    assert.equal(thread.asyncSliceGroup.slices[0].args['arg2'], '2');
+    assert.equal(thread.asyncSliceGroup.slices[0].args['arg3'], '3');
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 1);
+    assert.equal(counters[0].category, 'cat1');
+    assert.equal(counters[0].name, 'counter1');
+
+    assert.equal(counters[0].numSamples, 1);
+    assert.equal(counters[0].getSeries(0).getSample(0).value, 10);
+
+    assert.equal(Math.round((253.780659 - (255.663276 - 128)) * 1000),
+                 Math.round(thread.sliceGroup.slices[0].start));
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/bus_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/bus_parser.html
new file mode 100644
index 0000000..7e066d4
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/bus_parser.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="/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/core/trace_model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses trace_marker events that were inserted in the trace by
+ * userland.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux trace mark events that were inserted in the trace by userland.
+   * @constructor
+   */
+  function BusParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('memory_bus_usage',
+        BusParser.prototype.traceMarkWriteBusEvent.bind(this));
+
+    this.model_ = importer.model_;
+    this.ppids_ = {};
+  }
+
+  BusParser.prototype = {
+    __proto__: Parser.prototype,
+
+    traceMarkWriteBusEvent: function(eventName, cpuNumber, pid, ts,
+                                  eventBase, threadName) {
+      var re = new RegExp('bus=(\\S+) rw_bytes=(\\d+) r_bytes=(\\d+) ' +
+                            'w_bytes=(\\d+) cycles=(\\d+) ns=(\\d+)');
+      var event = re.exec(eventBase.details);
+
+      var name = event[1];
+      var rw_bytes = parseInt(event[2]);
+      var r_bytes = parseInt(event[3]);
+      var w_bytes = parseInt(event[4]);
+      var cycles = parseInt(event[5]);
+      var ns = parseInt(event[6]);
+
+      // BW in MB/s
+      var r_bw = r_bytes * 1000000000 / ns;
+      r_bw /= 1024 * 1024;
+      var w_bw = w_bytes * 1000000000 / ns;
+      w_bw /= 1024 * 1024;
+
+      var ctr = this.model_.kernel
+              .getOrCreateCounter(null, 'bus ' + name + ' read');
+      if (ctr.numSeries === 0) {
+        ctr.addSeries(new tv.c.trace_model.CounterSeries('value',
+            tv.b.ui.getColorIdForGeneralPurposeString(
+                ctr.name + '.' + 'value')));
+      }
+      ctr.series.forEach(function(series) {
+        series.addCounterSample(ts, r_bw);
+      });
+
+      ctr = this.model_.kernel
+              .getOrCreateCounter(null, 'bus ' + name + ' write');
+      if (ctr.numSeries === 0) {
+        ctr.addSeries(new tv.c.trace_model.CounterSeries('value',
+            tv.b.ui.getColorIdForGeneralPurposeString(
+                ctr.name + '.' + 'value')));
+      }
+      ctr.series.forEach(function(series) {
+        series.addCounterSample(ts, r_bw);
+      });
+
+      return true;
+    }
+  };
+
+  Parser.register(BusParser);
+
+  return {
+    BusParser: BusParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/bus_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/bus_parser_test.html
new file mode 100644
index 0000000..049ad1f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/bus_parser_test.html
@@ -0,0 +1,66 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('exynos5Bus', function() {
+    var lines = [
+      's3c-fb-vsync-85    [001] d..2  8116.730115: memory_bus_usage: ' +
+          'bus=RIGHT rw_bytes=0 r_bytes=0 w_bytes=0 cycles=2681746 ns=16760792',
+
+      's3c-fb-vsync-85    [001] d..2  8116.730118: memory_bus_usage: ' +
+          'bus=CPU rw_bytes=2756608 r_bytes=2267328 w_bytes=491328 ' +
+          'cycles=6705198 ns=16763375',
+
+      's3c-fb-vsync-85    [001] d..2  8116.746788: memory_bus_usage: ' +
+          'bus=DDR_C rw_bytes=2736128 r_bytes=2260864 w_bytes=479248 ' +
+          'cycles=6670677 ns=16676375',
+
+      's3c-fb-vsync-85    [001] d..2  8116.746790: memory_bus_usage: ' +
+          'bus=DDR_R1 rw_bytes=31457280 r_bytes=31460912 w_bytes=0 ' +
+          'cycles=6670521 ns=16676500',
+
+      's3c-fb-vsync-85    [001] d..2  8116.746792: memory_bus_usage: ' +
+          'bus=DDR_L rw_bytes=16953344 r_bytes=16731088 w_bytes=223664 ' +
+          'cycles=6669885 ns=16674833',
+
+      's3c-fb-vsync-85    [001] d..2  8116.746793: memory_bus_usage: ' +
+          'bus=RIGHT rw_bytes=0 r_bytes=0 w_bytes=0 cycles=2667378 ns=16671250',
+
+      's3c-fb-vsync-85    [001] d..2  8116.746798: memory_bus_usage: ' +
+          'bus=CPU rw_bytes=2797568 r_bytes=2309424 w_bytes=491968 ' +
+          'cycles=6672156 ns=16680458',
+
+      's3c-fb-vsync-85    [001] d..2  8116.763521: memory_bus_usage: ' +
+          'bus=DDR_C rw_bytes=2408448 r_bytes=1968448 w_bytes=441456 ' +
+          'cycles=6689562 ns=16723458',
+
+      's3c-fb-vsync-85    [001] d..2  8116.763523: memory_bus_usage: ' +
+          'bus=DDR_R1 rw_bytes=31490048 r_bytes=31493360 w_bytes=0 ' +
+          'cycles=6690012 ns=16725083',
+
+      's3c-fb-vsync-85    [001] d..2  8116.763525: memory_bus_usage: ' +
+          'bus=DDR_L rw_bytes=16941056 r_bytes=16719136 w_bytes=223472 ' +
+          'cycles=6690156 ns=16725375'
+
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 10);
+
+    assert.equal(counters[0].series[0].samples.length, 2);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/clock_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/clock_parser.html
new file mode 100644
index 0000000..fb619b0
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/clock_parser.html
@@ -0,0 +1,69 @@
+<!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="/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/core/trace_model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses trace_marker events that were inserted in the trace by
+ * userland.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux trace mark events that were inserted in the trace by userland.
+   * @constructor
+   */
+  function ClockParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('clock_set_rate',
+        ClockParser.prototype.traceMarkWriteClockEvent.bind(this));
+
+    this.model_ = importer.model_;
+    this.ppids_ = {};
+  }
+
+  ClockParser.prototype = {
+    __proto__: Parser.prototype,
+
+    traceMarkWriteClockEvent: function(eventName, cpuNumber, pid, ts,
+                                       eventBase, threadName) {
+      var event = /(\S+) state=(\d+) cpu_id=(\d+)/.exec(eventBase.details);
+
+
+      var name = event[1];
+      var rate = parseInt(event[2]);
+
+      var ctr = this.model_.kernel
+              .getOrCreateCounter(null, name);
+      // Initialize the counter's series fields if needed.
+      if (ctr.numSeries === 0) {
+        ctr.addSeries(new tv.c.trace_model.CounterSeries('value',
+            tv.b.ui.getColorIdForGeneralPurposeString(
+                ctr.name + '.' + 'value')));
+      }
+      ctr.series.forEach(function(series) {
+        series.addCounterSample(ts, rate);
+      });
+
+      return true;
+    }
+  };
+
+  Parser.register(ClockParser);
+
+  return {
+    ClockParser: ClockParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/clock_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/clock_parser_test.html
new file mode 100644
index 0000000..e7c30c0
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/clock_parser_test.html
@@ -0,0 +1,58 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('clock', function() {
+    var lines = [
+      'cfinteractive-23    [000] d..2  8113.233768: clock_set_rate: ' +
+          'fout_apll state=500000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.249509: clock_set_rate: ' +
+          'fout_apll state=300000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.289796: clock_set_rate: ' +
+          'fout_apll state=400000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.294568: clock_set_rate: ' +
+          'fout_apll state=500000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.309509: clock_set_rate: ' +
+          'fout_apll state=800000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.388732: clock_set_rate: ' +
+          'fout_apll state=200000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.410182: clock_set_rate: ' +
+          'fout_apll state=300000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.414872: clock_set_rate: ' +
+          'fout_apll state=600000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.494455: clock_set_rate: ' +
+          'fout_apll state=200000000 cpu_id=0',
+
+      'cfinteractive-23    [000] d..2  8113.515254: clock_set_rate: ' +
+          'fout_apll state=500000000 cpu_id=0'
+    ];
+
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 1);
+
+    assert.equal(counters[0].series[0].samples.length, 10);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/cpufreq_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/cpufreq_parser.html
new file mode 100644
index 0000000..3fd7b98
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/cpufreq_parser.html
@@ -0,0 +1,114 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses cpufreq events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux cpufreq trace events.
+   * @constructor
+   */
+  function CpufreqParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('cpufreq_interactive_up',
+        CpufreqParser.prototype.cpufreqUpDownEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_down',
+        CpufreqParser.prototype.cpufreqUpDownEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_already',
+        CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_notyet',
+        CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_setspeed',
+        CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_target',
+        CpufreqParser.prototype.cpufreqTargetEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_boost',
+        CpufreqParser.prototype.cpufreqBoostUnboostEvent.bind(this));
+    importer.registerEventHandler('cpufreq_interactive_unboost',
+        CpufreqParser.prototype.cpufreqBoostUnboostEvent.bind(this));
+  }
+
+  function splitData(input) {
+    // TODO(sleffler) split by cpu
+    var data = {};
+    var args = input.split(/\s+/);
+    var len = args.length;
+    for (var i = 0; i < len; i++) {
+      var item = args[i].split('=');
+      data[item[0]] = parseInt(item[1]);
+    }
+    return data;
+  }
+
+  CpufreqParser.prototype = {
+    __proto__: Parser.prototype,
+
+    cpufreqSlice: function(ts, eventName, cpu, args) {
+      // TODO(sleffler) should be per-cpu
+      var kthread = this.importer.getOrCreatePseudoThread('cpufreq');
+      kthread.openSlice = eventName;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    cpufreqBoostSlice: function(ts, eventName, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('cpufreq_boost');
+      kthread.openSlice = eventName;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    /**
+     * Parses cpufreq events and sets up state in the importer.
+     */
+    cpufreqUpDownEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var data = splitData(eventBase.details);
+      this.cpufreqSlice(ts, eventName, data.cpu, data);
+      return true;
+    },
+
+    cpufreqTargetEvent: function(eventName, cpuNumber, pid, ts,
+                                 eventBase) {
+      var data = splitData(eventBase.details);
+      this.cpufreqSlice(ts, eventName, data.cpu, data);
+      return true;
+    },
+
+    cpufreqBoostUnboostEvent: function(eventName, cpuNumber, pid, ts,
+                                       eventBase) {
+      this.cpufreqBoostSlice(ts, eventName,
+          {
+            type: eventBase.details
+          });
+      return true;
+    }
+  };
+
+  Parser.register(CpufreqParser);
+
+  return {
+    CpufreqParser: CpufreqParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/cpufreq_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/cpufreq_parser_test.html
new file mode 100644
index 0000000..c878474
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/cpufreq_parser_test.html
@@ -0,0 +1,167 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('cpuFreqTargetImport', function() {
+    var lines = [
+      '<idle>-0     [000] ..s3  1043.718825: cpufreq_interactive_target: ' +
+          'cpu=0 load=2 cur=2000000 targ=300000\n',
+      '<idle>-0     [000] ..s3  1043.718825: cpufreq_interactive_target: ' +
+          'cpu=0 load=12 cur=1000000 actual=1000000 targ=200000\n'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['cpu'], 0);
+    assert.equal(thread.sliceGroup.slices[0].args['load'], 2);
+    assert.equal(thread.sliceGroup.slices[0].args['cur'], 2000000);
+    assert.equal(thread.sliceGroup.slices[0].args['targ'], 300000);
+
+    assert.equal(thread.sliceGroup.slices[1].args['cpu'], 0);
+    assert.equal(thread.sliceGroup.slices[1].args['load'], 12);
+    assert.equal(thread.sliceGroup.slices[1].args['cur'], 1000000);
+    assert.equal(thread.sliceGroup.slices[1].args['actual'], 1000000);
+    assert.equal(thread.sliceGroup.slices[1].args['targ'], 200000);
+  });
+
+  test('cpuFreqNotYetImport', function() {
+    var lines = [
+      '<idle>-0     [001] ..s3  1043.718832: cpufreq_interactive_notyet: ' +
+          'cpu=1 load=10 cur=700000 targ=200000\n',
+      '<idle>-0     [001] ..s3  1043.718832: cpufreq_interactive_notyet: ' +
+          'cpu=1 load=10 cur=700000 actual=1000000 targ=200000\n'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['cpu'], 1);
+    assert.equal(thread.sliceGroup.slices[0].args['load'], 10);
+    assert.equal(thread.sliceGroup.slices[0].args['cur'], 700000);
+    assert.equal(thread.sliceGroup.slices[0].args['targ'], 200000);
+
+    assert.equal(thread.sliceGroup.slices[1].args['cpu'], 1);
+    assert.equal(thread.sliceGroup.slices[1].args['load'], 10);
+    assert.equal(thread.sliceGroup.slices[1].args['cur'], 700000);
+    assert.equal(thread.sliceGroup.slices[1].args['actual'], 1000000);
+    assert.equal(thread.sliceGroup.slices[1].args['targ'], 200000);
+  });
+
+  test('cpuFreqSetSpeedImport', function() {
+    var lines = [
+      'cfinteractive-23    [001] ...1  1043.719688: ' +
+          'cpufreq_interactive_setspeed: cpu=0 targ=200000 actual=700000\n'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['cpu'], 0);
+    assert.equal(thread.sliceGroup.slices[0].args['targ'], 200000);
+    assert.equal(thread.sliceGroup.slices[0].args['actual'], 700000);
+  });
+
+  test('cpuFreqAlreadyImport', function() {
+    var lines = [
+      '<idle>-0     [000] ..s3  1043.738822: cpufreq_interactive_already: cpu=0 load=18 cur=200000 actual=700000 targ=200000\n' // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['cpu'], 0);
+    assert.equal(thread.sliceGroup.slices[0].args['load'], 18);
+    assert.equal(thread.sliceGroup.slices[0].args['cur'], 200000);
+    assert.equal(thread.sliceGroup.slices[0].args['actual'], 700000);
+    assert.equal(thread.sliceGroup.slices[0].args['targ'], 200000);
+  });
+
+  test('cpuFreqBoostImport', function() {
+    var lines = [
+      'InputDispatcher-465   [001] ...1  1044.213948: ' +
+          'cpufreq_interactive_boost: pulse\n'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['type'], 'pulse');
+  });
+
+  test('cpuFreqUnBoostImport', function() {
+    var lines = [
+      'InputDispatcher-465   [001] ...1  1044.213948: ' +
+          'cpufreq_interactive_unboost: pulse\n'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['type'], 'pulse');
+  });
+
+  test('cpuFreqUpImport', function() {
+    var lines = [
+      'kinteractive-69    [003] .... 414324.164432: ' +
+          'cpufreq_interactive_up: cpu=1 targ=1400000 actual=800000'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['cpu'], 1);
+    assert.equal(thread.sliceGroup.slices[0].args['targ'], 1400000);
+    assert.equal(thread.sliceGroup.slices[0].args['actual'], 800000);
+  });
+
+  test('cpuFreqDownImport', function() {
+    var lines = [
+      'kinteractive-69    [003] .... 414365.834193: ' +
+          'cpufreq_interactive_down: cpu=3 targ=800000 actual=1000000'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var thread = threads[0];
+    assert.equal(thread.sliceGroup.slices[0].args['cpu'], 3);
+    assert.equal(thread.sliceGroup.slices[0].args['targ'], 800000);
+    assert.equal(thread.sliceGroup.slices[0].args['actual'], 1000000);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/disk_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/disk_parser.html
new file mode 100644
index 0000000..8f65c91
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/disk_parser.html
@@ -0,0 +1,312 @@
+<!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/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses filesystem and block device events in the Linux event
+ * trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux filesystem and block device trace events.
+   * @constructor
+   */
+  function DiskParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('f2fs_write_begin',
+        DiskParser.prototype.f2fsWriteBeginEvent.bind(this));
+    importer.registerEventHandler('f2fs_write_end',
+        DiskParser.prototype.f2fsWriteEndEvent.bind(this));
+    importer.registerEventHandler('f2fs_sync_file_enter',
+        DiskParser.prototype.f2fsSyncFileEnterEvent.bind(this));
+    importer.registerEventHandler('f2fs_sync_file_exit',
+        DiskParser.prototype.f2fsSyncFileExitEvent.bind(this));
+    importer.registerEventHandler('ext4_sync_file_enter',
+        DiskParser.prototype.ext4SyncFileEnterEvent.bind(this));
+    importer.registerEventHandler('ext4_sync_file_exit',
+        DiskParser.prototype.ext4SyncFileExitEvent.bind(this));
+    importer.registerEventHandler('ext4_da_write_begin',
+        DiskParser.prototype.ext4WriteBeginEvent.bind(this));
+    importer.registerEventHandler('ext4_da_write_end',
+        DiskParser.prototype.ext4WriteEndEvent.bind(this));
+    importer.registerEventHandler('block_rq_issue',
+        DiskParser.prototype.blockRqIssueEvent.bind(this));
+    importer.registerEventHandler('block_rq_complete',
+        DiskParser.prototype.blockRqCompleteEvent.bind(this));
+  }
+
+  DiskParser.prototype = {
+    __proto__: Parser.prototype,
+
+    openAsyncSlice: function(ts, category, threadName, pid, key, name) {
+      var kthread = this.importer.getOrCreateKernelThread(
+          category + ':' + threadName, pid);
+      var asyncSliceConstructor =
+         tv.c.trace_model.AsyncSlice.getConstructor(
+            category, name);
+      var slice = new asyncSliceConstructor(
+          category, name,
+          tv.b.ui.getColorIdForGeneralPurposeString(name),
+          ts);
+      slice.startThread = kthread.thread;
+
+      if (!kthread.openAsyncSlices) {
+        kthread.openAsyncSlices = { };
+      }
+      kthread.openAsyncSlices[key] = slice;
+    },
+
+    closeAsyncSlice: function(ts, category, threadName, pid, key, args) {
+      var kthread = this.importer.getOrCreateKernelThread(
+          category + ':' + threadName, pid);
+      if (kthread.openAsyncSlices) {
+        var slice = kthread.openAsyncSlices[key];
+        if (slice) {
+          slice.duration = ts - slice.start;
+          slice.args = args;
+          slice.endThread = kthread.thread;
+          slice.subSlices = [
+            new tv.c.trace_model.Slice(category, slice.title,
+                slice.colorId, slice.start, slice.args, slice.duration)
+          ];
+          kthread.thread.asyncSliceGroup.push(slice);
+          delete kthread.openAsyncSlices[key];
+        }
+      }
+    },
+
+    /**
+     * Parses events and sets up state in the importer.
+     */
+    f2fsWriteBeginEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev = \((\d+,\d+)\), ino = (\d+), pos = (\d+), len = (\d+), flags = (\d+)/. // @suppress longLineCheck
+          exec(eventBase.details);
+      if (!event)
+        return false;
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var pos = parseInt(event[3]);
+      var len = parseInt(event[4]);
+      var key = device + '-' + inode + '-' + pos + '-' + len;
+      this.openAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+          key, 'f2fs_write');
+      return true;
+    },
+
+    f2fsWriteEndEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev = \((\d+,\d+)\), ino = (\d+), pos = (\d+), len = (\d+), copied = (\d+)/. // @suppress longLineCheck
+          exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var pos = parseInt(event[3]);
+      var len = parseInt(event[4]);
+      var error = parseInt(event[5]) !== len;
+      var key = device + '-' + inode + '-' + pos + '-' + len;
+      this.closeAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+          key, {
+            device: device,
+            inode: inode,
+            error: error
+          });
+      return true;
+    },
+
+    ext4WriteBeginEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev (\d+,\d+) ino (\d+) pos (\d+) len (\d+) flags (\d+)/.
+          exec(eventBase.details);
+      if (!event)
+        return false;
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var pos = parseInt(event[3]);
+      var len = parseInt(event[4]);
+      var key = device + '-' + inode + '-' + pos + '-' + len;
+      this.openAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+          key, 'ext4_write');
+      return true;
+    },
+
+    ext4WriteEndEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev (\d+,\d+) ino (\d+) pos (\d+) len (\d+) copied (\d+)/.
+          exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var pos = parseInt(event[3]);
+      var len = parseInt(event[4]);
+      var error = parseInt(event[5]) !== len;
+      var key = device + '-' + inode + '-' + pos + '-' + len;
+      this.closeAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+          key, {
+            device: device,
+            inode: inode,
+            error: error
+          });
+      return true;
+    },
+
+    f2fsSyncFileEnterEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = new RegExp(
+          'dev = \\((\\d+,\\d+)\\), ino = (\\d+), pino = (\\d+), i_mode = (\\S+), ' + // @suppress longLineCheck
+          'i_size = (\\d+), i_nlink = (\\d+), i_blocks = (\\d+), i_advise = (\\d+)'). // @suppress longLineCheck
+          exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var key = device + '-' + inode;
+      this.openAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+          key, 'fsync');
+      return true;
+    },
+
+    f2fsSyncFileExitEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = new RegExp('dev = \\((\\d+,\\d+)\\), ino = (\\d+), checkpoint is (\\S+), ' + // @suppress longLineCheck
+          'datasync = (\\d+), ret = (\\d+)').
+          exec(eventBase.details.replace('not needed', 'not_needed'));
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var error = parseInt(event[5]);
+      var key = device + '-' + inode;
+      this.closeAsyncSlice(ts, 'f2fs', eventBase.threadName, eventBase.pid,
+          key, {
+            device: device,
+            inode: inode,
+            error: error
+          });
+      return true;
+    },
+
+    ext4SyncFileEnterEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev (\d+,\d+) ino (\d+) parent (\d+) datasync (\d+)/.
+          exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var datasync = event[4] == 1;
+      var key = device + '-' + inode;
+      var action = datasync ? 'fdatasync' : 'fsync';
+      this.openAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+          key, action);
+      return true;
+    },
+
+    ext4SyncFileExitEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev (\d+,\d+) ino (\d+) ret (\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var inode = parseInt(event[2]);
+      var error = parseInt(event[3]);
+      var key = device + '-' + inode;
+      this.closeAsyncSlice(ts, 'ext4', eventBase.threadName, eventBase.pid,
+          key, {
+            device: device,
+            inode: inode,
+            error: error
+          });
+      return true;
+    },
+
+    blockRqIssueEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = new RegExp('(\\d+,\\d+) (F)?([DWRN])(F)?(A)?(S)?(M)? ' +
+          '\\d+ \\(.*\\) (\\d+) \\+ (\\d+) \\[.*\\]').exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var action;
+      switch (event[3]) {
+        case 'D':
+          action = 'discard';
+          break;
+        case 'W':
+          action = 'write';
+          break;
+        case 'R':
+          action = 'read';
+          break;
+        case 'N':
+          action = 'none';
+          break;
+        default:
+          action = 'unknown';
+          break;
+      }
+
+      if (event[2]) {
+        action += ' flush';
+      }
+      if (event[4] == 'F') {
+        action += ' fua';
+      }
+      if (event[5] == 'A') {
+        action += ' ahead';
+      }
+      if (event[6] == 'S') {
+        action += ' sync';
+      }
+      if (event[7] == 'M') {
+        action += ' meta';
+      }
+      var device = event[1];
+      var sector = parseInt(event[8]);
+      var numSectors = parseInt(event[9]);
+      var key = device + '-' + sector + '-' + numSectors;
+      this.openAsyncSlice(ts, 'block', eventBase.threadName, eventBase.pid,
+          key, action);
+      return true;
+    },
+
+    blockRqCompleteEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = new RegExp('(\\d+,\\d+) (F)?([DWRN])(F)?(A)?(S)?(M)? ' +
+          '\\(.*\\) (\\d+) \\+ (\\d+) \\[(.*)\\]').exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var device = event[1];
+      var sector = parseInt(event[8]);
+      var numSectors = parseInt(event[9]);
+      var error = parseInt(event[10]);
+      var key = device + '-' + sector + '-' + numSectors;
+      this.closeAsyncSlice(ts, 'block', eventBase.threadName, eventBase.pid,
+          key, {
+            device: device,
+            sector: sector,
+            numSectors: numSectors,
+            error: error
+          });
+      return true;
+    }
+  };
+
+  Parser.register(DiskParser);
+
+  return {
+    DiskParser: DiskParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/disk_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/disk_parser_test.html
new file mode 100644
index 0000000..68f4cb5
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/disk_parser_test.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/test_utils.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('diskImport', function() {
+    var lines = [
+      // NB: spliced from different traces; mismatched timestamps don't matter
+      'AsyncTask #2-18830 [000] ...1 154578.668286: ext4_sync_file_enter: ' +
+          'dev 259,1 ino 81993 parent 81906 datasync 1',
+      'Binder_A-3179 [001] ...1  1354.510088: f2fs_sync_file_enter: ' +
+          'dev = (259,14), ino = 4882, pino = 313, i_mode = 0x81b0, i_size = ' +
+          '25136, i_nlink = 1, i_blocks = 8, i_advise = 0x0',
+      'Binder_A-3179 [001] ...1  1354.514013: f2fs_sync_file_exit: ' +
+          'dev = (259,14), ino = 4882, checkpoint is not needed, datasync = 1, ret = 0', // @suppress longLineCheck
+      'mmcqd/0-81    [000] d..2 154578.668390: block_rq_issue: ' +
+          '179,0 WS 0 () 3427120 + 16 [mmcqd/0]',
+      'mmcqd/0-81    [000] d..2 154578.669181: block_rq_complete: ' +
+          '179,0 WS () 3427120 + 16 [0]',
+      'mmcqd/0-81    [001] d..2 154578.670853: block_rq_issue: ' +
+          '179,0 FWS 0 () 18446744073709551615 + 0 [mmcqd/0]',
+      'mmcqd/0-81    [001] d..2 154578.670869: block_rq_complete: ' +
+          '179,0 FWS () 18446744073709551615 + 0 [0]',
+      'AsyncTask #2-18830 [001] ...1 154578.670901: ext4_sync_file_exit: ' +
+          'dev 259,1 ino 81993 ret 0',
+      'mmcqd/0-81    [001] d..2 154578.877038: block_rq_issue: ' +
+          '179,0 R 0 () 3255256 + 8 [mmcqd/0]',
+      'mmcqd/0-81    [001] d..2 154578.877110: block_rq_issue: ' +
+          '179,0 R 0 () 3255288 + 8 [mmcqd/0]',
+      'mmcqd/0-81    [000] d..2 154578.877345: block_rq_complete: ' +
+          '179,0 R () 3255256 + 8 [0]',
+      'mmcqd/0-81    [000] d..2 154578.877466: block_rq_complete: ' +
+          '179,0 R () 3255288 + 8 [0]',
+      'ContactsProvide-1184 [000] ...1 66.613719: f2fs_write_begin: ' +
+          'dev = (253,2), ino = 3342, pos = 0, len = 75, flags = 0',
+      'ContactsProvide-1184 [000] ...1 66.613733: f2fs_write_end: ' +
+          'dev = (253,2), ino = 3342, pos = 0, len = 75, copied = 75'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var blockThread = undefined;
+    var ext4Thread = undefined;
+    var f2fsSyncThread = undefined;
+    var f2fsWriteThread = undefined;
+
+    m.getAllThreads().forEach(function(t) {
+      switch (t.name) {
+        case 'block:mmcqd/0':
+          blockThread = t;
+          break;
+        case 'ext4:AsyncTask #2':
+          ext4Thread = t;
+          break;
+        case 'f2fs:Binder_A':
+          f2fsSyncThread = t;
+          break;
+        case 'f2fs:ContactsProvide':
+          f2fsWriteThread = t;
+          break;
+        default:
+          throw new unittest.TestError('Unexpected thread named ' + t.name);
+      }
+    });
+    assert.isDefined(blockThread);
+    assert.isDefined(ext4Thread);
+    assert.isDefined(f2fsSyncThread);
+    assert.isDefined(f2fsWriteThread);
+
+    assert.equal(blockThread.asyncSliceGroup.length, 4);
+
+    var slice = blockThread.asyncSliceGroup.slices[0];
+    assert.equal(slice.category, 'block');
+    assert.equal(slice.title, 'write sync');
+    assert.equal(slice.args.device, '179,0');
+    assert.equal(slice.args.error, 0);
+    assert.equal(slice.args.numSectors, 16);
+    assert.equal(slice.args.sector, 3427120);
+
+    assert.equal(ext4Thread.asyncSliceGroup.length, 1);
+
+    slice = ext4Thread.asyncSliceGroup.slices[0];
+    assert.equal(slice.category, 'ext4');
+    assert.equal(slice.title, 'fdatasync');
+    assert.equal(slice.args.device, '259,1');
+    assert.equal(slice.args.error, 0);
+    assert.equal(slice.args.inode, 81993);
+
+    assert.equal(f2fsSyncThread.asyncSliceGroup.length, 1);
+
+    slice = f2fsSyncThread.asyncSliceGroup.slices[0];
+    assert.equal(slice.category, 'f2fs');
+    assert.equal(slice.title, 'fsync');
+    assert.equal(slice.args.device, '259,14');
+    assert.equal(slice.args.error, 0);
+    assert.equal(slice.args.inode, 4882);
+
+    assert.equal(f2fsWriteThread.asyncSliceGroup.length, 1);
+
+    slice = f2fsWriteThread.asyncSliceGroup.slices[0];
+    assert.equal(slice.category, 'f2fs');
+    assert.equal(slice.title, 'f2fs_write');
+    assert.equal(slice.args.device, '253,2');
+    assert.equal(slice.args.inode, 3342);
+    assert.equal(slice.args.error, false);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/drm_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/drm_parser.html
new file mode 100644
index 0000000..a42a6bb
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/drm_parser.html
@@ -0,0 +1,70 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses drm driver events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux drm trace events.
+   * @constructor
+   */
+  function DrmParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('drm_vblank_event',
+        DrmParser.prototype.vblankEvent.bind(this));
+  }
+
+  DrmParser.prototype = {
+    __proto__: Parser.prototype,
+
+    drmVblankSlice: function(ts, eventName, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('drm_vblank');
+      kthread.openSlice = eventName;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    /**
+     * Parses drm driver events and sets up state in the importer.
+     */
+    vblankEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /crtc=(\d+), seq=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var crtc = parseInt(event[1]);
+      var seq = parseInt(event[2]);
+      this.drmVblankSlice(ts, 'vblank:' + crtc,
+          {
+            crtc: crtc,
+            seq: seq
+          });
+      return true;
+    }
+  };
+
+  Parser.register(DrmParser);
+
+  return {
+    DrmParser: DrmParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/drm_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/drm_parser_test.html
new file mode 100644
index 0000000..d9f4f5f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/drm_parser_test.html
@@ -0,0 +1,32 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('drmImport', function() {
+    var lines = [
+      ' chrome-2465  [000]    71.653157: drm_vblank_event: crtc=0, seq=4233',
+      ' <idle>-0     [000]    71.669851: drm_vblank_event: crtc=0, seq=4234'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var vblankThread = threads[0];
+    assert.equal(vblankThread.name, 'drm_vblank');
+    assert.equal(vblankThread.sliceGroup.length, 2);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/exynos_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/exynos_parser.html
new file mode 100644
index 0000000..31e1aab
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/exynos_parser.html
@@ -0,0 +1,127 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses exynos events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux exynos trace events.
+   * @constructor
+   */
+  function ExynosParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('exynos_busfreq_target_int',
+        ExynosParser.prototype.busfreqTargetIntEvent.bind(this));
+    importer.registerEventHandler('exynos_busfreq_target_mif',
+        ExynosParser.prototype.busfreqTargetMifEvent.bind(this));
+
+    importer.registerEventHandler('exynos_page_flip_state',
+        ExynosParser.prototype.pageFlipStateEvent.bind(this));
+  }
+
+  ExynosParser.prototype = {
+    __proto__: Parser.prototype,
+
+    exynosBusfreqSample: function(name, ts, frequency) {
+      var targetCpu = this.importer.getOrCreateCpu(0);
+      var counter = targetCpu.getOrCreateCounter('', name);
+      if (counter.numSeries === 0) {
+        counter.addSeries(new tv.c.trace_model.CounterSeries('frequency',
+            tv.b.ui.getColorIdForGeneralPurposeString(
+                counter.name + '.' + 'frequency')));
+      }
+      counter.series.forEach(function(series) {
+        series.addCounterSample(ts, frequency);
+      });
+    },
+
+    /**
+     * Parses exynos_busfreq_target_int events and sets up state.
+     */
+    busfreqTargetIntEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /frequency=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      this.exynosBusfreqSample('INT Frequency', ts, parseInt(event[1]));
+      return true;
+    },
+
+    /**
+     * Parses exynos_busfreq_target_mif events and sets up state.
+     */
+    busfreqTargetMifEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /frequency=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      this.exynosBusfreqSample('MIF Frequency', ts, parseInt(event[1]));
+      return true;
+    },
+
+    exynosPageFlipStateOpenSlice: function(ts, pipe, fb, state) {
+      var kthread = this.importer.getOrCreatePseudoThread(
+          'exynos_flip_state (pipe:' + pipe + ', fb:' + fb + ')');
+      kthread.openSliceTS = ts;
+      kthread.openSlice = state;
+    },
+
+    exynosPageFlipStateCloseSlice: function(ts, pipe, fb, args) {
+      var kthread = this.importer.getOrCreatePseudoThread(
+          'exynos_flip_state (pipe:' + pipe + ', fb:' + fb + ')');
+      if (kthread.openSlice) {
+        var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+            tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+            kthread.openSliceTS,
+            args,
+            ts - kthread.openSliceTS);
+        kthread.thread.sliceGroup.pushSlice(slice);
+      }
+      kthread.openSlice = undefined;
+    },
+
+    /**
+     * Parses page_flip_state events and sets up state in the importer.
+     */
+    pageFlipStateEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /pipe=(\d+), fb=(\d+), state=(.*)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var pipe = parseInt(event[1]);
+      var fb = parseInt(event[2]);
+      var state = event[3];
+
+      this.exynosPageFlipStateCloseSlice(ts, pipe, fb,
+          {
+            pipe: pipe,
+            fb: fb
+          });
+      if (state !== 'flipped')
+        this.exynosPageFlipStateOpenSlice(ts, pipe, fb, state);
+      return true;
+    }
+  };
+
+  Parser.register(ExynosParser);
+
+  return {
+    ExynosParser: ExynosParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/exynos_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/exynos_parser_test.html
new file mode 100644
index 0000000..9e6b917
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/exynos_parser_test.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="/core/test_utils.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('exynosBusfreqImport', function() {
+    var lines = [
+      '     kworker/1:0-4177  [001] ....  2803.129806: ' +
+          'exynos_busfreq_target_int: frequency=200000',
+      '     kworker/1:0-4177  [001] ....  2803.229207: ' +
+          'exynos_busfreq_target_int: frequency=267000',
+      '     kworker/1:0-4177  [001] ....  2803.329031: ' +
+          'exynos_busfreq_target_int: frequency=160000',
+      '     kworker/1:0-4177  [001] ....  2805.729039: ' +
+          'exynos_busfreq_target_mif: frequency=200000'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c0 = m.kernel.cpus[0];
+    assert.equal(c0.slices.length, 0);
+    assert.equal(c0.counters['INT Frequency'].series[0].samples.length, 3);
+    assert.equal(c0.counters['MIF Frequency'].series[0].samples.length, 1);
+  });
+
+  test('exynosPageFlipSlowRequestImport', function() {
+    var lines = [
+      '          <idle>-0     [000] d.h. 1000.000000: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_kds',
+      ' Chrome_IOThread-21603 [000] d.h. 1000.000001: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_apply',
+      '     kworker/0:1-25931 [000] .... 1000.000002: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_flip',
+      '     kworker/0:1-25931 [000] .... 1000.000003: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=flipped',
+      '          <idle>-0     [000] d.h. 1000.000004: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_kds',
+      ' Chrome_IOThread-21603 [000] d.h. 1000.000005: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_apply',
+      '     kworker/0:1-25931 [000] .... 1000.000006: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_flip',
+      '     kworker/0:1-25931 [000] .... 1000.000007: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=flipped'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    // there are 2 threads:
+    //   (1) "exynos_flip_state (pipe:0, fb:25)"
+    //   (2) "exynos_flip_state (pipe:0, fb:26)"
+    assert.equal(threads.length, 2);
+
+    // in the test data, event of fb=26 occurs first, so it's thread[0]
+    var gfxFbId26Thread = threads[0]; // thread where fb == 26
+    var gfxFbId25Thread = threads[1]; // thread where fb == 25
+    assert.equal(gfxFbId25Thread.name, 'exynos_flip_state (pipe:0, fb:25)');
+    assert.equal(gfxFbId26Thread.name, 'exynos_flip_state (pipe:0, fb:26)');
+    // Every state (except for 'flipped') will start a new slice.
+    // The last event will not be closed, so it's not a slice
+    assert.equal(gfxFbId25Thread.sliceGroup.length, 3);
+    assert.equal(gfxFbId26Thread.sliceGroup.length, 3);
+  });
+
+  test('exynosPageFlipFastRequestImport', function() {
+    var lines = [
+      '          <idle>-0     [000] d.h. 1000.000000: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_kds',
+      ' Chrome_IOThread-21603 [000] d.h. 1000.000001: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_kds',
+      '               X-21385 [000] .... 1000.000002: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_apply',
+      '     kworker/0:1-25931 [000] .... 1000.000003: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_flip',
+      '               X-21385 [001] .... 1000.000004: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_apply',
+      '     kworker/0:1-25931 [000] .... 1000.000005: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=flipped',
+      '          <idle>-0     [000] d.h. 1000.000006: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_kds',
+      '               X-21385 [000] .... 1000.000007: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_flip',
+      '     kworker/0:1-25931 [000] .... 1000.000008: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=flipped',
+      '     kworker/0:1-25931 [000] .... 1000.000009: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_kds',
+      ' Chrome_IOThread-21603 [000] d.h. 1000.000010: ' +
+          'exynos_page_flip_state: pipe=0, fb=25, state=wait_apply',
+      '          <idle>-0     [000] d.h. 1000.000011: ' +
+          'exynos_page_flip_state: pipe=0, fb=26, state=wait_apply'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    // there are 2 threads:
+    //   (1) "exynos_flip_state (pipe:0, fb:25)"
+    //   (2) "exynos_flip_state (pipe:0, fb:26)"
+    assert.equal(threads.length, 2);
+
+    // in the test data, event of fb=26 occurs first, so it's thread[0]
+    var gfxFbId26Thread = threads[0]; // thread where fb == 26
+    var gfxFbId25Thread = threads[1]; // thread where fb == 25
+    assert.equal(gfxFbId25Thread.name, 'exynos_flip_state (pipe:0, fb:25)');
+    assert.equal(gfxFbId26Thread.name, 'exynos_flip_state (pipe:0, fb:26)');
+    // Every state (except for 'flipped') will start a new slice.
+    // The last event will not be closed, so it's not a slice
+    assert.equal(gfxFbId25Thread.sliceGroup.length, 4);
+    assert.equal(gfxFbId26Thread.sliceGroup.length, 4);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/gesture_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/gesture_parser.html
new file mode 100644
index 0000000..418964a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/gesture_parser.html
@@ -0,0 +1,144 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses gesture events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses trace events generated by gesture library for touchpad.
+   * @constructor
+   */
+  function GestureParser(importer) {
+    Parser.call(this, importer);
+    importer.registerEventHandler('tracing_mark_write:log',
+        GestureParser.prototype.logEvent.bind(this));
+    importer.registerEventHandler('tracing_mark_write:SyncInterpret',
+        GestureParser.prototype.syncEvent.bind(this));
+    importer.registerEventHandler('tracing_mark_write:HandleTimer',
+        GestureParser.prototype.timerEvent.bind(this));
+  }
+
+  GestureParser.prototype = {
+    __proto__: Parser.prototype,
+
+    /**
+     * Parse events generate by gesture library.
+     * gestureOpenSlice and gestureCloseSlice are two common
+     * functions to store the begin time and end time for all
+     * events in gesture library
+     */
+    gestureOpenSlice: function(title, ts, opt_args) {
+      var thread = this.importer.getOrCreatePseudoThread('gesture').thread;
+      thread.sliceGroup.beginSlice(
+          'touchpad_gesture', title, ts, opt_args);
+    },
+
+    gestureCloseSlice: function(title, ts) {
+      var thread = this.importer.getOrCreatePseudoThread('gesture').thread;
+      if (thread.sliceGroup.openSliceCount) {
+        var slice = thread.sliceGroup.mostRecentlyOpenedPartialSlice;
+        if (slice.title != title) {
+          this.importer.model.importWarning({
+            type: 'title_match_error',
+            message: 'Titles do not match. Title is ' +
+                slice.title + ' in openSlice, and is ' +
+                title + ' in endSlice'
+          });
+        } else {
+          thread.sliceGroup.endSlice(ts);
+        }
+      }
+    },
+
+    /**
+     * For log events, events will come in pairs with a tag log:
+     * like this:
+     * tracing_mark_write: log: start: TimerLogOutputs
+     * tracing_mark_write: log: end: TimerLogOutputs
+     * which represent the start and the end time of certain log behavior
+     * Take these logs above for example, they are the start and end time
+     * of logging Output for HandleTimer function
+     */
+    logEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var innerEvent =
+          /^\s*(\w+):\s*(\w+)$/.exec(eventBase.details);
+      switch (innerEvent[1]) {
+        case 'start':
+          this.gestureOpenSlice('GestureLog', ts, {name: innerEvent[2]});
+          break;
+        case 'end':
+          this.gestureCloseSlice('GestureLog', ts);
+      }
+      return true;
+    },
+
+    /**
+     * For SyncInterpret events, events will come in pairs with
+     * a tag SyncInterpret:
+     * like this:
+     * tracing_mark_write: SyncInterpret: start: ClickWiggleFilterInterpreter
+     * tracing_mark_write: SyncInterpret: end: ClickWiggleFilterInterpreter
+     * which represent the start and the end time of SyncInterpret function
+     * inside the certain interpreter in the gesture library.
+     * Take the logs above for example, they are the start and end time
+     * of the SyncInterpret function inside ClickWiggleFilterInterpreter
+     */
+    syncEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var innerEvent = /^\s*(\w+):\s*(\w+)$/.exec(eventBase.details);
+      switch (innerEvent[1]) {
+        case 'start':
+          this.gestureOpenSlice('SyncInterpret', ts,
+                                {interpreter: innerEvent[2]});
+          break;
+        case 'end':
+          this.gestureCloseSlice('SyncInterpret', ts);
+      }
+      return true;
+    },
+
+    /**
+     * For HandleTimer events, events will come in pairs with
+     * a tag HandleTimer:
+     * like this:
+     * tracing_mark_write: HandleTimer: start: LookaheadFilterInterpreter
+     * tracing_mark_write: HandleTimer: end: LookaheadFilterInterpreter
+     * which represent the start and the end time of HandleTimer function
+     * inside the certain interpreter in the gesture library.
+     * Take the logs above for example, they are the start and end time
+     * of the HandleTimer function inside LookaheadFilterInterpreter
+     */
+    timerEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var innerEvent = /^\s*(\w+):\s*(\w+)$/.exec(eventBase.details);
+      switch (innerEvent[1]) {
+        case 'start':
+          this.gestureOpenSlice('HandleTimer', ts,
+                                {interpreter: innerEvent[2]});
+          break;
+        case 'end':
+          this.gestureCloseSlice('HandleTimer', ts);
+      }
+      return true;
+    }
+  };
+
+  Parser.register(GestureParser);
+
+  return {
+    GestureParser: GestureParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/gesture_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/gesture_parser_test.html
new file mode 100644
index 0000000..1c9b302
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/gesture_parser_test.html
@@ -0,0 +1,206 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('gestureImport', function() {
+    var lines = [
+      '<...>-1837  [000] ...1 875292.741648: tracing_mark_write: ' +
+          'log: start: TimerLogOutputs',  // 0
+      '<...>-1837  [000] ...1 875292.741651: tracing_mark_write: ' +
+          'log: end: TimerLogOutputs',
+      '<...>-1837  [000] ...1 875292.742796: tracing_mark_write: ' +
+          'log: start: LogTimerCallback',
+      '<...>-1837  [000] ...1 875292.742802: tracing_mark_write: ' +
+          'log: end: LogTimerCallback',
+      '<...>-1837  [000] ...1 875292.742805: tracing_mark_write: ' +
+          'HandleTimer: start: LoggingFilterInterpreter',  // 2
+      '<...>-1837  [000] ...1 875292.742809: tracing_mark_write: ' +
+          'HandleTimer: start: AppleTrackpadFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742814: tracing_mark_write: ' +
+          'HandleTimer: start: Cr48ProfileSensorFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742818: tracing_mark_write: ' +
+          'HandleTimer: start: T5R2CorrectingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742822: tracing_mark_write: ' +
+          'HandleTimer: start: StuckButtonInhibitorFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742825: tracing_mark_write: ' +
+          'HandleTimer: start: IntegralGestureFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742829: tracing_mark_write: ' +
+          'HandleTimer: start: ScalingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742833: tracing_mark_write: ' +
+          'HandleTimer: start: SplitCorrectingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742836: tracing_mark_write: ' +
+          'HandleTimer: start: AccelFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742840: tracing_mark_write: ' +
+          'HandleTimer: start: SensorJumpFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742843: tracing_mark_write: ' +
+          'HandleTimer: start: BoxFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742846: tracing_mark_write: ' +
+          'HandleTimer: start: LookaheadFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742853: tracing_mark_write: ' +
+          'SyncInterpret: start: IirFilterInterpreter',  // 14
+      '<...>-1837  [000] ...1 875292.742861: tracing_mark_write: ' +
+          'SyncInterpret: start: PalmClassifyingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742872: tracing_mark_write: ' +
+          'SyncInterpret: start: ClickWiggleFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742881: tracing_mark_write: ' +
+          'SyncInterpret: start: FlingStopFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742887: tracing_mark_write: ' +
+          'SyncInterpret: start: ImmediateInterpreter',
+      '<...>-1837  [000] ...1 875292.742906: tracing_mark_write: ' +
+          'SyncInterpret: end: ImmediateInterpreter',
+      '<...>-1837  [000] ...1 875292.742910: tracing_mark_write: ' +
+          'SyncInterpret: end: FlingStopFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742914: tracing_mark_write: ' +
+          'SyncInterpret: end: ClickWiggleFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742917: tracing_mark_write: ' +
+          'SyncInterpret: end: PalmClassifyingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742921: tracing_mark_write: ' +
+          'SyncInterpret: end: IirFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742926: tracing_mark_write: ' +
+          'HandleTimer: end: LookaheadFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742929: tracing_mark_write: ' +
+          'HandleTimer: end: BoxFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742932: tracing_mark_write: ' +
+          'HandleTimer: end: SensorJumpFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742937: tracing_mark_write: ' +
+          'HandleTimer: end: AccelFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742940: tracing_mark_write: ' +
+          'HandleTimer: end: SplitCorrectingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742944: tracing_mark_write: ' +
+          'HandleTimer: end: ScalingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742949: tracing_mark_write: ' +
+          'HandleTimer: end: IntegralGestureFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742952: tracing_mark_write: ' +
+          'HandleTimer: end: StuckButtonInhibitorFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742956: tracing_mark_write: ' +
+          'HandleTimer: end: T5R2CorrectingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742959: tracing_mark_write: ' +
+          'HandleTimer: end: Cr48ProfileSensorFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742962: tracing_mark_write: ' +
+          'HandleTimer: end: AppleTrackpadFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742966: tracing_mark_write: ' +
+          'HandleTimer: end: LoggingFilterInterpreter',
+      '<...>-1837  [000] ...1 875292.742969: tracing_mark_write: ' +
+          'log: start: TimerLogOutputs',
+      '<...>-1837  [000] ...1 875292.742973: tracing_mark_write: ' +
+          'log: end: TimerLogOutputs',
+      '<...>-1837  [000] ...1 875292.795219: tracing_mark_write: ' +
+          'log: start: LogHardwareState',
+      '<...>-1837  [000] ...1 875292.795231: tracing_mark_write: ' +
+          'log: end: LogHardwareState'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+
+    var gestureThread = threads[0];
+    assert.equal(gestureThread.name, 'gesture');
+    assert.equal(gestureThread.sliceGroup.length, 21);
+    assert.equal('touchpad_gesture',
+                 gestureThread.sliceGroup.slices[0].category);
+    assert.equal('GestureLog',
+                 gestureThread.sliceGroup.slices[0].title);
+    assert.equal('touchpad_gesture',
+                 gestureThread.sliceGroup.slices[2].category);
+    assert.equal('HandleTimer',
+                 gestureThread.sliceGroup.slices[2].title);
+    assert.equal('touchpad_gesture',
+                 gestureThread.sliceGroup.slices[14].category);
+    assert.equal('SyncInterpret',
+                 gestureThread.sliceGroup.slices[14].title);
+  });
+
+  test('unusualStart', function() {
+    var lines = [
+      'X-30368 [000] ...1 1819362.481867: tracing_mark_write: ' +
+          'SyncInterpret: start: IirFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481881: tracing_mark_write: ' +
+          'SyncInterpret: start: PalmClassifyingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481894: tracing_mark_write: ' +
+          'SyncInterpret: start: ClickWiggleFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481905: tracing_mark_write: ' +
+          'SyncInterpret: start: FlingStopFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481912: tracing_mark_write: ' +
+          'SyncInterpret: start: ImmediateInterpreter',
+      'X-30368 [000] ...1 1819362.481933: tracing_mark_write: ' +
+          'SyncInterpret: end: ImmediateInterpreter',
+      'X-30368 [000] ...1 1819362.481938: tracing_mark_write: ' +
+          'SyncInterpret: end: FlingStopFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481943: tracing_mark_write: ' +
+          'SyncInterpret: end: ClickWiggleFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481947: tracing_mark_write: ' +
+          'SyncInterpret: end: PalmClassifyingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481952: tracing_mark_write: ' +
+          'SyncInterpret: end: IirFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481958: tracing_mark_write: ' +
+          'HandleTimer: end: LookaheadFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481962: tracing_mark_write: ' +
+          'HandleTimer: end: BoxFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481967: tracing_mark_write: ' +
+          'HandleTimer: end: SensorJumpFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481973: tracing_mark_write: ' +
+          'HandleTimer: end: AccelFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481977: tracing_mark_write: ' +
+          'HandleTimer: end: SplitCorrectingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481982: tracing_mark_write: ' +
+          'HandleTimer: end: ScalingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481988: tracing_mark_write: ' +
+          'HandleTimer: end: IntegralGestureFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481993: tracing_mark_write: ' +
+          'HandleTimer: end: StuckButtonInhibitorFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481998: tracing_mark_write: ' +
+          'HandleTimer: end: T5R2CorrectingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.482033: tracing_mark_write: ' +
+          'HandleTimer: end: Cr48ProfileSensorFilterInterpreter',
+      'X-30368 [000] ...1 1819362.482038: tracing_mark_write: ' +
+          'HandleTimer: end: AppleTrackpadFilterInterpreter',
+      'X-30368 [000] ...1 1819362.482043: tracing_mark_write: ' +
+          'HandleTimer: end: LoggingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.482047: tracing_mark_write: ' +
+          'log: start: TimerLogOutputs',
+      'X-30368 [000] ...1 1819362.482053: tracing_mark_write: ' +
+          'log: end: TimerLogOutputs'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 1);
+  });
+
+  test('importError', function() {
+    var lines = [
+      'X-30368 [000] ...1 1819362.481912: tracing_mark_write: ' +
+          'SyncInterpret: start: ImmediateInterpreter',
+      'X-30368 [000] ...1 1819362.481958: tracing_mark_write: ' +
+          'HandleTimer: end: LookaheadFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481962: tracing_mark_write: ' +
+          'HandleTimer: end: BoxFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481967: tracing_mark_write: ' +
+          'HandleTimer: end: SensorJumpFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481973: tracing_mark_write: ' +
+          'HandleTimer: end: AccelFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481977: tracing_mark_write: ' +
+          'HandleTimer: end: SplitCorrectingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481982: tracing_mark_write: ' +
+          'HandleTimer: end: ScalingFilterInterpreter',
+      'X-30368 [000] ...1 1819362.481988: tracing_mark_write: ' +
+          'HandleTimer: end: IntegralGestureFilterInterpreter'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(m.importWarnings.length, 7);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/i915_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/i915_parser.html
new file mode 100644
index 0000000..6cf5b3f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/i915_parser.html
@@ -0,0 +1,370 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses i915 driver events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux i915 trace events.
+   * @constructor
+   */
+  function I915Parser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('i915_gem_object_create',
+        I915Parser.prototype.gemObjectCreateEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_bind',
+        I915Parser.prototype.gemObjectBindEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_unbind',
+        I915Parser.prototype.gemObjectBindEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_change_domain',
+        I915Parser.prototype.gemObjectChangeDomainEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_pread',
+        I915Parser.prototype.gemObjectPreadWriteEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_pwrite',
+        I915Parser.prototype.gemObjectPreadWriteEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_fault',
+        I915Parser.prototype.gemObjectFaultEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_clflush',
+        // NB: reuse destroy handler
+        I915Parser.prototype.gemObjectDestroyEvent.bind(this));
+    importer.registerEventHandler('i915_gem_object_destroy',
+        I915Parser.prototype.gemObjectDestroyEvent.bind(this));
+    importer.registerEventHandler('i915_gem_ring_dispatch',
+        I915Parser.prototype.gemRingDispatchEvent.bind(this));
+    importer.registerEventHandler('i915_gem_ring_flush',
+        I915Parser.prototype.gemRingFlushEvent.bind(this));
+    importer.registerEventHandler('i915_gem_request',
+        I915Parser.prototype.gemRequestEvent.bind(this));
+    importer.registerEventHandler('i915_gem_request_add',
+        I915Parser.prototype.gemRequestEvent.bind(this));
+    importer.registerEventHandler('i915_gem_request_complete',
+        I915Parser.prototype.gemRequestEvent.bind(this));
+    importer.registerEventHandler('i915_gem_request_retire',
+        I915Parser.prototype.gemRequestEvent.bind(this));
+    importer.registerEventHandler('i915_gem_request_wait_begin',
+        I915Parser.prototype.gemRequestEvent.bind(this));
+    importer.registerEventHandler('i915_gem_request_wait_end',
+        I915Parser.prototype.gemRequestEvent.bind(this));
+    importer.registerEventHandler('i915_gem_ring_wait_begin',
+        I915Parser.prototype.gemRingWaitEvent.bind(this));
+    importer.registerEventHandler('i915_gem_ring_wait_end',
+        I915Parser.prototype.gemRingWaitEvent.bind(this));
+    importer.registerEventHandler('i915_reg_rw',
+        I915Parser.prototype.regRWEvent.bind(this));
+    importer.registerEventHandler('i915_flip_request',
+        I915Parser.prototype.flipEvent.bind(this));
+    importer.registerEventHandler('i915_flip_complete',
+        I915Parser.prototype.flipEvent.bind(this));
+    importer.registerEventHandler('intel_gpu_freq_change',
+        I915Parser.prototype.gpuFrequency.bind(this));
+  }
+
+  I915Parser.prototype = {
+    __proto__: Parser.prototype,
+
+    i915FlipOpenSlice: function(ts, obj, plane) {
+      // use i915_flip_obj_plane?
+      var kthread = this.importer.getOrCreatePseudoThread('i915_flip');
+      kthread.openSliceTS = ts;
+      kthread.openSlice = 'flip:' + obj + '/' + plane;
+    },
+
+    i915FlipCloseSlice: function(ts, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('i915_flip');
+      if (kthread.openSlice) {
+        var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+            tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+            kthread.openSliceTS,
+            args,
+            ts - kthread.openSliceTS);
+
+        kthread.thread.sliceGroup.pushSlice(slice);
+      }
+      kthread.openSlice = undefined;
+    },
+
+    i915GemObjectSlice: function(ts, eventName, obj, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('i915_gem');
+      kthread.openSlice = eventName + ':' + obj;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    i915GemRingSlice: function(ts, eventName, dev, ring, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('i915_gem_ring');
+      kthread.openSlice = eventName + ':' + dev + '.' + ring;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    i915RegSlice: function(ts, eventName, reg, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('i915_reg');
+      kthread.openSlice = eventName + ':' + reg;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    i915FreqChangeSlice: function(ts, eventName, args) {
+      var kthread = this.importer.getOrCreatePseudoThread('i915_gpu_freq');
+      kthread.openSlice = eventName;
+      var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+          tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+          ts, args, 0);
+
+      kthread.thread.sliceGroup.pushSlice(slice);
+    },
+
+    /**
+     * Parses i915 driver events and sets up state in the importer.
+     */
+    gemObjectCreateEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /obj=(\w+), size=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var obj = event[1];
+      var size = parseInt(event[2]);
+      this.i915GemObjectSlice(ts, eventName, obj,
+          {
+            obj: obj,
+            size: size
+          });
+      return true;
+    },
+
+    gemObjectBindEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      // TODO(sleffler) mappable
+      var event = /obj=(\w+), offset=(\w+), size=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var obj = event[1];
+      var offset = event[2];
+      var size = parseInt(event[3]);
+      this.i915ObjectGemSlice(ts, eventName + ':' + obj,
+          {
+            obj: obj,
+            offset: offset,
+            size: size
+          });
+      return true;
+    },
+
+    gemObjectChangeDomainEvent: function(eventName, cpuNumber, pid, ts,
+                                         eventBase) {
+      var event = /obj=(\w+), read=(\w+=>\w+), write=(\w+=>\w+)/
+          .exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var obj = event[1];
+      var read = event[2];
+      var write = event[3];
+      this.i915GemObjectSlice(ts, eventName, obj,
+          {
+            obj: obj,
+            read: read,
+            write: write
+          });
+      return true;
+    },
+
+    gemObjectPreadWriteEvent: function(eventName, cpuNumber, pid, ts,
+                                       eventBase) {
+      var event = /obj=(\w+), offset=(\d+), len=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var obj = event[1];
+      var offset = parseInt(event[2]);
+      var len = parseInt(event[3]);
+      this.i915GemObjectSlice(ts, eventName, obj,
+          {
+            obj: obj,
+            offset: offset,
+            len: len
+          });
+      return true;
+    },
+
+    gemObjectFaultEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      // TODO(sleffler) writable
+      var event = /obj=(\w+), (\w+) index=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var obj = event[1];
+      var type = event[2];
+      var index = parseInt(event[3]);
+      this.i915GemObjectSlice(ts, eventName, obj,
+          {
+            obj: obj,
+            type: type,
+            index: index
+          });
+      return true;
+    },
+
+    gemObjectDestroyEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /obj=(\w+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var obj = event[1];
+      this.i915GemObjectSlice(ts, eventName, obj,
+          {
+            obj: obj
+          });
+      return true;
+    },
+
+    gemRingDispatchEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev=(\d+), ring=(\d+), seqno=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var dev = parseInt(event[1]);
+      var ring = parseInt(event[2]);
+      var seqno = parseInt(event[3]);
+      this.i915GemRingSlice(ts, eventName, dev, ring,
+          {
+            dev: dev,
+            ring: ring,
+            seqno: seqno
+          });
+      return true;
+    },
+
+    gemRingFlushEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev=(\d+), ring=(\w+), invalidate=(\w+), flush=(\w+)/
+          .exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var dev = parseInt(event[1]);
+      var ring = parseInt(event[2]);
+      var invalidate = event[3];
+      var flush = event[4];
+      this.i915GemRingSlice(ts, eventName, dev, ring,
+          {
+            dev: dev,
+            ring: ring,
+            invalidate: invalidate,
+            flush: flush
+          });
+      return true;
+    },
+
+    gemRequestEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev=(\d+), ring=(\d+), seqno=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var dev = parseInt(event[1]);
+      var ring = parseInt(event[2]);
+      var seqno = parseInt(event[3]);
+      this.i915GemRingSlice(ts, eventName, dev, ring,
+          {
+            dev: dev,
+            ring: ring,
+            seqno: seqno
+          });
+      return true;
+    },
+
+    gemRingWaitEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /dev=(\d+), ring=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var dev = parseInt(event[1]);
+      var ring = parseInt(event[2]);
+      this.i915GemRingSlice(ts, eventName, dev, ring,
+          {
+            dev: dev,
+            ring: ring
+          });
+      return true;
+    },
+
+    regRWEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /(\w+) reg=(\w+), len=(\d+), val=(\(\w+, \w+\))/
+          .exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var rw = event[1];
+      var reg = event[2];
+      var len = event[3];
+      var data = event[3];
+      this.i915RegSlice(ts, rw, reg,
+          {
+            rw: rw,
+            reg: reg,
+            len: len,
+            data: data
+          });
+      return true;
+    },
+
+    flipEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /plane=(\d+), obj=(\w+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var plane = parseInt(event[1]);
+      var obj = event[2];
+      if (eventName == 'i915_flip_request')
+        this.i915FlipOpenSlice(ts, obj, plane);
+      else
+        this.i915FlipCloseSlice(ts,
+            {
+              obj: obj,
+              plane: plane
+            });
+      return true;
+    },
+
+    gpuFrequency: function(eventName, cpuNumver, pid, ts, eventBase) {
+      var event = /new_freq=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+      var freq = parseInt(event[1]);
+
+      this.i915FreqChangeSlice(ts, eventName, {
+            freq: freq
+          });
+      return true;
+    }
+  };
+
+  Parser.register(I915Parser);
+
+  return {
+    I915Parser: I915Parser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/i915_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/i915_parser_test.html
new file mode 100644
index 0000000..40b723f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/i915_parser_test.html
@@ -0,0 +1,103 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('i915Import', function() {
+    var lines = [
+      // NB: spliced from different traces; mismatched timestamps don't matter
+      '          chrome-1223  [000]  2784.773556: i915_gem_object_pwrite: ' +
+                 'obj=ffff88013f13fc00, offset=0, len=2984',
+      '          chrome-1539  [000] 18420.677750: ' +
+                 'i915_gem_object_change_domain: ' +
+                 'obj=ffff8800a88d1400, read=44=>40, write=00=>40',
+      '          chrome-1539  [000] 18420.677759: i915_gem_object_fault: ' +
+                 'obj=ffff8800a88d1400, GTT index=0 , writable',
+      '               X-964   [000]  2784.774864: i915_flip_request: ' +
+                 'plane=0, obj=ffff88013f0b9a00',
+      '          <idle>-0     [000]  2784.788644: i915_flip_complete: ' +
+                 'plane=0, obj=ffff88013f0b9a00',
+      '          chrome-1539  [001] 18420.681687: i915_gem_request_retire: ' +
+                 'dev=0, ring=1, seqno=1178152',
+      '          chrome-1539  [000] 18422.955688: i915_gem_request_add: ' +
+                 'dev=0, ring=1, seqno=1178364',
+      '             cat-21833 [000] 18422.956832: i915_gem_request_complete: ' +
+                 'dev=0, ring=1, seqno=1178364',
+      '             X-1012  [001] 18420.682511: i915_gem_request_wait_begin: ' +
+                 'dev=0, ring=4, seqno=1178156',
+      '               X-1012  [000] 18422.765707: i915_gem_request_wait_end: ' +
+                 'dev=0, ring=4, seqno=1178359',
+      '          chrome-1539  [000] 18422.955655: i915_gem_ring_flush: ' +
+                 'dev=0, ring=1, invalidate=001e, flush=0040',
+      '          chrome-1539  [000] 18422.955660: i915_gem_ring_dispatch: ' +
+                 'dev=0, ring=1, seqno=1178364',
+      '          chrome-1539  [000] 18420.677772: i915_reg_rw: ' +
+              'write reg=0x100030, len=8, val=(0xfca9001, 0xfce8007)',
+      '          kworker/u16:2-13998 [005] 1577664.436065: ' +
+              'intel_gpu_freq_change: new_freq=350'
+
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var i915GemThread = undefined;
+    var i915FlipThread = undefined;
+    var i915GemRingThread = undefined;
+    var i915RegThread = undefined;
+    var i915GpuFreqThread = undefined;
+    m.getAllThreads().forEach(function(t) {
+      switch (t.name) {
+        case 'i915_gem':
+          i915GemThread = t;
+          break;
+        case 'i915_flip':
+          i915FlipThread = t;
+          break;
+        case 'i915_gem_ring':
+          i915GemRingThread = t;
+          break;
+        case 'i915_reg':
+          i915RegThread = t;
+          break;
+        case 'i915_gpu_freq':
+          i915GpuFreqThread = t;
+          break;
+        default:
+          throw new unittest.TestError('Unexpected thread named ' + t.name);
+      }
+    });
+    assert.isDefined(i915GemThread);
+    assert.isDefined(i915FlipThread);
+    assert.isDefined(i915GemRingThread);
+    assert.isDefined(i915RegThread);
+    assert.isDefined(i915GpuFreqThread);
+
+    assert.equal(i915GemThread.sliceGroup.length, 3);
+
+    assert.equal(i915FlipThread.sliceGroup.length, 1);
+
+    assert.closeTo(
+        2784.774864 * 1000.0,
+        i915FlipThread.sliceGroup.slices[0].start,
+        1e-5);
+    assert.closeTo(
+        (2784.788644 - 2784.774864) * 1000.0,
+        i915FlipThread.sliceGroup.slices[0].duration,
+        1e-5);
+
+    assert.equal(i915GemRingThread.sliceGroup.length, 7);
+    assert.equal(i915RegThread.sliceGroup.length, 1);
+    assert.equal(i915GpuFreqThread.sliceGroup.length, 1);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/irq_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/irq_parser.html
new file mode 100644
index 0000000..42219a8
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/irq_parser.html
@@ -0,0 +1,141 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses drm driver events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux irq trace events.
+   * @constructor
+   */
+  function IrqParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('irq_handler_entry',
+        IrqParser.prototype.irqHandlerEntryEvent.bind(this));
+    importer.registerEventHandler('irq_handler_exit',
+        IrqParser.prototype.irqHandlerExitEvent.bind(this));
+    importer.registerEventHandler('softirq_raise',
+        IrqParser.prototype.softirqRaiseEvent.bind(this));
+    importer.registerEventHandler('softirq_entry',
+        IrqParser.prototype.softirqEntryEvent.bind(this));
+    importer.registerEventHandler('softirq_exit',
+        IrqParser.prototype.softirqExitEvent.bind(this));
+  }
+
+  // Matches the irq_handler_entry record
+  var irqHandlerEntryRE = /irq=(\d+) name=(.+)/;
+
+  // Matches the irq_handler_exit record
+  var irqHandlerExitRE = /irq=(\d+) ret=(.+)/;
+
+  // Matches the softirq_raise record
+  var softirqRE = /vec=(\d+) \[action=(.+)\]/;
+
+  IrqParser.prototype = {
+    __proto__: Parser.prototype,
+
+    /**
+     * Parses irq events and sets up state in the mporter.
+     */
+    irqHandlerEntryEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = irqHandlerEntryRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var irq = parseInt(event[1]);
+      var name = event[2];
+
+      var thread = this.importer.getOrCreatePseudoThread(
+          'irqs cpu ' + cpuNumber);
+      thread.lastEntryTs = ts;
+      thread.irqName = name;
+
+      return true;
+    },
+
+    irqHandlerExitEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = irqHandlerExitRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var irq = parseInt(event[1]);
+      var ret = event[2];
+      var thread = this.importer.getOrCreatePseudoThread(
+          'irqs cpu ' + cpuNumber);
+
+      if (thread.lastEntryTs !== undefined) {
+        var duration = ts - thread.lastEntryTs;
+        var slice = new tv.c.trace_model.Slice(
+            '', thread.irqName,
+            tv.b.ui.getColorIdForGeneralPurposeString(event[1]),
+            thread.lastEntryTs, { ret: ret },
+            duration);
+        thread.thread.sliceGroup.pushSlice(slice);
+      }
+      thread.lastEntryTs = undefined;
+      thread.irqName = undefined;
+      return true;
+    },
+
+    softirqRaiseEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      return true;
+    },
+
+    softirqEntryEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = softirqRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var action = event[2];
+      var thread = this.importer.getOrCreatePseudoThread(
+          'softirq cpu ' + cpuNumber);
+      thread.lastEntryTs = ts;
+
+      return true;
+    },
+
+    softirqExitEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = softirqRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var vec = parseInt(event[1]);
+      var action = event[2];
+      var thread = this.importer.getOrCreatePseudoThread(
+          'softirq cpu ' + cpuNumber);
+
+      if (thread.lastEntryTs !== undefined) {
+        var duration = ts - thread.lastEntryTs;
+        var slice = new tv.c.trace_model.Slice(
+            '', action,
+            tv.b.ui.getColorIdForGeneralPurposeString(event[1]),
+            thread.lastEntryTs, { vec: vec },
+            duration);
+        thread.thread.sliceGroup.pushSlice(slice);
+      }
+      thread.lastEntryTs = undefined;
+      return true;
+    }
+  };
+
+  Parser.register(IrqParser);
+
+  return {
+    IrqParser: IrqParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/irq_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/irq_parser_test.html
new file mode 100644
index 0000000..06f2b83
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/irq_parser_test.html
@@ -0,0 +1,41 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('irqImport', function() {
+    var lines = [
+      ' kworker/u4:1-31907 (31907) [001] d.h3 14063.748288: ' +
+        'irq_handler_entry: irq=27 name=arch_timer',
+      ' kworker/u4:1-31907 (31907) [001] dNh3 14063.748384: ' +
+        'irq_handler_exit: irq=27 ret=handled',
+      ' kworker/u4:2-31908 (31908) [000] ..s3 14063.477231: ' +
+        'softirq_entry: vec=9 [action=RCU]',
+      ' kworker/u4:2-31908 (31908) [000] ..s3 14063.477246: ' +
+        'softirq_exit: vec=9 [action=RCU]'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 2);
+
+    var threads = m.findAllThreadsNamed('irqs cpu 1');
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].sliceGroup.length, 1);
+
+    var threads = m.findAllThreadsNamed('softirq cpu 0');
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].sliceGroup.length, 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/kfunc_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/kfunc_parser.html
new file mode 100644
index 0000000..6f24a8d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/kfunc_parser.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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses graph_ent and graph_ret events that were inserted by
+ * the Linux kernel's function graph trace.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var LinuxPerfParser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses graph_ent and graph_ret events that were inserted by the Linux
+   * kernel's function graph trace.
+   * @constructor
+   */
+  function KernelFuncParser(importer) {
+    LinuxPerfParser.call(this, importer);
+
+    importer.registerEventHandler('graph_ent',
+        KernelFuncParser.prototype.traceKernelFuncEnterEvent.
+            bind(this));
+    importer.registerEventHandler('graph_ret',
+        KernelFuncParser.prototype.traceKernelFuncReturnEvent.
+            bind(this));
+
+    this.model_ = importer.model_;
+    this.ppids_ = {};
+  }
+
+  var TestExports = {};
+
+  var funcEnterRE = new RegExp('func=(.+)');
+  TestExports.funcEnterRE = funcEnterRE;
+
+  KernelFuncParser.prototype = {
+    __proto__: LinuxPerfParser.prototype,
+
+    traceKernelFuncEnterEvent: function(eventName, cpuNumber, pid, ts,
+                                        eventBase) {
+      var eventData = funcEnterRE.exec(eventBase.details);
+      if (!eventData)
+        return false;
+
+      if (eventBase.tgid === undefined) {
+        return false;
+      }
+
+      var tgid = parseInt(eventBase.tgid);
+      var name = eventData[1];
+      var thread = this.model_.getOrCreateProcess(tgid)
+        .getOrCreateThread(pid);
+      thread.name = eventBase.threadName;
+
+      var slices = thread.kernelSliceGroup;
+      if (!slices.isTimestampValidForBeginOrEnd(ts)) {
+        this.model_.importWarning({
+          type: 'parse_error',
+          message: 'Timestamps are moving backward.'
+        });
+        return false;
+      }
+
+      var slice = slices.beginSlice(null, name, ts, {});
+
+      return true;
+    },
+
+    traceKernelFuncReturnEvent: function(eventName, cpuNumber, pid, ts,
+                                         eventBase) {
+      if (eventBase.tgid === undefined) {
+        return false;
+      }
+
+      var tgid = parseInt(eventBase.tgid);
+      var thread = this.model_.getOrCreateProcess(tgid)
+        .getOrCreateThread(pid);
+      thread.name = eventBase.threadName;
+
+      var slices = thread.kernelSliceGroup;
+      if (!slices.isTimestampValidForBeginOrEnd(ts)) {
+        this.model_.importWarning({
+          type: 'parse_error',
+          message: 'Timestamps are moving backward.'
+        });
+        return false;
+      }
+
+      if (slices.openSliceCount > 0) {
+        slices.endSlice(ts);
+      }
+
+      return true;
+    }
+  };
+
+  LinuxPerfParser.register(KernelFuncParser);
+
+  return {
+    KernelFuncParser: KernelFuncParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/kfunc_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/kfunc_parser_test.html
new file mode 100644
index 0000000..a4c9e37
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/kfunc_parser_test.html
@@ -0,0 +1,58 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('kernelFunctionParser', function() {
+    var lines = [
+      'Binder_2-127  ( 127) [001] ....  3431.906759: graph_ent: func=sys_write',
+      'Binder_2-127  ( 127) [001] ....  3431.906769: graph_ret: func=sys_write',
+      'Binder_2-127  ( 127) [001] ....  3431.906785: graph_ent: func=sys_write',
+      'Binder_2-127  ( 127) [001] ...1  3431.906798: tracing_mark_write: B|' +
+          '127|dequeueBuffer',
+      'Binder_2-127  ( 127) [001] ....  3431.906802: graph_ret: func=sys_write',
+      'Binder_2-127  ( 127) [001] ....  3431.906842: graph_ent: func=sys_write',
+      'Binder_2-127  ( 127) [001] ...1  3431.906849: tracing_mark_write: E',
+      'Binder_2-127  ( 127) [001] ....  3431.906853: graph_ret: func=sys_write',
+      'Binder_2-127  ( 127) [001] ....  3431.906896: graph_ent: func=sys_write',
+      'Binder_2-127  ( 127) [001] ....  3431.906906: graph_ret: func=sys_write'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var process = m.processes[127];
+    assert.isNotNull(process);
+
+    var thread = process.threads[127];
+    assert.isNotNull(thread);
+
+    var slices = thread.sliceGroup.slices;
+    assert.equal(thread.sliceGroup.length, 7);
+
+    // Slice 0 is an un-split sys_write
+    assert.equal(slices[0].title, 'sys_write');
+
+    // Slices 1 & 3 are a split sys_write
+    assert.equal(slices[1].title, 'sys_write');
+    assert.equal(slices[2].title, 'dequeueBuffer');
+    assert.equal(slices[3].title, 'sys_write (cont.)');
+
+    // Slices 4 & 5 are a split sys_write with the dequeueBuffer in between
+    assert.equal(slices[4].title, 'sys_write');
+    assert.equal(slices[5].title, 'sys_write (cont.)');
+
+    // Slice 6 is another un-split sys_write
+    assert.equal(slices[6].title, 'sys_write');
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer.html
new file mode 100644
index 0000000..f4f1656
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer.html
@@ -0,0 +1,796 @@
+<!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/trace_model.html">
+<link rel="import" href="/base/ui/color_scheme.html">
+<link rel="import" href="/extras/importer/linux_perf/bus_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/clock_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/cpufreq_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/disk_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/drm_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/irq_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/exynos_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/gesture_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/i915_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/mali_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/memreclaim_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/power_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/regulator_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/sched_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/sync_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/workqueue_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/android_parser.html">
+<link rel="import" href="/extras/importer/linux_perf/kfunc_parser.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/core/importer/simple_line_reader.html">
+
+
+<script>
+/**
+ * @fileoverview Imports text files in the Linux event trace format into the
+ * Tracemodel. This format is output both by sched_trace and by Linux's perf
+ * tool.
+ *
+ * This importer assumes the events arrive as a string. The unit tests provide
+ * examples of the trace format.
+ *
+ * Linux scheduler traces use a definition for 'pid' that is different than
+ * tracing uses. Whereas tracing uses pid to identify a specific process, a pid
+ * in a linux trace refers to a specific thread within a process. Within this
+ * file, we the definition used in Linux traces, as it improves the importing
+ * code's readability.
+ */
+'use strict';
+
+tv.exportTo('tv.e.importer.linux_perf', function() {
+  var Importer = tv.c.importer.Importer;
+  var ClockSyncRecord = tv.c.ClockSyncRecord;
+
+  /**
+   * Imports linux perf events into a specified model.
+   * @constructor
+   */
+  function LinuxPerfImporter(model, events) {
+    this.importPriority = 2;
+    this.model_ = model;
+    this.events_ = events;
+    this.newlyAddedClockSyncRecords_ = [];
+    this.wakeups_ = [];
+    this.kernelThreadStates_ = {};
+    this.buildMapFromLinuxPidsToThreads();
+    this.lines_ = [];
+    this.pseudoThreadCounter = 1;
+    this.parsers_ = [];
+    this.eventHandlers_ = {};
+  }
+
+  var TestExports = {};
+
+  // Matches the trace record in 3.2 and later with the print-tgid option:
+  //          <idle>-0    0 [001] d...  1.23: sched_switch
+  //
+  // A TGID (Thread Group ID) is basically what the Linux kernel calls what
+  // userland refers to as a process ID (as opposed to a Linux pid, which is
+  // what userland calls a thread ID).
+  var lineREWithTGID = new RegExp(
+      '^\\s*(.+)-(\\d+)\\s+\\(\\s*(\\d+|-+)\\)\\s\\[(\\d+)\\]' +
+      '\\s+[dX.][N.][Hhs.][0-9a-f.]' +
+      '\\s+(\\d+\\.\\d+):\\s+(\\S+):\\s(.*)$');
+  var lineParserWithTGID = function(line) {
+    var groups = lineREWithTGID.exec(line);
+    if (!groups) {
+      return groups;
+    }
+
+    var tgid = groups[3];
+    if (tgid[0] === '-')
+      tgid = undefined;
+
+    return {
+      threadName: groups[1],
+      pid: groups[2],
+      tgid: tgid,
+      cpuNumber: groups[4],
+      timestamp: groups[5],
+      eventName: groups[6],
+      details: groups[7]
+    };
+  };
+  TestExports.lineParserWithTGID = lineParserWithTGID;
+
+  // Matches the default trace record in 3.2 and later (includes irq-info):
+  //          <idle>-0     [001] d...  1.23: sched_switch
+  var lineREWithIRQInfo = new RegExp(
+      '^\\s*(.+)-(\\d+)\\s+\\[(\\d+)\\]' +
+      '\\s+[dX.][N.][Hhs.][0-9a-f.]' +
+      '\\s+(\\d+\\.\\d+):\\s+(\\S+):\\s(.*)$');
+  var lineParserWithIRQInfo = function(line) {
+    var groups = lineREWithIRQInfo.exec(line);
+    if (!groups) {
+      return groups;
+    }
+    return {
+      threadName: groups[1],
+      pid: groups[2],
+      cpuNumber: groups[3],
+      timestamp: groups[4],
+      eventName: groups[5],
+      details: groups[6]
+    };
+  };
+  TestExports.lineParserWithIRQInfo = lineParserWithIRQInfo;
+
+  // Matches the default trace record pre-3.2:
+  //          <idle>-0     [001]  1.23: sched_switch
+  var lineREWithLegacyFmt =
+      /^\s*(.+)-(\d+)\s+\[(\d+)\]\s*(\d+\.\d+):\s+(\S+):\s(.*)$/;
+  var lineParserWithLegacyFmt = function(line) {
+    var groups = lineREWithLegacyFmt.exec(line);
+    if (!groups) {
+      return groups;
+    }
+    return {
+      threadName: groups[1],
+      pid: groups[2],
+      cpuNumber: groups[3],
+      timestamp: groups[4],
+      eventName: groups[5],
+      details: groups[6]
+    };
+  };
+  TestExports.lineParserWithLegacyFmt = lineParserWithLegacyFmt;
+
+  // Matches the trace_event_clock_sync record
+  //  0: trace_event_clock_sync: parent_ts=19581477508
+  var traceEventClockSyncRE = /trace_event_clock_sync: parent_ts=(\d+\.?\d*)/;
+  TestExports.traceEventClockSyncRE = traceEventClockSyncRE;
+
+  var genericClockSyncRE = /trace_event_clock_sync: name=(\w+)/;
+
+  // Some kernel trace events are manually classified in slices and
+  // hand-assigned a pseudo PID.
+  var pseudoKernelPID = 0;
+
+  /**
+   * Deduce the format of trace data. Linux kernels prior to 3.3 used one
+   * format (by default); 3.4 and later used another.  Additionally, newer
+   * kernels can optionally trace the TGID.
+   *
+   * @return {function} the function for parsing data when the format is
+   * recognized; otherwise null.
+   */
+  function autoDetectLineParser(line) {
+    if (line[0] == '{')
+      return false;
+    if (lineREWithTGID.test(line))
+      return lineParserWithTGID;
+    if (lineREWithIRQInfo.test(line))
+      return lineParserWithIRQInfo;
+    if (lineREWithLegacyFmt.test(line))
+      return lineParserWithLegacyFmt;
+    return null;
+  };
+  TestExports.autoDetectLineParser = autoDetectLineParser;
+
+  /**
+   * Guesses whether the provided events is a Linux perf string.
+   * Looks for the magic string "# tracer" at the start of the file,
+   * or the typical task-pid-cpu-timestamp-function sequence of a typical
+   * trace's body.
+   *
+   * @return {boolean} True when events is a linux perf array.
+   */
+  LinuxPerfImporter.canImport = function(events) {
+    if (!(typeof(events) === 'string' || events instanceof String))
+      return false;
+
+    if (LinuxPerfImporter._extractEventsFromSystraceHTML(events, false).ok)
+      return true;
+
+    if (/^# tracer:/.test(events))
+      return true;
+
+    var m = /^(.+)\n/.exec(events);
+    if (m)
+      events = m[1];
+    if (autoDetectLineParser(events))
+      return true;
+
+    return false;
+  };
+
+  LinuxPerfImporter._extractEventsFromSystraceHTML = function(
+      incoming_events, produce_result) {
+    var failure = {ok: false};
+    if (produce_result === undefined)
+      produce_result = true;
+
+    if (/^<!DOCTYPE HTML>/.test(incoming_events) == false)
+      return failure;
+    var r = new tv.c.importer.SimpleLineReader(incoming_events);
+
+    // Try to find the data...
+    if (!r.advanceToLineMatching(/^  <script>$/))
+      return failure;
+    if (!r.advanceToLineMatching(/^  var linuxPerfData = "\\$/))
+      return failure;
+
+    var events_begin_at_line = r.curLineNumber + 1;
+    r.beginSavingLines();
+    if (!r.advanceToLineMatching(/^  <\/script>$/))
+      return failure;
+
+    var raw_events = r.endSavingLinesAndGetResult();
+
+    // Drop off first and last event as it contains the tag.
+    raw_events = raw_events.slice(1, raw_events.length - 1);
+
+    if (!r.advanceToLineMatching(/^<\/body>$/))
+      return failure;
+    if (!r.advanceToLineMatching(/^<\/html>$/))
+      return failure;
+
+    function endsWith(str, suffix) {
+      return str.indexOf(suffix, str.length - suffix.length) !== -1;
+    }
+    function stripSuffix(str, suffix) {
+      if (!endsWith(str, suffix))
+        return str;
+      return str.substring(str, str.length - suffix.length);
+    }
+
+    // Strip off escaping in the file needed to preserve linebreaks.
+    var events = [];
+    if (produce_result) {
+      for (var i = 0; i < raw_events.length; i++) {
+        var event = raw_events[i];
+        event = stripSuffix(event, '\\n\\');
+        events.push(event);
+      }
+    } else {
+      events = [raw_events[raw_events.length - 1]];
+    }
+
+    // Last event ends differently. Strip that off too,
+    // treating absence of that trailing string as a failure.
+    var oldLastEvent = events[events.length - 1];
+    var newLastEvent = stripSuffix(oldLastEvent, '\\n";');
+    if (newLastEvent == oldLastEvent)
+      return failure;
+    events[events.length - 1] = newLastEvent;
+
+    return {ok: true,
+      lines: produce_result ? events : undefined,
+      events_begin_at_line: events_begin_at_line};
+  };
+
+  LinuxPerfImporter.prototype = {
+    __proto__: Importer.prototype,
+
+    get model() {
+      return this.model_;
+    },
+
+    /**
+     * Precomputes a lookup table from linux pids back to existing
+     * Threads. This is used during importing to add information to each
+     * thread about whether it was running, descheduled, sleeping, et
+     * cetera.
+     */
+    buildMapFromLinuxPidsToThreads: function() {
+      this.threadsByLinuxPid = {};
+      this.model_.getAllThreads().forEach(
+          function(thread) {
+            this.threadsByLinuxPid[thread.tid] = thread;
+          }.bind(this));
+    },
+
+    /**
+     * @return {Cpu} A Cpu corresponding to the given cpuNumber.
+     */
+    getOrCreateCpu: function(cpuNumber) {
+      return this.model_.kernel.getOrCreateCpu(cpuNumber);
+    },
+
+    /**
+     * @return {TimelineThread} A thread corresponding to the kernelThreadName.
+     */
+    getOrCreateKernelThread: function(kernelThreadName, pid, tid) {
+      if (!this.kernelThreadStates_[kernelThreadName]) {
+        var thread = this.model_.getOrCreateProcess(pid).getOrCreateThread(tid);
+        thread.name = kernelThreadName;
+        this.kernelThreadStates_[kernelThreadName] = {
+          pid: pid,
+          thread: thread,
+          openSlice: undefined,
+          openSliceTS: undefined
+        };
+        this.threadsByLinuxPid[pid] = thread;
+      }
+      return this.kernelThreadStates_[kernelThreadName];
+    },
+
+    /**
+     * @return {TimelineThread} A pseudo thread corresponding to the
+     * threadName.  Pseudo threads are for events that we want to break
+     * out to a separate timeline but would not otherwise happen.
+     * These threads are assigned to pseudoKernelPID and given a
+     * unique (incrementing) TID.
+     */
+    getOrCreatePseudoThread: function(threadName) {
+      var thread = this.kernelThreadStates_[threadName];
+      if (!thread) {
+        thread = this.getOrCreateKernelThread(threadName, pseudoKernelPID,
+            this.pseudoThreadCounter);
+        this.pseudoThreadCounter++;
+      }
+      return thread;
+    },
+
+    /**
+     * Imports the data in this.events_ into model_.
+     */
+    importEvents: function(isSecondaryImport) {
+      this.parsers_ = this.createParsers_();
+      this.registerDefaultHandlers_();
+      this.parseLines();
+      this.importClockSyncRecords();
+      var timeShift = this.computeTimeTransform();
+      if (timeShift === undefined) {
+        this.model_.importWarning({
+          type: 'clock_sync',
+          message: 'Cannot import kernel trace without a clock sync.'
+        });
+        return;
+      }
+      this.shiftNewlyAddedClockSyncRecords(timeShift);
+      this.importCpuData(timeShift);
+      this.buildMapFromLinuxPidsToThreads();
+      this.buildPerThreadCpuSlicesFromCpuState();
+      this.computeCpuTimestampsForSlicesAsNeeded();
+    },
+
+    /**
+     * Builds the timeSlices array on each thread based on our knowledge of what
+     * each Cpu is doing.  This is done only for Threads that are
+     * already in the model, on the assumption that not having any traced data
+     * on a thread means that it is not of interest to the user.
+     */
+    buildPerThreadCpuSlicesFromCpuState: function() {
+      // Push the cpu slices to the threads that they run on.
+      for (var cpuNumber in this.model_.kernel.cpus) {
+        var cpu = this.model_.kernel.cpus[cpuNumber];
+
+        for (var i = 0; i < cpu.slices.length; i++) {
+          var cpuSlice = cpu.slices[i];
+
+          var thread = this.threadsByLinuxPid[cpuSlice.args.tid];
+          if (!thread)
+            continue;
+
+          cpuSlice.threadThatWasRunning = thread;
+
+          if (!thread.tempCpuSlices)
+            thread.tempCpuSlices = [];
+          thread.tempCpuSlices.push(cpuSlice);
+        }
+      }
+
+      for (var i in this.wakeups_) {
+        var wakeup = this.wakeups_[i];
+        var thread = this.threadsByLinuxPid[wakeup.tid];
+        if (!thread)
+          continue;
+        thread.tempWakeups = thread.tempWakeups || [];
+        thread.tempWakeups.push(wakeup);
+      }
+
+      // Create slices for when the thread is not running.
+      var getColorIdForReservedName = tv.b.ui.getColorIdForReservedName;
+      var runningId = getColorIdForReservedName('thread_state_running');
+      var runnableId = getColorIdForReservedName('thread_state_runnable');
+      var sleepingId = getColorIdForReservedName('thread_state_sleeping');
+      var ioWaitId = getColorIdForReservedName('thread_state_iowait');
+      var unknownStateId = getColorIdForReservedName('thread_state_unknown');
+
+      this.model_.getAllThreads().forEach(function(thread) {
+        if (thread.tempCpuSlices === undefined)
+          return;
+        var origSlices = thread.tempCpuSlices;
+        delete thread.tempCpuSlices;
+
+        origSlices.sort(function(x, y) {
+          return x.start - y.start;
+        });
+
+        var wakeups = thread.tempWakeups || [];
+        delete thread.tempWakeups;
+        wakeups.sort(function(x, y) {
+          return x.ts - y.ts;
+        });
+
+        // Walk the slice list and put slices between each original slice to
+        // show when the thread isn't running.
+        var slices = [];
+
+        if (origSlices.length) {
+          var slice = origSlices[0];
+
+          if (wakeups.length && wakeups[0].ts < slice.start) {
+            var wakeup = wakeups.shift();
+            var wakeupDuration = slice.start - wakeup.ts;
+            var args = {'wakeup from tid': wakeup.fromTid};
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'Runnable', runnableId,
+                wakeup.ts, args, wakeupDuration));
+          }
+
+          var runningSlice = new tv.c.trace_model.ThreadTimeSlice(
+              thread, '', 'Running', runningId,
+              slice.start, {}, slice.duration);
+          runningSlice.cpuOnWhichThreadWasRunning = slice.cpu;
+          slices.push(runningSlice);
+        }
+
+        var wakeup = undefined;
+        for (var i = 1; i < origSlices.length; i++) {
+          var prevSlice = origSlices[i - 1];
+          var nextSlice = origSlices[i];
+          var midDuration = nextSlice.start - prevSlice.end;
+          while (wakeups.length && wakeups[0].ts < nextSlice.start) {
+            var w = wakeups.shift();
+            if (wakeup === undefined && w.ts > prevSlice.end) {
+              wakeup = w;
+            }
+          }
+
+          // Push a sleep slice onto the slices list, interrupting it with a
+          // wakeup if appropriate.
+          var pushSleep = function(title, id) {
+            if (wakeup !== undefined) {
+              midDuration = wakeup.ts - prevSlice.end;
+            }
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread,
+                '', title, id, prevSlice.end, {}, midDuration));
+            if (wakeup !== undefined) {
+              var wakeupDuration = nextSlice.start - wakeup.ts;
+              var args = {'wakeup from tid': wakeup.fromTid};
+              slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                  thread,
+                  '', 'Runnable', runnableId, wakeup.ts, args, wakeupDuration));
+              wakeup = undefined;
+            }
+          };
+
+          if (prevSlice.args.stateWhenDescheduled == 'S') {
+            pushSleep('Sleeping', sleepingId);
+          } else if (prevSlice.args.stateWhenDescheduled == 'R' ||
+                     prevSlice.args.stateWhenDescheduled == 'R+') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread,
+                '', 'Runnable', runnableId, prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'D') {
+            pushSleep('Uninterruptible Sleep', ioWaitId);
+          } else if (prevSlice.args.stateWhenDescheduled == 'T') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', '__TASK_STOPPED', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 't') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'debug', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'Z') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'Zombie', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'X') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'Exit Dead', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'x') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'Task Dead', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'K') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'Wakekill', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'W') {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'Waking', ioWaitId,
+                prevSlice.end, {}, midDuration));
+          } else if (prevSlice.args.stateWhenDescheduled == 'D|K') {
+            pushSleep('Uninterruptible Sleep | WakeKill', ioWaitId);
+          } else if (prevSlice.args.stateWhenDescheduled == 'D|W') {
+            pushSleep('Uninterruptible Sleep | Waking', ioWaitId);
+          } else {
+            slices.push(new tv.c.trace_model.ThreadTimeSlice(
+                thread, '', 'UNKNOWN', unknownStateId,
+                prevSlice.end, {}, midDuration));
+            this.model_.importWarning({
+              type: 'parse_error',
+              message: 'Unrecognized sleep state: ' +
+                  prevSlice.args.stateWhenDescheduled
+            });
+          }
+
+          var runningSlice = new tv.c.trace_model.ThreadTimeSlice(
+              thread, '', 'Running', runningId,
+              nextSlice.start, {}, nextSlice.duration);
+          runningSlice.cpuOnWhichThreadWasRunning = prevSlice.cpu;
+          slices.push(runningSlice);
+        }
+        thread.timeSlices = slices;
+      }, this);
+    },
+
+    computeCpuTimestampsForSlicesAsNeeded: function() {
+      /* iterate all slices and try to figure out cpuStart/endTimes */
+
+    },
+
+    /**
+     * Computes a time transform from perf time to parent time based on the
+     * imported clock sync records.
+     * @return {number} offset from perf time to parent time or undefined if
+     * the necessary sync records were not found.
+     */
+    computeTimeTransform: function() {
+      var isSecondaryImport = this.model.getClockSyncRecordsNamed(
+          'linux_perf_importer').length !== 0;
+
+      var mSyncs = this.model_.getClockSyncRecordsNamed('monotonic');
+      // If this is a secondary import, and no clock syncing records were
+      // found, then abort the import. Otherwise, just skip clock alignment.
+      if (mSyncs.length == 0)
+        return isSecondaryImport ? undefined : 0;
+
+      // Shift all the slice times based on the sync record.
+      // TODO(skyostil): Compute a scaling factor if we have multiple clock sync
+      // records.
+      var sync = mSyncs[0].args;
+      // NB: parentTS of zero denotes no times-shift; this is
+      // used when user and kernel event clocks are identical.
+      if (sync.parentTS == 0 || sync.parentTS == sync.perfTS)
+        return 0;
+      return sync.parentTS - sync.perfTS;
+    },
+
+    /**
+     * Creates an instance of each registered linux perf event parser.
+     * This allows the parsers to register handlers for the events they
+     * understand.  We also register our own special handlers (for the
+     * timestamp synchronization markers).
+     */
+    createParsers_: function() {
+      // Instantiate the parsers; this will register handlers for known events
+      var allTypeInfos = tv.e.importer.linux_perf.
+          Parser.getAllRegisteredTypeInfos();
+      var parsers = allTypeInfos.map(
+          function(typeInfo) {
+            return new typeInfo.constructor(this);
+          }, this);
+
+      return parsers;
+    },
+
+    registerDefaultHandlers_: function() {
+      this.registerEventHandler('tracing_mark_write',
+          LinuxPerfImporter.prototype.traceMarkingWriteEvent.bind(this));
+      // NB: old-style trace markers; deprecated
+      this.registerEventHandler('0',
+          LinuxPerfImporter.prototype.traceMarkingWriteEvent.bind(this));
+      // Register dummy clock sync handlers to avoid warnings in the log.
+      this.registerEventHandler('tracing_mark_write:trace_event_clock_sync',
+          function() { return true; });
+      this.registerEventHandler('0:trace_event_clock_sync',
+          function() { return true; });
+    },
+
+    /**
+     * Registers a linux perf event parser used by importCpuData.
+     */
+    registerEventHandler: function(eventName, handler) {
+      // TODO(sleffler) how to handle conflicts?
+      this.eventHandlers_[eventName] = handler;
+    },
+
+    /**
+     * Records the fact that a pid has become runnable. This data will
+     * eventually get used to derive each thread's timeSlices array.
+     */
+    markPidRunnable: function(ts, pid, comm, prio, fromPid) {
+      // The the pids that get passed in to this function are Linux kernel
+      // pids, which identify threads.  The rest of trace-viewer refers to
+      // these as tids, so the change of nomenclature happens in the following
+      // construction of the wakeup object.
+      this.wakeups_.push({ts: ts, tid: pid, fromTid: fromPid});
+    },
+
+    /**
+     * Processes a trace_event_clock_sync event.
+     */
+    traceClockSyncEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      // Check for new-style clock sync records.
+      var event = /name=(\w+?)\s(.+)/.exec(eventBase.details);
+      if (event) {
+        var name = event[1];
+        var pieces = event[2].split(' ');
+        var args = {
+          perfTS: ts
+        };
+        for (var i = 0; i < pieces.length; i++) {
+          var parts = pieces[i].split('=');
+          if (parts.length != 2)
+            throw new Error('omgbbq');
+          args[parts[0]] = parts[1];
+        }
+        this.addClockSyncRecord(new ClockSyncRecord(name, ts, args));
+        return true;
+      }
+
+      // Old-style clock sync records from chromium
+      event = /parent_ts=(\d+\.?\d*)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      this.addClockSyncRecord(new ClockSyncRecord('monotonic', ts, {
+        perfTS: ts,
+        parentTS: event[1] * 1000
+      }));
+      return true;
+    },
+
+    /**
+     * Processes a trace_marking_write event.
+     */
+    traceMarkingWriteEvent: function(eventName, cpuNumber, pid, ts, eventBase,
+                                     threadName) {
+
+      // Some profiles end up with a \n\ on the end of each line. Strip it
+      // before we do the comparisons.
+      eventBase.details = eventBase.details.replace(/\\n.*$/, '');
+
+      var event = /^\s*(\w+):\s*(.*)$/.exec(eventBase.details);
+      if (!event) {
+        // Check if the event matches events traced by the Android framework
+        var tag = eventBase.details.substring(0, 2);
+        if (tag == 'B|' || tag == 'E' || tag == 'E|' || tag == 'X|' ||
+            tag == 'C|' || tag == 'S|' || tag == 'F|') {
+          eventBase.subEventName = 'android';
+        } else {
+          return false;
+        }
+      } else {
+        eventBase.subEventName = event[1];
+        eventBase.details = event[2];
+      }
+
+      var writeEventName = eventName + ':' + eventBase.subEventName;
+      var handler = this.eventHandlers_[writeEventName];
+      if (!handler) {
+        this.model_.importWarning({
+          type: 'parse_error',
+          message: 'Unknown trace_marking_write event ' + writeEventName
+        });
+        return true;
+      }
+      return handler(writeEventName, cpuNumber, pid, ts, eventBase, threadName);
+    },
+
+    /**
+     * Populates model clockSyncRecords with found clock sync markers.
+     */
+    importClockSyncRecords: function() {
+      this.forEachLine(function(text, eventBase, cpuNumber, pid, ts) {
+        var eventName = eventBase.eventName;
+        if (eventName !== 'tracing_mark_write' && eventName !== '0')
+          return;
+        if (traceEventClockSyncRE.exec(eventBase.details))
+          this.traceClockSyncEvent(eventName, cpuNumber, pid, ts, eventBase);
+        if (genericClockSyncRE.exec(eventBase.details))
+          this.traceClockSyncEvent(eventName, cpuNumber, pid, ts, eventBase);
+      }.bind(this));
+    },
+    addClockSyncRecord: function(csr) {
+      this.newlyAddedClockSyncRecords_.push(csr);
+      this.model_.clockSyncRecords.push(csr);
+    },
+
+    shiftNewlyAddedClockSyncRecords: function(timeShift) {
+      this.newlyAddedClockSyncRecords_.forEach(function(csr) {
+        csr.ts += timeShift;
+      });
+    },
+
+    /**
+     * Walks the this.events_ structure and creates Cpu objects.
+     */
+    importCpuData: function(timeShift) {
+      this.forEachLine(function(text, eventBase, cpuNumber, pid, ts) {
+        var eventName = eventBase.eventName;
+        var handler = this.eventHandlers_[eventName];
+        if (!handler) {
+          this.model_.importWarning({
+            type: 'parse_error',
+            message: 'Unknown event ' + eventName + ' (' + text + ')'
+          });
+          return;
+        }
+        ts += timeShift;
+        if (!handler(eventName, cpuNumber, pid, ts, eventBase)) {
+          this.model_.importWarning({
+            type: 'parse_error',
+            message: 'Malformed ' + eventName + ' event (' + text + ')'
+          });
+        }
+      }.bind(this));
+    },
+
+    /**
+     * Walks the this.events_ structure and populates this.lines_.
+     */
+    parseLines: function() {
+      var extractResult = LinuxPerfImporter._extractEventsFromSystraceHTML(
+          this.events_, true);
+      var lines = extractResult.ok ?
+          extractResult.lines : this.events_.split('\n');
+
+      var lineParser = null;
+      for (var lineNumber = 0; lineNumber < lines.length; ++lineNumber) {
+        var line = lines[lineNumber];
+        if (line.length == 0 || /^#/.test(line))
+          continue;
+        if (lineParser == null) {
+          lineParser = autoDetectLineParser(line);
+          if (lineParser == null) {
+            this.model_.importWarning({
+              type: 'parse_error',
+              message: 'Cannot parse line: ' + line
+            });
+            continue;
+          }
+        }
+        var eventBase = lineParser(line);
+        if (!eventBase) {
+          this.model_.importWarning({
+            type: 'parse_error',
+            message: 'Unrecognized line: ' + line
+          });
+          continue;
+        }
+
+        this.lines_.push([
+          line,
+          eventBase,
+          parseInt(eventBase.cpuNumber),
+          parseInt(eventBase.pid),
+          parseFloat(eventBase.timestamp) * 1000
+        ]);
+      }
+    },
+
+    /**
+     * Calls |handler| for every parsed line.
+     */
+    forEachLine: function(handler) {
+      for (var i = 0; i < this.lines_.length; ++i) {
+        var line = this.lines_[i];
+        handler.apply(this, line);
+      }
+    }
+  };
+
+  tv.c.importer.Importer.register(LinuxPerfImporter);
+
+  return {
+    LinuxPerfImporter: LinuxPerfImporter,
+    _LinuxPerfImporterTestExports: TestExports
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer_test.html
new file mode 100644
index 0000000..7a4a93e
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer_test.html
@@ -0,0 +1,397 @@
+<!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="/base/xhr.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var LinuxPerfImporter = tv.e.importer.linux_perf.LinuxPerfImporter;
+  var LinuxPerfImporterTestExports =
+      tv.e.importer.linux_perf._LinuxPerfImporterTestExports;
+  test('lineParserWithLegacyFmt', function() {
+    var p = LinuxPerfImporterTestExports.lineParserWithLegacyFmt; // @suppress longLineCheck
+    var x = p('   <idle>-0     [001]  4467.843475: sched_switch: ' +
+        'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+        'next_comm=SurfaceFlinger next_pid=178 next_prio=112');
+    assert.isNotNull(x);
+    assert.equal(x.threadName, '<idle>');
+    assert.equal(x.pid, '0');
+    assert.equal(x.cpuNumber, '001');
+    assert.equal(x.timestamp, '4467.843475');
+    assert.equal(x.eventName, 'sched_switch');
+    assert.equal('prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R' +
+        ' ==> next_comm=SurfaceFlinger next_pid=178 next_prio=112', x.details);
+
+    var x = p('Binder-Thread #-647   [001]   260.464294: sched_switch: ' +
+        'prev_comm=Binder Thread # prev_pid=647 prev_prio=120 prev_state=D ' +
+        ' ==> next_comm=.android.chrome next_pid=1562 next_prio=120');
+    assert.isNotNull(x);
+    assert.equal(x.threadName, 'Binder-Thread #');
+    assert.equal(x.pid, '647');
+  });
+
+  test('lineParserWithIRQInfo', function() {
+    var p = LinuxPerfImporterTestExports.lineParserWithIRQInfo; // @suppress longLineCheck
+    var x = p('     systrace.sh-5441  [001] d...  1031.091570: ' +
+        'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+    assert.isNotNull(x);
+    assert.equal(x.threadName, 'systrace.sh');
+    assert.equal(x.pid, '5441');
+    assert.equal(x.cpuNumber, '001');
+    assert.equal(x.timestamp, '1031.091570');
+    assert.equal(x.eventName, 'sched_wakeup');
+    assert.equal(x.details, 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000'); // @suppress longLineCheck
+  });
+
+  test('lineParserWithTGID', function() {
+    var p = LinuxPerfImporterTestExports.lineParserWithTGID;
+    var x = p('     systrace.sh-5441  (54321) [001] d...  1031.091570: ' +
+        'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+    assert.isNotNull(x);
+    assert.equal(x.threadName, 'systrace.sh');
+    assert.equal(x.pid, '5441');
+    assert.equal(x.tgid, '54321');
+    assert.equal(x.cpuNumber, '001');
+    assert.equal(x.timestamp, '1031.091570');
+    assert.equal(x.eventName, 'sched_wakeup');
+    assert.equal(x.details, 'comm=debugd pid=4978 prio=120 success=1 target_cpu=000'); // @suppress longLineCheck
+
+    var x = p('     systrace.sh-5441  (  321) [001] d...  1031.091570: ' +
+        'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+    assert.isNotNull(x);
+    assert.equal(x.tgid, '321');
+
+    var x = p('     systrace.sh-5441  (-----) [001] d...  1031.091570: ' +
+        'sched_wakeup: comm=debugd pid=4978 prio=120 success=1 target_cpu=000');
+    assert.isNotNull(x);
+    assert.isUndefined(x.tgid);
+  });
+
+  test('autodetectLineCornerCases', function() {
+    var detectParser =
+        LinuxPerfImporterTestExports.autoDetectLineParser;
+    var lineParserWithLegacyFmt =
+        LinuxPerfImporterTestExports.lineParserWithLegacyFmt;
+    var lineParserWithIRQInfo =
+        LinuxPerfImporterTestExports.lineParserWithIRQInfo;
+    var lineParserWithTGID =
+        LinuxPerfImporterTestExports.lineParserWithTGID;
+
+    var lineWithLegacyFmt =
+        'systrace.sh-8170  [001] 15180.978813: sched_switch: ' +
+        'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+        'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+        'next_prio=120';
+    var detected = detectParser(lineWithLegacyFmt);
+    assert.equal(lineParserWithLegacyFmt, detected);
+
+    var lineWithIRQInfo =
+        'systrace.sh-8170  [001] d... 15180.978813: sched_switch: ' +
+        'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+        'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+        'next_prio=120';
+    var detected = detectParser(lineWithIRQInfo);
+    assert.equal(lineParserWithIRQInfo, detected);
+
+    var lineWithTGID =
+        'systrace.sh-8170  (54321) [001] d... 15180.978813: sched_switch: ' +
+        'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+        'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+        'next_prio=120';
+    var detected = detectParser(lineWithTGID);
+    assert.equal(lineParserWithTGID, detected);
+  });
+
+  test('traceEventClockSyncRE', function() {
+    var re = LinuxPerfImporterTestExports.traceEventClockSyncRE; // @suppress longLineCheck
+    var x = re.exec('trace_event_clock_sync: parent_ts=19581477508');
+    assert.isNotNull(x);
+    assert.equal(x[1], '19581477508');
+
+    var x = re.exec('trace_event_clock_sync: parent_ts=123.456');
+    assert.isNotNull(x);
+    assert.equal(x[1], '123.456');
+  });
+
+  test('genericClockSync', function() {
+    var lines = [
+      '# tracer: nop',
+      '#',
+      '#           TASK-PID    CPU#    TIMESTAMP  FUNCTION',
+      '#              | |       |          |         |',
+      'sh-26121 [000] ...1 107464.0: tracing_mark_write: trace_event_clock_sync: name=battor regulator=8941_smbb_boost' // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var battorSyncs = m.getClockSyncRecordsNamed('battor');
+    assert.equal(battorSyncs.length, 1);
+    assert.equal(battorSyncs[0].ts, 107464000.0);
+    assert.equal(battorSyncs[0].args.perfTS, 107464000.0);
+    assert.equal(battorSyncs[0].args.regulator, '8941_smbb_boost');
+  });
+
+  test('canImport', function() {
+    var lines = [
+      '# tracer: nop',
+      '#',
+      '#           TASK-PID    CPU#    TIMESTAMP  FUNCTION',
+      '#              | |       |          |         |',
+      '          <idle>-0     [001]  4467.843475: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+          'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+      '  SurfaceFlinger-178   [001]  4467.843536: sched_switch: ' +
+          'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 prev_state=S ' +
+          '==> next_comm=kworker/u:2 next_pid=2844 next_prio=120',
+
+      '     kworker/u:2-2844  [001]  4467.843567: sched_switch: ' +
+          'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 prev_state=S ' +
+          '==> next_comm=swapper next_pid=0 next_prio=120',
+
+      '          <idle>-0     [001]  4467.844208: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+          'next_comm=kworker/u:2 next_pid=2844 next_prio=120'
+    ];
+    assert.isTrue(LinuxPerfImporter.canImport(lines.join('\n')));
+
+    var lines = [
+      '          <idle>-0     [001]  4467.843475: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+              'next_comm=SurfaceFlinger next_pid=178 next_prio=112'
+    ];
+    assert.isTrue(LinuxPerfImporter.canImport(lines.join('\n')));
+
+    var lines = [
+      '          <idle>-0     [001]  4467.843475: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+          'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+      '  SurfaceFlinger-178   [001]  4467.843536: sched_switch: ' +
+          'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+          'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+          'next_prio=120'
+    ];
+    assert.isTrue(LinuxPerfImporter.canImport(lines.join('\n')));
+
+    var lines = [
+      'SomeRandomText',
+      'More random text'
+    ];
+    assert.isFalse(LinuxPerfImporter.canImport(lines.join('\n')));
+  });
+
+  test('canImport34AndLater', function() {
+    var lines = [
+      '# tracer: nop',
+      '#',
+      '# entries-in-buffer/entries-written: 55191/55191   #P:2',
+      '#',
+      '#                              _-----=> irqs-off',
+      '#                             / _----=> need-resched',
+      '#                            | / _---=> hardirq/softirq',
+      '#                            || / _--=> preempt-depth',
+      '#                            ||| /     delay',
+      '#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION',
+      '#              | |       |   ||||       |         |',
+      '     systrace.sh-5441  [001] d...  1031.091570: sched_wakeup: ' +
+          'comm=debugd pid=4978 prio=120 success=1 target_cpu=000',
+      '     systrace.sh-5441  [001] d...  1031.091584: sched_switch: ' +
+          'prev_comm=systrace.sh prev_pid=5441 prev_prio=120 prev_state=x ' +
+          '==> next_comm=chrome next_pid=5418 next_prio=120'
+    ];
+    assert.isTrue(LinuxPerfImporter.canImport(lines.join('\n')));
+
+    var lines = [
+      '     systrace.sh-5441  [001] d...  1031.091570: sched_wakeup: ' +
+          'comm=debugd pid=4978 prio=120 success=1 target_cpu=000',
+      '     systrace.sh-5441  [001] d...  1031.091584: sched_switch: ' +
+          'prev_comm=systrace.sh prev_pid=5441 prev_prio=120 prev_state=x ' +
+          '==> next_comm=chrome next_pid=5418 next_prio=120'
+    ];
+    assert.isTrue(LinuxPerfImporter.canImport(lines.join('\n')));
+  });
+
+  test('importOneSequence', function() {
+    var lines = [
+      '          <idle>-0     [001]  4467.843475: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+          'next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+
+      '  SurfaceFlinger-178   [001]  4467.843536: sched_switch: ' +
+          'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+          'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+          'next_prio=120',
+
+      '     kworker/u:2-2844  [001]  4467.843567: sched_switch: ' +
+          'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+          'prev_state=S ==> next_comm=swapper next_pid=0 next_prio=120'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c = m.kernel.cpus[1];
+    assert.equal(c.slices.length, 2);
+
+    assert.equal(c.slices[0].title, 'SurfaceFlinger');
+    assert.equal(c.slices[0].start, 4467843.475);
+    assert.closeTo(.536 - .475, c.slices[0].duration, 1e-5);
+  });
+
+  test('importOneSequenceWithSpacyThreadName', function() {
+    var lines = [
+      '          <idle>-0     [001]  4467.843475: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ==> ' +
+          'next_comm=Surface Flinger  next_pid=178 next_prio=112',
+
+      'Surface Flinger -178   [001]  4467.843536: sched_switch: ' +
+          'prev_comm=Surface Flinger  prev_pid=178 prev_prio=112 ' +
+          'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+          'next_prio=120',
+
+      '     kworker/u:2-2844  [001]  4467.843567: sched_switch: ' +
+          'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+          'prev_state=S ==> next_comm=swapper next_pid=0 next_prio=120'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c = m.kernel.cpus[1];
+    assert.equal(c.slices.length, 2);
+
+    assert.equal(c.slices[0].title, 'Surface Flinger ');
+    assert.equal(c.slices[0].start, 4467843.475);
+    assert.closeTo(.536 - .475, c.slices[0].duration, 1e-5);
+  });
+
+  test('importWithNewline', function() {
+    var lines = [
+      ''
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'));
+    assert.isFalse(m.hasImportWarnings);
+  });
+
+  test('importHtml', function() {
+    var p = tv.b.getAsync(
+        '/extras/importer/linux_perf/linux_perf_importer_test_data.html');
+    return p.then(function(data) {
+      var m = new tv.c.TraceModel(data, false);
+      assert.isFalse(m.hasImportWarnings);
+
+      assert.isDefined(m.processes[124]);
+      assert.isDefined(m.processes[360]);
+
+      assert.isDefined(m.processes[124].counters['android.StatusBar']);
+      assert.equal(m.processes[124].counters['android.StatusBar'].numSamples,
+                   1);
+      assert.isDefined(m.processes[124].counters['android.VSYNC']);
+      assert.equal(2, m.processes[124].counters['android.VSYNC'].numSamples);
+      assert.isDefined(m.processes[360].counters['android.iq']);
+      assert.equal(1, m.processes[360].counters['android.iq'].numSamples);
+    }, function(err) {
+      throw err;
+    });
+  });
+
+  test('clockSync', function() {
+    var lines = [
+      '          <idle>-0     [001]  4467.843475: sched_switch: ' +
+          'prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ' +
+          '==> next_comm=SurfaceFlinger next_pid=178 next_prio=112',
+      '  SurfaceFlinger-178   [001]  4467.843536: sched_switch: ' +
+          'prev_comm=SurfaceFlinger prev_pid=178 prev_prio=112 ' +
+          'prev_state=S ==> next_comm=kworker/u:2 next_pid=2844 ' +
+          'next_prio=120',
+      '     kworker/u:2-2844  [001]  4467.843567: sched_switch: ' +
+          'prev_comm=kworker/u:2 prev_pid=2844 prev_prio=120 ' +
+          'prev_state=S ==> next_comm=swapper next_pid=0 ' +
+          'next_prio=120',
+      '     kworker/u:2-2844  [001]  4467.843000: 0: ' +
+          'trace_event_clock_sync: parent_ts=0.1'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c = m.kernel.cpus[1];
+    assert.equal(c.slices.length, 2);
+
+    assert.closeTo(
+        (467.843475 - (467.843 - 0.1)) * 1000,
+        c.slices[0].start,
+        1e-5);
+  });
+
+  test('clockSyncMarkWrite', function() {
+    var lines = [
+      'systrace.sh-8170  [001] 15180.978813: sched_switch: ' +
+          'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+          'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+          'next_prio=120',
+      ' kworker/1:0-7873  [001] 15180.978836: sched_switch: ' +
+          'prev_comm=kworker/1:0 prev_pid=7873 prev_prio=120 ' +
+          'prev_state=S ==> next_comm=debugd next_pid=4404 next_prio=120',
+      '     debugd-4404  [001] 15180.979010: sched_switch: prev_comm=debugd ' +
+          'prev_pid=4404 prev_prio=120 prev_state=S ==> ' +
+          'next_comm=dbus-daemon next_pid=510 next_prio=120',
+      'systrace.sh-8182  [000] 15186.203900: tracing_mark_write: ' +
+          'trace_event_clock_sync: parent_ts=0'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c = m.kernel.cpus[1];
+    assert.equal(c.slices.length, 2);
+
+    assert.closeTo((15180.978813 - 0) * 1000, c.slices[0].start, 1e-5);
+  });
+
+  test('tracingMarkWriteEOLCleanup', function() {
+    var lines = [
+      'systrace.sh-8182  [001] ...1 2068001.677892: tracing_mark_write: ' +
+          'B|9304|test\\n\\',
+      'systrace.sh-8182  [002] ...1 2068991.686415: tracing_mark_write: E\\n\\'
+    ];
+
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c = m.processes[9304].threads[8182].sliceGroup;
+    assert.equal(c.slices.length, 1);
+
+    assert.closeTo((2068001.677892 - 0) * 1000, c.slices[0].start, 1e-5);
+    assert.closeTo(
+        (2068991.686415 - 2068001.677892) * 1000,
+        c.slices[0].duration,
+        1e-5);
+  });
+
+  test('cpuCount', function() {
+    var lines = [
+      'systrace.sh-8170  [001] 15180.978813: sched_switch: ' +
+          'prev_comm=systrace.sh prev_pid=8170 prev_prio=120 ' +
+          'prev_state=x ==> next_comm=kworker/1:0 next_pid=7873 ' +
+          'next_prio=120',
+      ' kworker/1:0-7873  [001] 15180.978836: sched_switch: ' +
+          'prev_comm=kworker/1:0 prev_pid=7873 prev_prio=120 ' +
+          'prev_state=S ==> next_comm=debugd next_pid=4404 next_prio=120',
+      '     debugd-4404  [000] 15180.979010: sched_switch: prev_comm=debugd ' +
+          'prev_pid=4404 prev_prio=120 prev_state=S ==> ' +
+          'next_comm=dbus-daemon next_pid=510 next_prio=120'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    assert.equal(tv.b.dictionaryLength(m.kernel.cpus), 2);
+    assert.equal(m.kernel.bestGuessAtCpuCount, 2);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer_test_data.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer_test_data.html
new file mode 100644
index 0000000..0234c26
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/linux_perf_importer_test_data.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head i18n-values="dir:textdirection;">
+<title>Android System Trace</title>
+<style type="text/css">tabbox{display:-webkit-box;}</style>
+<script language="javascript">'use strict';function onLoad(){};
+document.addEventListener("click",function(g){});
+</script>
+<style>
+  .view {
+    overflow: hidden;
+  }
+</style>
+</head>
+<body>
+  <div class="view">
+  </div>
+  <script>
+  'use strict';
+  var linuxPerfData = "\
+# tracer: nop\n\
+#\n\
+#            TASK-PID    CPU#    TIMESTAMP  FUNCTION\n\
+#               | |       |          |         |\n\
+     hwc_eventmon-336   [000] 50260.929925: 0: C|124|VSYNC|1\n\
+         Binder_1-340   [000] 50260.935656: 0: C|124|StatusBar|1\n\
+     hwc_eventmon-336   [000] 50260.946573: 0: C|124|VSYNC|0\n\
+      InputReader-419   [000] 50262.538578: 0: C|360|iq|1\n";
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/mali_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/mali_parser.html
new file mode 100644
index 0000000..23c0d24
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/mali_parser.html
@@ -0,0 +1,567 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses Mali DDK/kernel events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses Mali DDK/kernel trace events.
+   * @constructor
+   */
+  function MaliParser(importer) {
+    Parser.call(this, importer);
+
+    // kernel DVFS events
+    importer.registerEventHandler('mali_dvfs_event',
+        MaliParser.prototype.dvfsEventEvent.bind(this));
+    importer.registerEventHandler('mali_dvfs_set_clock',
+        MaliParser.prototype.dvfsSetClockEvent.bind(this));
+    importer.registerEventHandler('mali_dvfs_set_voltage',
+        MaliParser.prototype.dvfsSetVoltageEvent.bind(this));
+
+    // kernel Mali hw counter events
+    this.addJMCounter('mali_hwc_MESSAGES_SENT', 'Messages Sent');
+    this.addJMCounter('mali_hwc_MESSAGES_RECEIVED', 'Messages Received');
+    this.addJMCycles('mali_hwc_GPU_ACTIVE', 'GPU Active');
+    this.addJMCycles('mali_hwc_IRQ_ACTIVE', 'IRQ Active');
+
+    for (var i = 0; i < 7; i++) {
+      var jobStr = 'JS' + i;
+      var jobHWCStr = 'mali_hwc_' + jobStr;
+      this.addJMCounter(jobHWCStr + '_JOBS', jobStr + ' Jobs');
+      this.addJMCounter(jobHWCStr + '_TASKS', jobStr + ' Tasks');
+      this.addJMCycles(jobHWCStr + '_ACTIVE', jobStr + ' Active');
+      this.addJMCycles(jobHWCStr + '_WAIT_READ', jobStr + ' Wait Read');
+      this.addJMCycles(jobHWCStr + '_WAIT_ISSUE', jobStr + ' Wait Issue');
+      this.addJMCycles(jobHWCStr + '_WAIT_DEPEND', jobStr + ' Wait Depend');
+      this.addJMCycles(jobHWCStr + '_WAIT_FINISH', jobStr + ' Wait Finish');
+    }
+
+    this.addTilerCounter('mali_hwc_TRIANGLES', 'Triangles');
+    this.addTilerCounter('mali_hwc_QUADS', 'Quads');
+    this.addTilerCounter('mali_hwc_POLYGONS', 'Polygons');
+    this.addTilerCounter('mali_hwc_POINTS', 'Points');
+    this.addTilerCounter('mali_hwc_LINES', 'Lines');
+    this.addTilerCounter('mali_hwc_VCACHE_HIT', 'VCache Hit');
+    this.addTilerCounter('mali_hwc_VCACHE_MISS', 'VCache Miss');
+    this.addTilerCounter('mali_hwc_FRONT_FACING', 'Front Facing');
+    this.addTilerCounter('mali_hwc_BACK_FACING', 'Back Facing');
+    this.addTilerCounter('mali_hwc_PRIM_VISIBLE', 'Prim Visible');
+    this.addTilerCounter('mali_hwc_PRIM_CULLED', 'Prim Culled');
+    this.addTilerCounter('mali_hwc_PRIM_CLIPPED', 'Prim Clipped');
+
+    this.addTilerCounter('mali_hwc_WRBUF_HIT', 'Wrbuf Hit');
+    this.addTilerCounter('mali_hwc_WRBUF_MISS', 'Wrbuf Miss');
+    this.addTilerCounter('mali_hwc_WRBUF_LINE', 'Wrbuf Line');
+    this.addTilerCounter('mali_hwc_WRBUF_PARTIAL', 'Wrbuf Partial');
+    this.addTilerCounter('mali_hwc_WRBUF_STALL', 'Wrbuf Stall');
+
+    this.addTilerCycles('mali_hwc_ACTIVE', 'Tiler Active');
+    this.addTilerCycles('mali_hwc_INDEX_WAIT', 'Index Wait');
+    this.addTilerCycles('mali_hwc_INDEX_RANGE_WAIT', 'Index Range Wait');
+    this.addTilerCycles('mali_hwc_VERTEX_WAIT', 'Vertex Wait');
+    this.addTilerCycles('mali_hwc_PCACHE_WAIT', 'Pcache Wait');
+    this.addTilerCycles('mali_hwc_WRBUF_WAIT', 'Wrbuf Wait');
+    this.addTilerCycles('mali_hwc_BUS_READ', 'Bus Read');
+    this.addTilerCycles('mali_hwc_BUS_WRITE', 'Bus Write');
+
+    this.addTilerCycles('mali_hwc_TILER_UTLB_STALL', 'Tiler UTLB Stall');
+    this.addTilerCycles('mali_hwc_TILER_UTLB_HIT', 'Tiler UTLB Hit');
+
+    this.addFragCycles('mali_hwc_FRAG_ACTIVE', 'Active');
+    /* NB: don't propagate spelling mistakes to labels */
+    this.addFragCounter('mali_hwc_FRAG_PRIMATIVES', 'Primitives');
+    this.addFragCounter('mali_hwc_FRAG_PRIMATIVES_DROPPED',
+        'Primitives Dropped');
+    this.addFragCycles('mali_hwc_FRAG_CYCLE_DESC', 'Descriptor Processing');
+    this.addFragCycles('mali_hwc_FRAG_CYCLES_PLR', 'PLR Processing??');
+    this.addFragCycles('mali_hwc_FRAG_CYCLES_VERT', 'Vertex Processing');
+    this.addFragCycles('mali_hwc_FRAG_CYCLES_TRISETUP', 'Triangle Setup');
+    this.addFragCycles('mali_hwc_FRAG_CYCLES_RAST', 'Rasterization???');
+    this.addFragCounter('mali_hwc_FRAG_THREADS', 'Threads');
+    this.addFragCounter('mali_hwc_FRAG_DUMMY_THREADS', 'Dummy Threads');
+    this.addFragCounter('mali_hwc_FRAG_QUADS_RAST', 'Quads Rast');
+    this.addFragCounter('mali_hwc_FRAG_QUADS_EZS_TEST', 'Quads EZS Test');
+    this.addFragCounter('mali_hwc_FRAG_QUADS_EZS_KILLED', 'Quads EZS Killed');
+    this.addFragCounter('mali_hwc_FRAG_QUADS_LZS_TEST', 'Quads LZS Test');
+    this.addFragCounter('mali_hwc_FRAG_QUADS_LZS_KILLED', 'Quads LZS Killed');
+    this.addFragCycles('mali_hwc_FRAG_CYCLE_NO_TILE', 'No Tiles');
+    this.addFragCounter('mali_hwc_FRAG_NUM_TILES', 'Tiles');
+    this.addFragCounter('mali_hwc_FRAG_TRANS_ELIM', 'Transactions Eliminated');
+
+    this.addComputeCycles('mali_hwc_COMPUTE_ACTIVE', 'Active');
+    this.addComputeCounter('mali_hwc_COMPUTE_TASKS', 'Tasks');
+    this.addComputeCounter('mali_hwc_COMPUTE_THREADS', 'Threads Started');
+    this.addComputeCycles('mali_hwc_COMPUTE_CYCLES_DESC',
+        'Waiting for Descriptors');
+
+    this.addTripipeCycles('mali_hwc_TRIPIPE_ACTIVE', 'Active');
+
+    this.addArithCounter('mali_hwc_ARITH_WORDS', 'Instructions (/Pipes)');
+    this.addArithCycles('mali_hwc_ARITH_CYCLES_REG',
+        'Reg scheduling stalls (/Pipes)');
+    this.addArithCycles('mali_hwc_ARITH_CYCLES_L0',
+        'L0 cache miss stalls (/Pipes)');
+    this.addArithCounter('mali_hwc_ARITH_FRAG_DEPEND',
+        'Frag dep check failures (/Pipes)');
+
+    this.addLSCounter('mali_hwc_LS_WORDS', 'Instruction Words Completed');
+    this.addLSCounter('mali_hwc_LS_ISSUES', 'Full Pipeline Issues');
+    this.addLSCounter('mali_hwc_LS_RESTARTS', 'Restarts (unpairable insts)');
+    this.addLSCounter('mali_hwc_LS_REISSUES_MISS',
+        'Pipeline reissue (cache miss/uTLB)');
+    this.addLSCounter('mali_hwc_LS_REISSUES_VD',
+        'Pipeline reissue (varying data)');
+    /* TODO(sleffler) fix kernel event typo */
+    this.addLSCounter('mali_hwc_LS_REISSUE_ATTRIB_MISS',
+        'Pipeline reissue (attribute cache miss)');
+    this.addLSCounter('mali_hwc_LS_REISSUE_NO_WB', 'Writeback not used');
+
+    this.addTexCounter('mali_hwc_TEX_WORDS', 'Words');
+    this.addTexCounter('mali_hwc_TEX_BUBBLES', 'Bubbles');
+    this.addTexCounter('mali_hwc_TEX_WORDS_L0', 'Words L0');
+    this.addTexCounter('mali_hwc_TEX_WORDS_DESC', 'Words Desc');
+    this.addTexCounter('mali_hwc_TEX_THREADS', 'Threads');
+    this.addTexCounter('mali_hwc_TEX_RECIRC_FMISS', 'Recirc due to Full Miss');
+    this.addTexCounter('mali_hwc_TEX_RECIRC_DESC', 'Recirc due to Desc Miss');
+    this.addTexCounter('mali_hwc_TEX_RECIRC_MULTI', 'Recirc due to Multipass');
+    this.addTexCounter('mali_hwc_TEX_RECIRC_PMISS',
+        'Recirc due to Partial Cache Miss');
+    this.addTexCounter('mali_hwc_TEX_RECIRC_CONF',
+        'Recirc due to Cache Conflict');
+
+    this.addLSCCounter('mali_hwc_LSC_READ_HITS', 'Read Hits');
+    this.addLSCCounter('mali_hwc_LSC_READ_MISSES', 'Read Misses');
+    this.addLSCCounter('mali_hwc_LSC_WRITE_HITS', 'Write Hits');
+    this.addLSCCounter('mali_hwc_LSC_WRITE_MISSES', 'Write Misses');
+    this.addLSCCounter('mali_hwc_LSC_ATOMIC_HITS', 'Atomic Hits');
+    this.addLSCCounter('mali_hwc_LSC_ATOMIC_MISSES', 'Atomic Misses');
+    this.addLSCCounter('mali_hwc_LSC_LINE_FETCHES', 'Line Fetches');
+    this.addLSCCounter('mali_hwc_LSC_DIRTY_LINE', 'Dirty Lines');
+    this.addLSCCounter('mali_hwc_LSC_SNOOPS', 'Snoops');
+
+    this.addAXICounter('mali_hwc_AXI_TLB_STALL', 'Address channel stall');
+    this.addAXICounter('mali_hwc_AXI_TLB_MISS', 'Cache Miss');
+    this.addAXICounter('mali_hwc_AXI_TLB_TRANSACTION', 'Transactions');
+    this.addAXICounter('mali_hwc_LS_TLB_MISS', 'LS Cache Miss');
+    this.addAXICounter('mali_hwc_LS_TLB_HIT', 'LS Cache Hit');
+    this.addAXICounter('mali_hwc_AXI_BEATS_READ', 'Read Beats');
+    this.addAXICounter('mali_hwc_AXI_BEATS_WRITE', 'Write Beats');
+
+    this.addMMUCounter('mali_hwc_MMU_TABLE_WALK', 'Page Table Walks');
+    this.addMMUCounter('mali_hwc_MMU_REPLAY_MISS',
+        'Cache Miss from Replay Buffer');
+    this.addMMUCounter('mali_hwc_MMU_REPLAY_FULL', 'Replay Buffer Full');
+    this.addMMUCounter('mali_hwc_MMU_NEW_MISS', 'Cache Miss on New Request');
+    this.addMMUCounter('mali_hwc_MMU_HIT', 'Cache Hit');
+
+    this.addMMUCycles('mali_hwc_UTLB_STALL', 'UTLB Stalled');
+    this.addMMUCycles('mali_hwc_UTLB_REPLAY_MISS', 'UTLB Replay Miss');
+    this.addMMUCycles('mali_hwc_UTLB_REPLAY_FULL', 'UTLB Replay Full');
+    this.addMMUCycles('mali_hwc_UTLB_NEW_MISS', 'UTLB New Miss');
+    this.addMMUCycles('mali_hwc_UTLB_HIT', 'UTLB Hit');
+
+    this.addL2Counter('mali_hwc_L2_READ_BEATS', 'Read Beats');
+    this.addL2Counter('mali_hwc_L2_WRITE_BEATS', 'Write Beats');
+    this.addL2Counter('mali_hwc_L2_ANY_LOOKUP', 'Any Lookup');
+    this.addL2Counter('mali_hwc_L2_READ_LOOKUP', 'Read Lookup');
+    this.addL2Counter('mali_hwc_L2_SREAD_LOOKUP', 'Shareable Read Lookup');
+    this.addL2Counter('mali_hwc_L2_READ_REPLAY', 'Read Replayed');
+    this.addL2Counter('mali_hwc_L2_READ_SNOOP', 'Read Snoop');
+    this.addL2Counter('mali_hwc_L2_READ_HIT', 'Read Cache Hit');
+    this.addL2Counter('mali_hwc_L2_CLEAN_MISS', 'CleanUnique Miss');
+    this.addL2Counter('mali_hwc_L2_WRITE_LOOKUP', 'Write Lookup');
+    this.addL2Counter('mali_hwc_L2_SWRITE_LOOKUP', 'Shareable Write Lookup');
+    this.addL2Counter('mali_hwc_L2_WRITE_REPLAY', 'Write Replayed');
+    this.addL2Counter('mali_hwc_L2_WRITE_SNOOP', 'Write Snoop');
+    this.addL2Counter('mali_hwc_L2_WRITE_HIT', 'Write Cache Hit');
+    this.addL2Counter('mali_hwc_L2_EXT_READ_FULL', 'ExtRD with BIU Full');
+    this.addL2Counter('mali_hwc_L2_EXT_READ_HALF', 'ExtRD with BIU >1/2 Full');
+    this.addL2Counter('mali_hwc_L2_EXT_WRITE_FULL', 'ExtWR with BIU Full');
+    this.addL2Counter('mali_hwc_L2_EXT_WRITE_HALF', 'ExtWR with BIU >1/2 Full');
+
+    this.addL2Counter('mali_hwc_L2_EXT_READ', 'External Read (ExtRD)');
+    this.addL2Counter('mali_hwc_L2_EXT_READ_LINE', 'ExtRD (linefill)');
+    this.addL2Counter('mali_hwc_L2_EXT_WRITE', 'External Write (ExtWR)');
+    this.addL2Counter('mali_hwc_L2_EXT_WRITE_LINE', 'ExtWR (linefill)');
+    this.addL2Counter('mali_hwc_L2_EXT_WRITE_SMALL', 'ExtWR (burst size <64B)');
+    this.addL2Counter('mali_hwc_L2_EXT_BARRIER', 'External Barrier');
+    this.addL2Counter('mali_hwc_L2_EXT_AR_STALL', 'Address Read stalls');
+    this.addL2Counter('mali_hwc_L2_EXT_R_BUF_FULL',
+        'Response Buffer full stalls');
+    this.addL2Counter('mali_hwc_L2_EXT_RD_BUF_FULL',
+        'Read Data Buffer full stalls');
+    this.addL2Counter('mali_hwc_L2_EXT_R_RAW', 'RAW hazard stalls');
+    this.addL2Counter('mali_hwc_L2_EXT_W_STALL', 'Write Data stalls');
+    this.addL2Counter('mali_hwc_L2_EXT_W_BUF_FULL', 'Write Data Buffer full');
+    this.addL2Counter('mali_hwc_L2_EXT_R_W_HAZARD', 'WAW or WAR hazard stalls');
+    this.addL2Counter('mali_hwc_L2_TAG_HAZARD', 'Tag hazard replays');
+    this.addL2Cycles('mali_hwc_L2_SNOOP_FULL', 'Snoop buffer full');
+    this.addL2Cycles('mali_hwc_L2_REPLAY_FULL', 'Replay buffer full');
+
+    // DDK events (from X server)
+    importer.registerEventHandler('tracing_mark_write:mali_driver',
+        MaliParser.prototype.maliDDKEvent.bind(this));
+
+    this.model_ = importer.model_;
+  }
+
+  MaliParser.prototype = {
+    __proto__: Parser.prototype,
+
+    maliDDKOpenSlice: function(pid, tid, ts, func, blockinfo) {
+      var thread = this.importer.model_.getOrCreateProcess(pid)
+        .getOrCreateThread(tid);
+      var funcArgs = /^([\w\d_]*)(?:\(\))?:?\s*(.*)$/.exec(func);
+      thread.sliceGroup.beginSlice('gpu-driver', funcArgs[1], ts,
+          { 'args': funcArgs[2],
+            'blockinfo': blockinfo });
+    },
+
+    maliDDKCloseSlice: function(pid, tid, ts, args, blockinfo) {
+      var thread = this.importer.model_.getOrCreateProcess(pid)
+        .getOrCreateThread(tid);
+      if (!thread.sliceGroup.openSliceCount) {
+        // Discard unmatched ends.
+        return;
+      }
+      thread.sliceGroup.endSlice(ts);
+    },
+
+    /**
+     * Deduce the format of Mali perf events.
+     *
+     * @return {RegExp} the regular expression for parsing data when the format
+     * is recognized; otherwise null.
+     */
+    autoDetectLineRE: function(line) {
+      // Matches Mali perf events with thread info
+      var lineREWithThread =
+          /^\s*\(([\w\-]*)\)\s*(\w+):\s*([\w\\\/\.\-]*@\d*):?\s*(.*)$/;
+      if (lineREWithThread.test(line))
+        return lineREWithThread;
+
+      // Matches old-style Mali perf events
+      var lineRENoThread = /^s*()(\w+):\s*([\w\\\/.\-]*):?\s*(.*)$/;
+      if (lineRENoThread.test(line))
+        return lineRENoThread;
+      return null;
+    },
+
+    lineRE: null,
+
+    /**
+     * Parses maliDDK events and sets up state in the importer.
+     * events will come in pairs with a cros_trace_print_enter
+     * like this (line broken here for formatting):
+     *
+     * tracing_mark_write: mali_driver: (mali-012345) cros_trace_print_enter: \
+     *   gles/src/texture/mali_gles_texture_slave.c@1505: gles2_texturep_upload
+     *
+     * and a cros_trace_print_exit like this:
+     *
+     * tracing_mark_write: mali_driver: (mali-012345) cros_trace_print_exit: \
+     *   gles/src/texture/mali_gles_texture_slave.c@1505:
+     */
+    maliDDKEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      if (this.lineRE == null) {
+        this.lineRE = this.autoDetectLineRE(eventBase.details);
+        if (this.lineRE == null)
+          return false;
+      }
+      var maliEvent = this.lineRE.exec(eventBase.details);
+      // Old-style Mali perf events have no thread id, so make one.
+      var tid = (maliEvent[1] === '' ? 'mali' : maliEvent[1]);
+      switch (maliEvent[2]) {
+        case 'cros_trace_print_enter':
+          this.maliDDKOpenSlice(pid, tid, ts, maliEvent[4],
+              maliEvent[3]);
+          break;
+        case 'cros_trace_print_exit':
+          this.maliDDKCloseSlice(pid, tid, ts, [], maliEvent[3]);
+      }
+      return true;
+    },
+
+    /*
+     * Kernel event support.
+     */
+
+    dvfsSample: function(counterName, seriesName, ts, s) {
+      var value = parseInt(s);
+      var counter = this.model_.kernel.
+          getOrCreateCounter('DVFS', counterName);
+      if (counter.numSeries === 0) {
+        counter.addSeries(new tv.c.trace_model.CounterSeries(seriesName,
+            tv.b.ui.getColorIdForGeneralPurposeString(counter.name)));
+      }
+      counter.series.forEach(function(series) {
+        series.addCounterSample(ts, value);
+      });
+    },
+
+    dvfsEventEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /utilization=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      this.dvfsSample('DVFS Utilization', 'utilization', ts, event[1]);
+      return true;
+    },
+
+    dvfsSetClockEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /frequency=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      this.dvfsSample('DVFS Frequency', 'frequency', ts, event[1]);
+      return true;
+    },
+
+    dvfsSetVoltageEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /voltage=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      this.dvfsSample('DVFS Voltage', 'voltage', ts, event[1]);
+      return true;
+    },
+
+    hwcSample: function(cat, counterName, seriesName, ts, eventBase) {
+      var event = /val=(\d+)/.exec(eventBase.details);
+      if (!event)
+        return false;
+      var value = parseInt(event[1]);
+
+      var counter = this.model_.kernel.
+          getOrCreateCounter(cat, counterName);
+      if (counter.numSeries === 0) {
+        counter.addSeries(new tv.c.trace_model.CounterSeries(seriesName,
+            tv.b.ui.getColorIdForGeneralPurposeString(counter.name)));
+      }
+      counter.series.forEach(function(series) {
+        series.addCounterSample(ts, value);
+      });
+      return true;
+    },
+
+    /*
+     * Job Manager block counters.
+     */
+    jmSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:jm', 'JM: ' + ctrName, seriesName, ts,
+          eventBase);
+    },
+    addJMCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.jmSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addJMCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.jmSample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Tiler block counters.
+     */
+    tilerSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:tiler', 'Tiler: ' + ctrName, seriesName,
+          ts, eventBase);
+    },
+    addTilerCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.tilerSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addTilerCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.tilerSample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Fragment counters.
+     */
+    fragSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:fragment', 'Fragment: ' + ctrName,
+          seriesName, ts, eventBase);
+    },
+    addFragCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.fragSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addFragCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.fragSample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Compute counters.
+     */
+    computeSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:compute', 'Compute: ' + ctrName,
+          seriesName, ts, eventBase);
+    },
+    addComputeCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.computeSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addComputeCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.computeSample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Tripipe counters.
+     */
+    addTripipeCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.hwcSample('mali:shader', 'Tripipe: ' + hwcTitle, 'cycles',
+            ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Arith counters.
+     */
+    arithSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:arith', 'Arith: ' + ctrName, seriesName, ts,
+          eventBase);
+    },
+    addArithCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.arithSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addArithCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.arithSample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Load/Store counters.
+     */
+    addLSCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.hwcSample('mali:ls', 'LS: ' + hwcTitle, 'count', ts,
+            eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * Texture counters.
+     */
+    textureSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:texture', 'Texture: ' + ctrName,
+          seriesName, ts, eventBase);
+    },
+    addTexCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.textureSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * LSC counters.
+     */
+    addLSCCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.hwcSample('mali:lsc', 'LSC: ' + hwcTitle, 'count', ts,
+            eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * TLB counters.
+     */
+    addAXICounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.hwcSample('mali:axi', 'AXI: ' + hwcTitle, 'count', ts,
+            eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * MMU counters.
+     */
+    mmuSample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:mmu', 'MMU: ' + ctrName, seriesName, ts,
+          eventBase);
+    },
+    addMMUCounter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.mmuSample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addMMUCycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.mmuSample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+
+    /*
+     * L2 counters.
+     */
+    l2Sample: function(ctrName, seriesName, ts, eventBase) {
+      return this.hwcSample('mali:l2', 'L2: ' + ctrName, seriesName, ts,
+          eventBase);
+    },
+    addL2Counter: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.l2Sample(hwcTitle, 'count', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    },
+    addL2Cycles: function(hwcEventName, hwcTitle) {
+      function handler(eventName, cpuNumber, pid, ts, eventBase) {
+        return this.l2Sample(hwcTitle, 'cycles', ts, eventBase);
+      }
+      this.importer.registerEventHandler(hwcEventName, handler.bind(this));
+    }
+  };
+
+  Parser.register(MaliParser);
+
+  return {
+    MaliParser: MaliParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/mali_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/mali_parser_test.html
new file mode 100644
index 0000000..b58ecbd
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/mali_parser_test.html
@@ -0,0 +1,481 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('maliDDKImport', function() {
+    var linesNoThread = [
+      // Row 1 open
+      '           chrome-1780  [001] ...1   28.562633: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'gles/src/dispatch/mali_gles_dispatch_entrypoints.c992: ' +
+          'glTexSubImage2D',
+      // Row 2 open
+      '           chrome-1780  [001] ...1   28.562655: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_api.c996: ' +
+          'gles_texture_tex_sub_image_2d',
+      // Row 3 open
+      '            chrome-1780  [001] ...1   28.562671: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_slave.c295: ' +
+          'gles_texturep_slave_map_master',
+      // Row 3 close
+      '           chrome-1780  [001] ...1   28.562684: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_slave.c295: ',
+      // Row 3 open
+      '           chrome-1780  [001] ...1   28.562700: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_slave.c1505: ' +
+          'gles2_texturep_upload_2d',
+      // Row 4 open
+      '           chrome-1780  [001] ...1   28.562726: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_slave.c1612: ' +
+          'gles2_texturep_upload_2d: pixel array: wait for dependencies',
+      // Row 5 open
+      '           chrome-1780  [001] ...1   28.562742: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c1693: ' +
+          'cobj_convert_pixels_to_surface',
+      // Row 6 open
+      '           chrome-1780  [001] ...1   28.562776: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c1461: ' +
+          'cobj_convert_pixels',
+      // Row 7 open
+      '           chrome-1780  [001] ...1   28.562791: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c1505: ' +
+          'cobj_convert_pixels: fast-path linear copy',
+      // Row 8 open
+      '           chrome-1780  [001] ...1   28.562808: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c1511: ' +
+          'cobj_convert_pixels: reorder-only',
+      // Row 8 close
+      '           chrome-1780  [001] ...1   28.563383: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c1511',
+      // Row 7 close
+      '           chrome-1780  [001] ...1   28.563397: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c1505',
+      // Row 6 close
+      '           chrome-1780  [001] ...1   28.563409: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c1461',
+      // Row 5 close
+      '           chrome-1780  [001] ...1   28.563438: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c1693',
+      // Row 4 close
+      '           chrome-1780  [001] ...1   28.563451: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_slave.c1612',
+      // Row 3 close
+      '           chrome-1780  [001] ...1   28.563462: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_slave.c1505',
+      // Row 2 close
+      '           chrome-1780  [001] ...1   28.563475: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_api.c996',
+      // Row 1 close
+      '           chrome-1780  [001] ...1   28.563486: tracing_mark_write: ' +
+          'mali_driver: cros_trace_print_exit: ' +
+          'gles/src/dispatch/mali_gles_dispatch_entrypoints.c992'
+    ];
+
+    var linesWithThread = [
+      // Row 1 open
+      '           chrome-1780  [001] ...1   28.562633: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'gles/src/dispatch/mali_gles_dispatch_entrypoints.c@992: ' +
+          'glTexSubImage2D',
+      // Row 2 open
+      '           chrome-1780  [001] ...1   28.562655: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_api.c@996: ' +
+          'gles_texture_tex_sub_image_2d',
+      // Row 3 open
+      '            chrome-1780  [001] ...1   28.562671: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_slave.c@295: ' +
+          'gles_texturep_slave_map_master',
+      // Row 3 close
+      '           chrome-1780  [001] ...1   28.562684: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_slave.c@295: ',
+      // Row 3 open
+      '           chrome-1780  [001] ...1   28.562700: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_slave.c@1505: ' +
+          'gles2_texturep_upload_2d',
+      // Row 4 open
+      '           chrome-1780  [001] ...1   28.562726: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'gles/src/texture/mali_gles_texture_slave.c@1612: ' +
+          'gles2_texturep_upload_2d: pixel array: wait for dependencies',
+      // Row 5 open
+      '           chrome-1780  [001] ...1   28.562742: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1693: ' +
+          'cobj_convert_pixels_to_surface',
+      // Row 6 open
+      '           chrome-1780  [001] ...1   28.562776: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1461: ' +
+          'cobj_convert_pixels',
+      // Row 7 open
+      '           chrome-1780  [001] ...1   28.562791: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1505: ' +
+          'cobj_convert_pixels: fast-path linear copy',
+      // Row 8 open
+      '           chrome-1780  [001] ...1   28.562808: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_enter: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1511: ' +
+          'cobj_convert_pixels: reorder-only',
+      // Row 8 close
+      '           chrome-1780  [001] ...1   28.563383: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1511',
+      // Row 7 close
+      '           chrome-1780  [001] ...1   28.563397: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1505',
+      // Row 6 close
+      '           chrome-1780  [001] ...1   28.563409: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1461',
+      // Row 5 close
+      '           chrome-1780  [001] ...1   28.563438: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'cobj/src/mali_cobj_surface_operations.c@1693',
+      // Row 4 close
+      '           chrome-1780  [001] ...1   28.563451: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_slave.c@1612',
+      // Row 3 close
+      '           chrome-1780  [001] ...1   28.563462: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_slave.c@1505',
+      // Row 2 close
+      '           chrome-1780  [001] ...1   28.563475: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'gles/src/texture/mali_gles_texture_api.c@996',
+      // Row 1 close
+      '           chrome-1780  [001] ...1   28.563486: tracing_mark_write: ' +
+          'mali_driver: (mali-1878934320) cros_trace_print_exit: ' +
+          'gles/src/dispatch/mali_gles_dispatch_entrypoints.c@992'
+    ];
+    var traceNoThread =
+        new tv.c.TraceModel(linesNoThread.join('\n'), false);
+    var traceWithThread =
+        new tv.c.TraceModel(linesWithThread.join('\n'), false);
+    assert.isFalse(traceNoThread.hasImportWarnings);
+    assert.isFalse(traceWithThread.hasImportWarnings);
+
+    var threadsNoThread = traceNoThread.getAllThreads();
+    var threadsWithThread = traceWithThread.getAllThreads();
+    assert.equal(threadsNoThread.length, 1);
+    assert.equal(threadsWithThread.length, 1);
+
+    var maliThreadNoThread = threadsNoThread[0];
+    var maliThreadWithThread = threadsWithThread[0];
+    assert.equal(maliThreadNoThread.tid, 'mali');
+    assert.equal(maliThreadWithThread.tid, 'mali-1878934320');
+    assert.equal(maliThreadNoThread.sliceGroup.length, 9);
+    assert.equal(maliThreadWithThread.sliceGroup.length, 9);
+  });
+
+  test('DVFSFrequencyImport', function() {
+    var lines = [
+      '     kworker/u:0-5     [001] ....  1174.839552: mali_dvfs_set_clock: ' +
+                     'frequency=266',
+      '     kworker/u:0-5     [000] ....  1183.840486: mali_dvfs_set_clock: ' +
+                     'frequency=400'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 1);
+
+    var c0 = counters[0];
+    assert.equal(c0.name, 'DVFS Frequency');
+    assert.equal(c0.series[0].samples.length, 2);
+  });
+
+  test('DVFSVoltageImport', function() {
+    var lines = [
+      '    kworker/u:0-5     [001] ....  1174.839562: mali_dvfs_set_voltage: ' +
+                     'voltage=937500',
+      '    kworker/u:0-5     [000] ....  1183.840009: mali_dvfs_set_voltage: ' +
+                     'voltage=1100000'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 1);
+
+    var c0 = counters[0];
+    assert.equal(c0.name, 'DVFS Voltage');
+    assert.equal(c0.series[0].samples.length, 2);
+  });
+
+  test('DVFSUtilizationImport', function() {
+    var lines = [
+      '     kworker/u:0-5     [001] ....  1174.839552: mali_dvfs_event: ' +
+                     'utilization=7',
+      '     kworker/u:0-5     [000] ....  1183.840486: mali_dvfs_event: ' +
+                     'utilization=37'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 1);
+
+    var c0 = counters[0];
+    assert.equal(c0.name, 'DVFS Utilization');
+    assert.equal(c0.series[0].samples.length, 2);
+  });
+
+  test('maliHWCImport', function() {
+    var lines = [
+      '     kworker/u:0-5     [000] ....    78.896588: ' +
+                     'mali_hwc_ACTIVE: val=238',
+      '     kworker/u:0-5     [000] ....    79.046889: ' +
+                     'mali_hwc_ARITH_CYCLES_L0: val=1967',
+      '     kworker/u:0-5     [000] ....    79.046888: ' +
+                     'mali_hwc_ARITH_CYCLES_REG: val=136',
+      '     kworker/u:0-5     [000] ....    79.046890: ' +
+                     'mali_hwc_ARITH_FRAG_DEPEND: val=19676',
+      '     kworker/u:0-5     [000] ....    79.046886: ' +
+                     'mali_hwc_ARITH_WORDS: val=255543',
+      '     kworker/u:0-5     [000] ....    79.046920: ' +
+                     'mali_hwc_AXI_BEATS_READ: val=257053',
+      '     kworker/u:0-5     [000] ....    78.896594: ' +
+                     'mali_hwc_AXI_TLB_STALL: val=1',
+      '     kworker/u:0-5     [000] ....    78.946646: ' +
+                     'mali_hwc_AXI_TLB_TRANSACTION: val=4',
+      '     kworker/u:0-5     [000] ....    79.046853: ' +
+                     'mali_hwc_BACK_FACING: val=104',
+      '     kworker/u:0-5     [000] ....    79.046880: ' +
+                     'mali_hwc_COMPUTE_ACTIVE: val=17462',
+      '     kworker/u:0-5     [000] ....    79.046884: ' +
+                     'mali_hwc_COMPUTE_CYCLES_DESC: val=3933',
+      '     kworker/u:0-5     [000] ....    79.046881: ' +
+                     'mali_hwc_COMPUTE_TASKS: val=15',
+      '     kworker/u:0-5     [000] ....    79.046883: ' +
+                     'mali_hwc_COMPUTE_THREADS: val=60',
+      '     kworker/u:0-5     [000] ....    79.046860: ' +
+                     'mali_hwc_FRAG_ACTIVE: val=690986',
+      '     kworker/u:0-5     [000] ....    79.046864: ' +
+                     'mali_hwc_FRAG_CYCLE_DESC: val=13980',
+      '     kworker/u:0-5     [000] ....    79.046876: ' +
+                     'mali_hwc_FRAG_CYCLE_NO_TILE: val=3539',
+      '     kworker/u:0-5     [000] ....    79.046865: ' +
+                     'mali_hwc_FRAG_CYCLES_PLR: val=1499',
+      '     kworker/u:0-5     [000] ....    79.046869: ' +
+                     'mali_hwc_FRAG_CYCLES_RAST: val=1999',
+      '     kworker/u:0-5     [000] ....    79.046868: ' +
+                     'mali_hwc_FRAG_CYCLES_TRISETUP: val=22353',
+      '     kworker/u:0-5     [000] ....    79.046867: ' +
+                     'mali_hwc_FRAG_CYCLES_VERT: val=20763',
+      '     kworker/u:0-5     [000] ....    79.046872: ' +
+                     'mali_hwc_FRAG_DUMMY_THREADS: val=1968',
+      '     kworker/u:0-5     [000] ....    79.046877: ' +
+                     'mali_hwc_FRAG_NUM_TILES: val=1840',
+      '     kworker/u:0-5     [000] ....    79.046862: ' +
+                     'mali_hwc_FRAG_PRIMATIVES: val=3752',
+      '     kworker/u:0-5     [000] ....    79.046863: ' +
+                     'mali_hwc_FRAG_PRIMATIVES_DROPPED: val=18',
+      '     kworker/u:0-5     [000] ....    79.046874: ' +
+                     'mali_hwc_FRAG_QUADS_EZS_TEST: val=117925',
+      '     kworker/u:0-5     [000] ....    79.046873: ' +
+                     'mali_hwc_FRAG_QUADS_RAST: val=117889',
+      '     kworker/u:0-5     [000] ....    79.046870: ' +
+                     'mali_hwc_FRAG_THREADS: val=471507',
+      '     kworker/u:0-5     [000] ....    79.046879: ' +
+                     'mali_hwc_FRAG_TRANS_ELIM: val=687',
+      '     kworker/u:0-5     [000] ....    80.315162: ' +
+                     'mali_hwc_FRONT_FACING: val=56',
+      '     kworker/u:0-5     [000] ....    78.896582: ' +
+                     'mali_hwc_GPU_ACTIVE: val=1316',
+      '     kworker/u:0-5     [000] ....    78.896584: ' +
+                     'mali_hwc_IRQ_ACTIVE: val=17',
+      '     kworker/u:0-5     [000] ....    79.046834: ' +
+                     'mali_hwc_JS0_ACTIVE: val=709444',
+      '     kworker/u:0-5     [000] ....    79.046831: ' +
+                     'mali_hwc_JS0_JOBS: val=2',
+      '     kworker/u:0-5     [000] ....    79.046832: ' +
+                     'mali_hwc_JS0_TASKS: val=7263',
+      '     kworker/u:0-5     [000] ....    79.046836: ' +
+                     'mali_hwc_JS0_WAIT_DEPEND: val=665876',
+      '     kworker/u:0-5     [000] ....    79.046835: ' +
+                     'mali_hwc_JS0_WAIT_ISSUE: val=910',
+      '     kworker/u:0-5     [000] ....    79.046840: ' +
+                     'mali_hwc_JS1_ACTIVE: val=153980',
+      '     kworker/u:0-5     [000] ....    79.046838: ' +
+                     'mali_hwc_JS1_JOBS: val=133',
+      '     kworker/u:0-5     [000] ....    79.046839: ' +
+                     'mali_hwc_JS1_TASKS: val=128',
+      '     kworker/u:0-5     [000] ....    79.046843: ' +
+                     'mali_hwc_JS1_WAIT_FINISH: val=74404',
+      '     kworker/u:0-5     [000] ....    79.046842: ' +
+                     'mali_hwc_JS1_WAIT_ISSUE: val=10146',
+      '     kworker/u:0-5     [000] ....    78.896603: ' +
+                     'mali_hwc_L2_ANY_LOOKUP: val=22',
+      '     kworker/u:0-5     [000] ....    79.046942: ' +
+                     'mali_hwc_L2_CLEAN_MISS: val=116',
+      '     kworker/u:0-5     [000] ....    79.063515: ' +
+                     'mali_hwc_L2_EXT_AR_STALL: val=9',
+      '     kworker/u:0-5     [000] ....    78.963384: ' +
+                     'mali_hwc_L2_EXT_BARRIER: val=1',
+      '     kworker/u:0-5     [000] ....    79.063516: ' +
+                     'mali_hwc_L2_EXT_R_BUF_FULL: val=43',
+      '     kworker/u:0-5     [000] ....    78.896611: ' +
+                     'mali_hwc_L2_EXT_READ: val=4',
+      '     kworker/u:0-5     [000] ....    78.896612: ' +
+                     'mali_hwc_L2_EXT_READ_LINE: val=4',
+      '     kworker/u:0-5     [000] ....    79.046956: ' +
+                     'mali_hwc_L2_EXT_R_RAW: val=1',
+      '     kworker/u:0-5     [000] ....    79.063518: ' +
+                     'mali_hwc_L2_EXT_R_W_HAZARD: val=15',
+      '     kworker/u:0-5     [000] ....    78.963381: ' +
+                     'mali_hwc_L2_EXT_WRITE: val=25',
+      '     kworker/u:0-5     [000] ....    79.046952: ' +
+                     'mali_hwc_L2_EXT_WRITE_LINE: val=63278',
+      '     kworker/u:0-5     [000] ....    78.963382: ' +
+                     'mali_hwc_L2_EXT_WRITE_SMALL: val=1',
+      '     kworker/u:0-5     [000] ....    79.814532: ' +
+                     'mali_hwc_L2_EXT_W_STALL: val=9',
+      '     kworker/u:0-5     [000] ....    78.896602: ' +
+                     'mali_hwc_L2_READ_BEATS: val=16',
+      '     kworker/u:0-5     [000] ....    78.896607: ' +
+                     'mali_hwc_L2_READ_HIT: val=11',
+      '     kworker/u:0-5     [000] ....    78.896604: ' +
+                     'mali_hwc_L2_READ_LOOKUP: val=19',
+      '     kworker/u:0-5     [000] ....    78.896606: ' +
+                     'mali_hwc_L2_READ_REPLAY: val=2',
+      '     kworker/u:0-5     [000] ....    79.046940: ' +
+                     'mali_hwc_L2_READ_SNOOP: val=24',
+      '     kworker/u:0-5     [000] ....    79.046959: ' +
+                     'mali_hwc_L2_REPLAY_FULL: val=6629',
+      '     kworker/u:0-5     [000] .N..    80.565684: ' +
+                     'mali_hwc_L2_SNOOP_FULL: val=5',
+      '     kworker/u:0-5     [000] ....    79.046937: ' +
+                     'mali_hwc_L2_SREAD_LOOKUP: val=241',
+      '     kworker/u:0-5     [000] ....    79.046944: ' +
+                     'mali_hwc_L2_SWRITE_LOOKUP: val=133',
+      '     kworker/u:0-5     [000] ....    78.896614: ' +
+                     'mali_hwc_L2_TAG_HAZARD: val=4',
+      '     kworker/u:0-5     [000] ....    78.963368: ' +
+                     'mali_hwc_L2_WRITE_BEATS: val=96',
+      '     kworker/u:0-5     [000] ....    79.046947: ' +
+                     'mali_hwc_L2_WRITE_HIT: val=78265',
+      '     kworker/u:0-5     [000] ....    78.896608: ' +
+                     'mali_hwc_L2_WRITE_LOOKUP: val=3',
+      '     kworker/u:0-5     [000] ....    79.046946: ' +
+                     'mali_hwc_L2_WRITE_REPLAY: val=15879',
+      '     kworker/u:0-5     [000] ....    79.046912: ' +
+                     'mali_hwc_LSC_LINE_FETCHES: val=15',
+      '     kworker/u:0-5     [000] ....    79.046909: ' +
+                     'mali_hwc_LSC_READ_HITS: val=2961',
+      '     kworker/u:0-5     [000] ....    79.046911: ' +
+                     'mali_hwc_LSC_READ_MISSES: val=22',
+      '     kworker/u:0-5     [000] ....    79.046914: ' +
+                     'mali_hwc_LSC_SNOOPS: val=10',
+      '     kworker/u:0-5     [000] ....    79.046893: ' +
+                     'mali_hwc_LS_ISSUES: val=524219',
+      '     kworker/u:0-5     [000] ....    79.046894: ' +
+                     'mali_hwc_LS_REISSUES_MISS: val=439',
+      '     kworker/u:0-5     [000] ....    79.046895: ' +
+                     'mali_hwc_LS_REISSUES_VD: val=52007',
+      '     kworker/u:0-5     [000] ....    79.046919: ' +
+                     'mali_hwc_LS_TLB_HIT: val=3043',
+      '     kworker/u:0-5     [000] ....    79.046918: ' +
+                     'mali_hwc_LS_TLB_MISS: val=5',
+      '     kworker/u:0-5     [000] ....    79.046891: ' +
+                     'mali_hwc_LS_WORDS: val=471514',
+      '     kworker/u:0-5     [000] ....    79.046925: ' +
+                     'mali_hwc_MMU_HIT: val=771',
+      '     kworker/u:0-5     [000] ....    79.046924: ' +
+                     'mali_hwc_MMU_NEW_MISS: val=494',
+      '     kworker/u:0-5     [000] ....    79.046922: ' +
+                     'mali_hwc_MMU_REPLAY_MISS: val=841',
+      '     kworker/u:0-5     [000] ....    79.046921: ' +
+                     'mali_hwc_MMU_TABLE_WALK: val=3119',
+      '     kworker/u:0-5     [000] ....    79.046848: ' +
+                     'mali_hwc_POINTS: val=5',
+      '     kworker/u:0-5     [000] ....    79.046856: ' +
+                     'mali_hwc_PRIM_CLIPPED: val=70',
+      '     kworker/u:0-5     [000] ....    79.046855: ' +
+                     'mali_hwc_PRIM_CULLED: val=26',
+      '     kworker/u:0-5     [000] ....    79.046854: ' +
+                     'mali_hwc_PRIM_VISIBLE: val=109',
+      '     kworker/u:0-5     [000] ....    79.046898: ' +
+                     'mali_hwc_TEX_BUBBLES: val=24874',
+      '     kworker/u:0-5     [000] ....    79.046905: ' +
+                     'mali_hwc_TEX_RECIRC_DESC: val=5937',
+      '     kworker/u:0-5     [000] ....    79.046904: ' +
+                     'mali_hwc_TEX_RECIRC_FMISS: val=209450',
+      '     kworker/u:0-5     [000] ....    78.896592: ' +
+                     'mali_hwc_TEX_RECIRC_MULTI: val=238',
+      '     kworker/u:0-5     [000] ....    79.046908: ' +
+                     'mali_hwc_TEX_RECIRC_PMISS: val=9672',
+      '     kworker/u:0-5     [000] ....    79.046903: ' +
+                     'mali_hwc_TEX_THREADS: val=660900',
+      '     kworker/u:0-5     [000] ....    79.046897: ' +
+                     'mali_hwc_TEX_WORDS: val=471193',
+      '     kworker/u:0-5     [000] ....    79.046901: ' +
+                     'mali_hwc_TEX_WORDS_DESC: val=707',
+      '     kworker/u:0-5     [000] ....    79.046900: ' +
+                     'mali_hwc_TEX_WORDS_L0: val=32',
+      '     kworker/u:0-5     [000] ....    79.046846: ' +
+                     'mali_hwc_TRIANGLES: val=130',
+      '     kworker/u:0-5     [000] ....    79.046885: ' +
+                     'mali_hwc_TRIPIPE_ACTIVE: val=691001',
+      '     kworker/u:0-5     [000] ....    78.896600: ' +
+                     'mali_hwc_UTLB_NEW_MISS: val=6',
+      '     kworker/u:0-5     [000] ....    78.896599: ' +
+                     'mali_hwc_UTLB_REPLAY_FULL: val=248',
+      '     kworker/u:0-5     [000] ....    78.896597: ' +
+                     'mali_hwc_UTLB_REPLAY_MISS: val=1',
+      '     kworker/u:0-5     [000] ....    78.896596: ' +
+                     'mali_hwc_UTLB_STALL: val=1',
+      '     kworker/u:0-5     [000] ....    79.046850: ' +
+                     'mali_hwc_VCACHE_HIT: val=311',
+      '     kworker/u:0-5     [000] ....    79.046851: ' +
+                     'mali_hwc_VCACHE_MISS: val=70'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var counters = m.getAllCounters();
+    assert.equal(counters.length, 103);
+
+    // all counters should have 1 sample
+    for (var tI = 0; tI < counters.length; tI++) {
+      var counter = counters[tI];
+      assert.equal(counter.series[0].samples.length, 1);
+    }
+    // TODO(sleffler) verify counter names? (not sure if it's worth the effort)
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/memreclaim_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/memreclaim_parser.html
new file mode 100644
index 0000000..45d89fb
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/memreclaim_parser.html
@@ -0,0 +1,188 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses drm driver events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux vmscan trace events.
+   * @constructor
+   */
+  function MemReclaimParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('mm_vmscan_kswapd_wake',
+        MemReclaimParser.prototype.kswapdWake.bind(this));
+    importer.registerEventHandler('mm_vmscan_kswapd_sleep',
+        MemReclaimParser.prototype.kswapdSleep.bind(this));
+    importer.registerEventHandler('mm_vmscan_direct_reclaim_begin',
+        MemReclaimParser.prototype.reclaimBegin.bind(this));
+    importer.registerEventHandler('mm_vmscan_direct_reclaim_end',
+        MemReclaimParser.prototype.reclaimEnd.bind(this));
+  }
+
+  // Matches the mm_vmscan_kswapd_wake record
+  //  mm_vmscan_kswapd_wake: nid=%d order=%d
+  var kswapdWakeRE = /nid=(\d+) order=(\d+)/;
+
+  // Matches the mm_vmscan_kswapd_sleep record
+  //  mm_vmscan_kswapd_sleep: order=%d
+  var kswapdSleepRE = /nid=(\d+)/;
+
+  // Matches the mm_vmscan_direct_reclaim_begin record
+  //  mm_vmscan_direct_reclaim_begin: order=%d may_writepage=%d gfp_flags=%s
+  var reclaimBeginRE = /order=(\d+) may_writepage=\d+ gfp_flags=(.+)/;
+
+  // Matches the mm_vmscan_direct_reclaim_end record
+  //  mm_vmscan_direct_reclaim_end: nr_reclaimed=%lu
+  var reclaimEndRE = /nr_reclaimed=(\d+)/;
+
+  MemReclaimParser.prototype = {
+    __proto__: Parser.prototype,
+
+    openAsyncSlice: function(ts, category, threadName, pid, key, name) {
+      var kthread = this.importer.getOrCreateKernelThread(
+          category + ':' + threadName, pid);
+      var slice = new tv.c.trace_model.AsyncSlice(
+          category, name,
+          tv.c.getColorIdForGeneralPurposeString(name),
+          ts);
+      slice.startThread = kthread.thread;
+
+      if (!kthread.openAsyncSlices) {
+        kthread.openAsyncSlices = { };
+      }
+      kthread.openAsyncSlices[key] = slice;
+    },
+
+    closeAsyncSlice: function(ts, category, threadName, pid, key, args) {
+      var kthread = this.importer.getOrCreateKernelThread(
+          category + ':' + threadName, pid);
+      if (kthread.openAsyncSlices) {
+        var slice = kthread.openAsyncSlices[key];
+        if (slice) {
+          slice.duration = ts - slice.start;
+          slice.args = args;
+          slice.endThread = kthread.thread;
+          slice.subSlices = [
+            new tv.c.trace_model.Slice(category, slice.title,
+                slice.colorId, slice.start, slice.args, slice.duration)
+          ];
+          kthread.thread.asyncSliceGroup.push(slice);
+          delete kthread.openAsyncSlices[key];
+        }
+      }
+    },
+
+    /**
+     * Parses memreclaim events and sets up state in the importer.
+     */
+    kswapdWake: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = kswapdWakeRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var nid = parseInt(event[1]);
+      var order = parseInt(event[2]);
+
+      var kthread = this.importer.getOrCreateKernelThread(
+          'kswapd: ' + eventBase.threadName,
+          pid, pid);
+      if (kthread.openSliceTS) {
+        if (order > kthread.order) {
+          kthread.order = order;
+        }
+      } else {
+        kthread.openSliceTS = ts;
+        kthread.order = order;
+      }
+      return true;
+    },
+
+    kswapdSleep: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var kthread = this.importer.getOrCreateKernelThread(
+          'kswapd: ' + eventBase.threadName,
+          pid, pid);
+      if (kthread.openSliceTS) {
+        var slice = new tv.c.trace_model.Slice('', eventBase.threadName,
+            tv.b.ui.getColorIdForGeneralPurposeString(eventBase.threadName),
+            kthread.openSliceTS,
+            {
+                order: kthread.order
+            },
+            ts - kthread.openSliceTS);
+
+        kthread.thread.sliceGroup.pushSlice(slice);
+      }
+      kthread.openSliceTS = undefined;
+      kthread.order = undefined;
+      return true;
+    },
+
+    reclaimBegin: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = reclaimBeginRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var order = parseInt(event[1]);
+      var gfp = event[2];
+
+      var kthread = this.importer.getOrCreateKernelThread(
+          'direct reclaim: ' + eventBase.threadName,
+          pid, pid);
+      kthread.openSliceTS = ts;
+      kthread.order = order;
+      kthread.gfp = gfp;
+      return true;
+    },
+
+    reclaimEnd: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = reclaimEndRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var nr_reclaimed = parseInt(event[1]);
+
+      var kthread = this.importer.getOrCreateKernelThread(
+          'direct reclaim: ' + eventBase.threadName,
+          pid, pid);
+      if (kthread.openSliceTS !== undefined) {
+        var slice = new tv.c.trace_model.Slice(
+            '', 'direct reclaim',
+            tv.b.ui.getColorIdForGeneralPurposeString(eventBase.threadName),
+            kthread.openSliceTS,
+            {
+                order: kthread.order,
+                gfp: kthread.gfp,
+                nr_reclaimed: nr_reclaimed
+            },
+            ts - kthread.openSliceTS);
+        kthread.thread.sliceGroup.pushSlice(slice);
+      }
+      kthread.openSliceTS = undefined;
+      kthread.order = undefined;
+      kthread.gfp = undefined;
+      return true;
+    }
+
+  };
+
+  Parser.register(MemReclaimParser);
+
+  return {
+    MemReclaimParser: MemReclaimParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/memreclaim_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/memreclaim_parser_test.html
new file mode 100644
index 0000000..e724bbc
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/memreclaim_parser_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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('memreclaimImport', function() {
+    var lines = [
+      ' surfaceflinger-1155  ( 1155) [001] ...1 12839.528756: ' +
+          'mm_vmscan_direct_reclaim_begin: order=0 ' +
+          'may_writepage=1 ' +
+                 'gfp_flags=GFP_KERNEL|GFP_NOWARN|GFP_ZERO|0x2',
+      ' surfaceflinger-1155  ( 1155) [001] ...1 12839.531950: ' +
+                 'mm_vmscan_direct_reclaim_end: nr_reclaimed=66',
+      ' kswapd0-33    (   33) [001] ...1 12838.491407: mm_vmscan_kswapd_wake: nid=0 order=0', // @suppress longLineCheck
+      ' kswapd0-33    (   33) [001] ...1 12838.529770: mm_vmscan_kswapd_wake: nid=0 order=2', // @suppress longLineCheck
+      ' kswapd0-33    (   33) [001] ...1 12840.545737: mm_vmscan_kswapd_sleep: nid=0' // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    assert.equal(m.processes['1155'].threads['1155'].sliceGroup.length, 1);
+    assert.equal(m.processes['33'].threads['33'].sliceGroup.length, 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/parser.html
new file mode 100644
index 0000000..e2cb18f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/parser.html
@@ -0,0 +1,90 @@
+<!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/extension_registry.html">
+<script>
+'use strict';
+
+/**
+ * @fileoverview Base class for linux perf event parsers.
+ *
+ * The linux perf trace event importer depends on subclasses of
+ * Parser to parse event data.  Each subclass corresponds
+ * to a group of trace events; e.g. SchedParser implements
+ * parsing of sched:* kernel trace events.  Parser subclasses must
+ * call Parser.register to arrange to be instantiated
+ * and their constructor must register their event handlers with the
+ * importer.  For example,
+ *
+ * var Parser = tv.e.importer.linux_perf.Parser;
+ *
+ * function WorkqueueParser(importer) {
+ *   Parser.call(this, importer);
+ *
+ *   importer.registerEventHandler('workqueue_execute_start',
+ *       WorkqueueParser.prototype.executeStartEvent.bind(this));
+ *   importer.registerEventHandler('workqueue_execute_end',
+ *       WorkqueueParser.prototype.executeEndEvent.bind(this));
+ * }
+ *
+ * Parser.register(WorkqueueParser);
+ *
+ * When a registered event name is found in the data stream the associated
+ * event handler is invoked:
+ *
+ *   executeStartEvent: function(eventName, cpuNumber, ts, eventBase)
+ *
+ * If the routine returns false the caller will generate an import error
+ * saying there was a problem parsing it.  Handlers can also emit import
+ * messages using this.importer.model.importWarning.  If this is done in lieu of
+ * the generic import error it may be desirable for the handler to return
+ * true.
+ *
+ * Trace events generated by writing to the trace_marker file are expected
+ * to have a leading text marker followed by a ':'; e.g. the trace clock
+ * synchronization event is:
+ *
+ *  tracing_mark_write: trace_event_clock_sync: parent_ts=0
+ *
+ * To register an event handler for these events, prepend the marker with
+ * 'tracing_mark_write:'; e.g.
+ *
+ *    this.registerEventHandler('tracing_mark_write:trace_event_clock_sync',
+ *
+ * All subclasses should depend on importer.linux_perf.parser, e.g.
+ *
+ * tv.defineModule('importer.linux_perf.workqueue_parser')
+ *   .dependsOn('importer.linux_perf.parser')
+ *   .exportsTo('tracing', function()
+ *
+ * and be listed in the dependsOn of LinuxPerfImporter.  Beware that after
+ * adding a new subclass you must run build/generate_about_tracing_contents.py
+ * to regenerate tv.e.about_tracing.*.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+  /**
+   * Parses linux perf events.
+   * @constructor
+   */
+  function Parser(importer) {
+    this.importer = importer;
+    this.model = importer.model;
+  }
+
+  Parser.prototype = {
+    __proto__: Object.prototype
+  };
+
+  var options = new tv.b.ExtensionRegistryOptions(tv.b.BASIC_REGISTRY_MODE);
+  options.mandatoryBaseClass = Parser;
+  tv.b.decorateExtensionRegistry(Parser, options);
+
+  return {
+    Parser: Parser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/power_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/power_parser.html
new file mode 100644
index 0000000..09b7889
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/power_parser.html
@@ -0,0 +1,148 @@
+<!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="/extras/importer/linux_perf/parser.html">
+<link rel="import" href="/core/trace_model/counter_series.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses power events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux power trace events.
+   * @constructor
+   */
+  function PowerParser(importer) {
+    Parser.call(this, importer);
+
+    // NB: old-style power events, deprecated
+    importer.registerEventHandler('power_start',
+        PowerParser.prototype.powerStartEvent.bind(this));
+    importer.registerEventHandler('power_frequency',
+        PowerParser.prototype.powerFrequencyEvent.bind(this));
+
+    importer.registerEventHandler('cpu_frequency',
+        PowerParser.prototype.cpuFrequencyEvent.bind(this));
+    importer.registerEventHandler('cpu_idle',
+        PowerParser.prototype.cpuIdleEvent.bind(this));
+  }
+
+  PowerParser.prototype = {
+    __proto__: Parser.prototype,
+
+    cpuStateSlice: function(ts, targetCpuNumber, eventType, cpuState) {
+      var targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+      var powerCounter;
+      if (eventType != '1') {
+        this.importer.model.importWarning({
+          type: 'parse_error',
+          message: 'Don\'t understand power_start events of ' +
+              'type ' + eventType
+        });
+        return;
+      }
+      powerCounter = targetCpu.getOrCreateCounter('', 'C-State');
+      if (powerCounter.numSeries === 0) {
+        powerCounter.addSeries(new tv.c.trace_model.CounterSeries('state',
+            tv.b.ui.getColorIdForGeneralPurposeString(
+                powerCounter.name + '.' + 'state')));
+      }
+      powerCounter.series.forEach(function(series) {
+        series.addCounterSample(ts, cpuState);
+      });
+    },
+
+    cpuIdleSlice: function(ts, targetCpuNumber, cpuState) {
+      var targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+      var powerCounter = targetCpu.getOrCreateCounter('', 'C-State');
+      if (powerCounter.numSeries === 0) {
+        powerCounter.addSeries(new tv.c.trace_model.CounterSeries('state',
+            tv.b.ui.getColorIdForGeneralPurposeString(powerCounter.name)));
+      }
+      // NB: 4294967295/-1 means an exit from the current state
+      var val = (cpuState != 4294967295 ? cpuState + 1 : 0);
+      powerCounter.series.forEach(function(series) {
+        series.addCounterSample(ts, val);
+      });
+    },
+
+    cpuFrequencySlice: function(ts, targetCpuNumber, powerState) {
+      var targetCpu = this.importer.getOrCreateCpu(targetCpuNumber);
+      var powerCounter =
+          targetCpu.getOrCreateCounter('', 'Clock Frequency');
+      if (powerCounter.numSeries === 0) {
+        powerCounter.addSeries(new tv.c.trace_model.CounterSeries('state',
+            tv.b.ui.getColorIdForGeneralPurposeString(
+                powerCounter.name + '.' + 'state')));
+      }
+      powerCounter.series.forEach(function(series) {
+        series.addCounterSample(ts, powerState);
+      });
+    },
+
+    /**
+     * Parses power events and sets up state in the importer.
+     */
+    powerStartEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /type=(\d+) state=(\d) cpu_id=(\d)+/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var targetCpuNumber = parseInt(event[3]);
+      var cpuState = parseInt(event[2]);
+      this.cpuStateSlice(ts, targetCpuNumber, event[1], cpuState);
+      return true;
+    },
+
+    powerFrequencyEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /type=(\d+) state=(\d+) cpu_id=(\d)+/
+          .exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var targetCpuNumber = parseInt(event[3]);
+      var powerState = parseInt(event[2]);
+      this.cpuFrequencySlice(ts, targetCpuNumber, powerState);
+      return true;
+    },
+
+    cpuFrequencyEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /state=(\d+) cpu_id=(\d)+/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var targetCpuNumber = parseInt(event[2]);
+      var powerState = parseInt(event[1]);
+      this.cpuFrequencySlice(ts, targetCpuNumber, powerState);
+      return true;
+    },
+
+    cpuIdleEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = /state=(\d+) cpu_id=(\d)+/.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var targetCpuNumber = parseInt(event[2]);
+      var cpuState = parseInt(event[1]);
+      this.cpuIdleSlice(ts, targetCpuNumber, cpuState);
+      return true;
+    }
+  };
+
+  Parser.register(PowerParser);
+
+  return {
+    PowerParser: PowerParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/power_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/power_parser_test.html
new file mode 100644
index 0000000..13a6ded
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/power_parser_test.html
@@ -0,0 +1,87 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('powerFrequencyImport', function() {
+    var lines = [
+      ' kworker/0:3-6880  [000]  2784.783015: power_frequency: ' +
+                 'type=2 state=1000000 cpu_id=0',
+      ' kworker/1:2-7269  [001]  2784.788993: power_frequency: ' +
+                 'type=2 state=800000 cpu_id=1',
+      ' kworker/1:2-7269  [001]  2784.993120: power_frequency: ' +
+                 'type=2 state=1300000 cpu_id=1'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c0 = m.kernel.cpus[0];
+    assert.equal(c0.slices.length, 0);
+    assert.equal(c0.counters['Clock Frequency'].series[0].samples.length, 1);
+
+    var c1 = m.kernel.cpus[1];
+    assert.equal(c1.slices.length, 0);
+    assert.equal(c1.counters['Clock Frequency'].series[0].samples.length, 2);
+  });
+
+  test('cpuFrequencyImport', function() {
+    var lines = [
+      '     kworker/1:0-9665  [001] 15051.007301: cpu_frequency: ' +
+                     'state=800000 cpu_id=1',
+      '     kworker/1:0-9665  [001] 15051.010278: cpu_frequency: ' +
+                     'state=1300000 cpu_id=1',
+      '     kworker/0:2-7972  [000] 15051.010278: cpu_frequency: ' +
+                     'state=1000000 cpu_id=0',
+      '     kworker/0:2-7972  [000] 15051.020304: cpu_frequency: ' +
+                     'state=800000 cpu_id=0'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c0 = m.kernel.cpus[0];
+    assert.equal(c0.slices.length, 0);
+    assert.equal(c0.counters['Clock Frequency'].series[0].samples.length, 2);
+
+    var c1 = m.kernel.cpus[1];
+    assert.equal(c1.slices.length, 0);
+    assert.equal(c1.counters['Clock Frequency'].series[0].samples.length, 2);
+  });
+
+  test('cpuIdleImport', function() {
+    var lines = [
+      '          <idle>-0     [000] 15050.992883: cpu_idle: ' +
+          'state=1 cpu_id=0',
+      '          <idle>-0     [000] 15050.993027: cpu_idle: ' +
+          'state=4294967295 cpu_id=0',
+      '          <idle>-0     [001] 15050.993132: cpu_idle: ' +
+          'state=1 cpu_id=1',
+      '          <idle>-0     [001] 15050.993276: cpu_idle: ' +
+          'state=4294967295 cpu_id=1',
+      '          <idle>-0     [001] 15050.993279: cpu_idle: ' +
+          'state=3 cpu_id=1',
+      '          <idle>-0     [001] 15050.993457: cpu_idle: ' +
+          'state=4294967295 cpu_id=1'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var c0 = m.kernel.cpus[0];
+    assert.equal(c0.slices.length, 0);
+    assert.equal(c0.counters['C-State'].series[0].samples.length, 2);
+
+    var c1 = m.kernel.cpus[1];
+    assert.equal(c1.slices.length, 0);
+    assert.equal(c1.counters['C-State'].series[0].samples.length, 4);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/regulator_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/regulator_parser.html
new file mode 100644
index 0000000..d41b7ef
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/regulator_parser.html
@@ -0,0 +1,142 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses regulator events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux regulator trace events.
+   * @constructor
+   */
+  function RegulatorParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('regulator_enable',
+        RegulatorParser.prototype.regulatorEnableEvent.bind(this));
+    importer.registerEventHandler('regulator_enable_delay',
+        RegulatorParser.prototype.regulatorEnableDelayEvent.bind(this));
+    importer.registerEventHandler('regulator_enable_complete',
+        RegulatorParser.prototype.regulatorEnableCompleteEvent.bind(this));
+    importer.registerEventHandler('regulator_disable',
+        RegulatorParser.prototype.regulatorDisableEvent.bind(this));
+    importer.registerEventHandler('regulator_disable_complete',
+        RegulatorParser.prototype.regulatorDisableCompleteEvent.bind(this));
+    importer.registerEventHandler('regulator_set_voltage',
+        RegulatorParser.prototype.regulatorSetVoltageEvent.bind(this));
+    importer.registerEventHandler('regulator_set_voltage_complete',
+        RegulatorParser.prototype.regulatorSetVoltageCompleteEvent.bind(this));
+
+    this.model_ = importer.model_;
+  }
+
+  // Matches the regulator_enable record
+  var regulatorEnableRE = /name=(.+)/;
+
+  // Matches the regulator_disable record
+  var regulatorDisableRE = /name=(.+)/;
+
+  // Matches the regulator_set_voltage_complete record
+  var regulatorSetVoltageCompleteRE = /name=(\S+), val=(\d+)/;
+
+  RegulatorParser.prototype = {
+    __proto__: Parser.prototype,
+
+    /*
+     * Get or create a counter with one series.
+     */
+    getCtr_: function(ctrName, valueName) {
+      var ctr = this.model_.kernel
+        .getOrCreateCounter(null, 'vreg ' + ctrName + ' ' + valueName);
+      //Initialize the counter's series fields if needed.
+      if (ctr.series[0] === undefined) {
+        ctr.addSeries(new tv.c.trace_model.CounterSeries(valueName,
+        tv.b.ui.getColorIdForGeneralPurposeString(
+        ctrName + '.' + valueName)));
+      }
+      return ctr;
+    },
+
+    /**
+     * Parses regulator events and sets up state in the importer.
+     */
+    regulatorEnableEvent: function(eventName, cpuNum, pid, ts, eventBase) {
+      var event = regulatorEnableRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var name = event[1];
+
+      var ctr = this.getCtr_(name, 'enabled');
+      ctr.series[0].addCounterSample(ts, 1);
+
+      return true;
+    },
+
+    regulatorEnableDelayEvent: function(eventName, cpuNum, pid, ts, eventBase) {
+      return true;
+    },
+
+    regulatorEnableCompleteEvent: function(eventName, cpuNum, pid, ts,
+                                           eventBase) {
+      return true;
+    },
+
+    regulatorDisableEvent: function(eventName, cpuNum, pid, ts, eventBase) {
+      var event = regulatorDisableRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var name = event[1];
+
+      var ctr = this.getCtr_(name, 'enabled');
+      ctr.series[0].addCounterSample(ts, 0);
+
+      return true;
+    },
+
+    regulatorDisableCompleteEvent: function(eventName, cpuNum, pid, ts,
+                                            eventBase) {
+      return true;
+    },
+
+    regulatorSetVoltageEvent: function(eventName, cpuNum, pid, ts, eventBase) {
+      return true;
+    },
+
+    regulatorSetVoltageCompleteEvent: function(eventName, cpuNum, pid, ts,
+                                               eventBase) {
+      var event = regulatorSetVoltageCompleteRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var name = event[1];
+      var voltage = parseInt(event[2]);
+
+      var ctr = this.getCtr_(name, 'voltage');
+      ctr.series[0].addCounterSample(ts, voltage);
+
+      return true;
+    }
+
+  };
+
+  Parser.register(RegulatorParser);
+
+  return {
+    RegulatorParser: RegulatorParser
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/regulator_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/regulator_parser_test.html
new file mode 100644
index 0000000..5522029
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/regulator_parser_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/test_utils.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  test('regulatorImport', function() {
+    var lines = [
+      ' kworker/0:2H-14312 [000] ...1 143713.787749: ' +
+        'regulator_set_voltage: name=krait0 (810000-1100000)',
+      ' kworker/0:2H-14312 [000] ...1 143713.787778: ' +
+        'regulator_set_voltage_complete: name=krait0, val=810000',
+      ' kworker/0:2H-14312 [000] ...1 143714.037871: ' +
+        'regulator_set_voltage: name=krait0 (800000-1100000)',
+      ' kworker/0:2H-14312 [000] ...1 143714.037895: ' +
+        'regulator_set_voltage_complete: name=krait0, val=800000',
+      'kworker/0:1-30321 [000] ...1 144568.624596: ' +
+        'regulator_enable: name=8941_smbb_boost',
+      'kworker/0:1-30321 [000] ...1 144568.624715: ' +
+        'regulator_enable_delay: name=8941_smbb_boost',
+      'kworker/0:1-30321 [000] ...1 144568.624723: ' +
+        'regulator_enable_complete: name=8941_smbb_boost',
+      'kworker/0:1-30321 [000] ...1 144568.653546: ' +
+        'regulator_disable: name=8941_smbb_boost',
+      'kworker/0:1-30321 [000] ...1 144568.654785: ' +
+        'regulator_disable_complete: name=8941_smbb_boost'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    console.log(m.importWarnings);
+    assert.isFalse(m.hasImportWarnings);
+
+    assert.property(m.kernel.counters, 'null.vreg krait0 voltage');
+    assert.property(m.kernel.counters, 'null.vreg 8941_smbb_boost enabled');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/sched_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/sched_parser.html
new file mode 100644
index 0000000..c9fc913
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/sched_parser.html
@@ -0,0 +1,107 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses scheduler events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux sched trace events.
+   * @constructor
+   */
+  function SchedParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('sched_switch',
+        SchedParser.prototype.schedSwitchEvent.bind(this));
+    importer.registerEventHandler('sched_wakeup',
+        SchedParser.prototype.schedWakeupEvent.bind(this));
+  }
+
+  var TestExports = {};
+
+  // Matches the sched_switch record
+  var schedSwitchRE = new RegExp(
+      'prev_comm=(.+) prev_pid=(\\d+) prev_prio=(\\d+) ' +
+      'prev_state=(\\S\\+?|\\S\\|\\S) ==> ' +
+      'next_comm=(.+) next_pid=(\\d+) next_prio=(\\d+)');
+  TestExports.schedSwitchRE = schedSwitchRE;
+
+  // Matches the sched_wakeup record
+  var schedWakeupRE =
+      /comm=(.+) pid=(\d+) prio=(\d+) success=(\d+) target_cpu=(\d+)/;
+  TestExports.schedWakeupRE = schedWakeupRE;
+
+  SchedParser.prototype = {
+    __proto__: Parser.prototype,
+
+    /**
+     * Parses scheduler events and sets up state in the CPUs of the importer.
+     */
+    schedSwitchEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = schedSwitchRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var prevState = event[4];
+      var nextComm = event[5];
+      var nextPid = parseInt(event[6]);
+      var nextPrio = parseInt(event[7]);
+
+      var nextThread = this.importer.threadsByLinuxPid[nextPid];
+      var nextName;
+      if (nextThread)
+        nextName = nextThread.userFriendlyName;
+      else
+        nextName = nextComm;
+
+      var cpu = this.importer.getOrCreateCpu(cpuNumber);
+      cpu.switchActiveThread(
+          ts,
+          {stateWhenDescheduled: prevState},
+          nextPid,
+          nextName,
+          {
+            comm: nextComm,
+            tid: nextPid,
+            prio: nextPrio
+          });
+
+      return true;
+    },
+
+    schedWakeupEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = schedWakeupRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var fromPid = pid;
+      var comm = event[1];
+      var pid = parseInt(event[2]);
+      var prio = parseInt(event[3]);
+      this.importer.markPidRunnable(ts, pid, comm, prio, fromPid);
+      return true;
+    }
+  };
+
+  Parser.register(SchedParser);
+
+  return {
+    SchedParser: SchedParser,
+    _SchedParserTestExports: TestExports
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/sched_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/sched_parser_test.html
new file mode 100644
index 0000000..450512e
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/sched_parser_test.html
@@ -0,0 +1,165 @@
+<!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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('schedSwitchRE', function() {
+    var re = tv.e.importer.linux_perf._SchedParserTestExports.schedSwitchRE;
+    var x = re.exec('prev_comm=swapper prev_pid=0 prev_prio=120 prev_state=R ' +
+        '==> next_comm=SurfaceFlinger next_pid=178 next_prio=112');
+    assert.isNotNull(x);
+    assert.equal(x[1], 'swapper');
+    assert.equal(x[2], '0');
+    assert.equal(x[3], '120');
+    assert.equal(x[4], 'R');
+    assert.equal(x[5], 'SurfaceFlinger');
+    assert.equal(x[6], '178');
+    assert.equal(x[7], '112');
+
+    var x = re.exec('prev_comm=.android.chrome prev_pid=1562 prev_prio=120 prev_state=R ==> next_comm=Binder Thread # next_pid=195 next_prio=120'); // @suppress longLineCheck
+    assert.isNotNull(x);
+    assert.equal(x[1], '.android.chrome');
+    assert.equal(x[5], 'Binder Thread #');
+
+    var x = re.exec('prev_comm=Binder Thread # prev_pid=1562 prev_prio=120 prev_state=R ==> next_comm=.android.chrome next_pid=195 next_prio=120'); // @suppress longLineCheck
+    assert.isNotNull(x);
+    assert.equal(x[1], 'Binder Thread #');
+    assert.equal(x[5], '.android.chrome');
+
+    // explicit test for prev_state of D|W
+    var x = re.exec('prev_comm=.android.chrome prev_pid=1562 prev_prio=120 ' +
+        'prev_state=D|W ==> next_comm=Binder Thread # next_pid=195 ' +
+        'next_prio=120');
+    assert.isNotNull(x);
+    assert.equal(x[4], 'D|W');
+  });
+
+  test('schedWakeupRE', function() {
+    var re = tv.e.importer.linux_perf._SchedParserTestExports.schedWakeupRE;
+    var x = re.exec(
+        'comm=SensorService pid=207 prio=112 success=1 target_cpu=000');
+    assert.isNotNull(x);
+    assert.equal(x[1], 'SensorService');
+    assert.equal(x[2], '207');
+    assert.equal(x[3], '112');
+    assert.equal(x[4], '1');
+    assert.equal(x[5], '000');
+  });
+
+  test('importOneSequenceWithSchedWakeUp', function() {
+    var lines = [
+      'ndroid.launcher-584   [001] d..3 12622.506890: sched_switch: prev_comm=ndroid.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=ndroid.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+      'ndroid.launcher-584   [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+      'ndroid.launcher-584   [001] d..3 12622.506950: sched_switch: prev_comm=ndroid.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=ndroid.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var thread = m.findAllThreadsNamed('Binder_1')[0];
+    var timeSlices = thread.timeSlices;
+    assert.equal(timeSlices.length, 4);
+
+    var runningSlice = timeSlices[0];
+    assert.equal(runningSlice.title, 'Running');
+    assert.closeTo(12622506.890, runningSlice.start, 1e-5);
+    assert.closeTo(.918 - .890, runningSlice.duration, 1e-5);
+
+    var sleepSlice = timeSlices[1];
+    assert.equal(sleepSlice.title, 'Uninterruptible Sleep');
+    assert.closeTo(12622506.918, sleepSlice.start, 1e-5);
+    assert.closeTo(.936 - .918, sleepSlice.duration, 1e-5);
+
+    var wakeupSlice = timeSlices[2];
+    assert.equal(wakeupSlice.title, 'Runnable');
+    assert.closeTo(12622506.936, wakeupSlice.start, 1e-5);
+    assert.closeTo(.950 - .936, wakeupSlice.duration, 1e-5);
+    assert.equal(wakeupSlice.args['wakeup from tid'], 584);
+
+    var runningSlice2 = timeSlices[3];
+    assert.equal(runningSlice2.title, 'Running');
+    assert.closeTo(12622506.950, runningSlice2.start, 1e-5);
+    assert.closeTo(7.253 - 6.950, runningSlice2.duration, 1e-5);
+  });
+
+  test('importWithUnknownSleepState', function() {
+    var lines = [
+      'ndroid.launcher-584   [001] d..3 12622.506890: sched_switch: prev_comm=ndroid.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=F|O ==> next_comm=ndroid.launcher next_pid=584 next_prio=120', // @suppress longLineCheck
+      'ndroid.launcher-584   [001] d..4 12622.506936: sched_wakeup: comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001', // @suppress longLineCheck
+      'ndroid.launcher-584   [001] d..3 12622.506950: sched_switch: prev_comm=ndroid.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=F|O ==> next_comm=ndroid.launcher next_pid=584 next_prio=120' // @suppress longLineCheck
+    ];
+
+    var m;
+    assert.doesNotThrow(function() {
+      m = new tv.c.TraceModel(lines.join('\n'), false);
+    });
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(m.importWarnings[0].message, 'Unrecognized sleep state: F|O');
+
+    var thread = m.findAllThreadsNamed('Binder_1')[0];
+    var timeSlices = thread.timeSlices;
+
+    assert.equal(timeSlices[1].title, 'UNKNOWN');
+  });
+
+  test('importWithUninterruptibleSleep', function() {
+    var lines = [
+      'ndroid.launcher-584   [001] d..3 12622.506890: sched_switch: ' +
+          'prev_comm=ndroid.launcher prev_pid=584 ' +
+          'prev_prio=120 prev_state=R+ ' +
+          '==> next_comm=Binder_1 next_pid=217 next_prio=120',
+
+      '       Binder_1-217   [001] d..3 12622.506918: sched_switch: ' +
+          'prev_comm=Binder_1 prev_pid=217 prev_prio=120 prev_state=D|K ' +
+          '==> next_comm=ndroid.launcher next_pid=584 next_prio=120',
+
+      'ndroid.launcher-584   [001] d..4 12622.506936: sched_wakeup: ' +
+          'comm=Binder_1 pid=217 prio=120 success=1 target_cpu=001',
+
+      'ndroid.launcher-584   [001] d..3 12622.506950: sched_switch: ' +
+          'prev_comm=ndroid.launcher prev_pid=584 ' +
+          'prev_prio=120 prev_state=R+ ' +
+          '==> next_comm=Binder_1 next_pid=217 next_prio=120',
+
+      '       Binder_1-217   [001] ...1 12622.507057: tracing_mark_write: ' +
+          'B|128|queueBuffer',
+
+      '       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=ndroid.launcher next_pid=584 next_prio=120'
+    ];
+
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var thread = m.findAllThreadsNamed('Binder_1')[0];
+    var timeSlices = thread.timeSlices;
+    assert.equal(timeSlices.length, 4);
+
+    var wakeKillSlice = timeSlices[1];
+    assert.equal(wakeKillSlice.title, 'Uninterruptible Sleep | WakeKill');
+    assert.closeTo(12622506.918, wakeKillSlice.start, 1e-5);
+    assert.closeTo(.936 - .918, wakeKillSlice.duration, 1e-5);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/sync_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/sync_parser.html
new file mode 100644
index 0000000..b46596d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/sync_parser.html
@@ -0,0 +1,134 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses sync events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux sync trace events.
+   * @constructor
+   */
+  function SyncParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler(
+        'sync_timeline',
+        SyncParser.prototype.timelineEvent.bind(this));
+    importer.registerEventHandler(
+        'sync_wait',
+        SyncParser.prototype.syncWaitEvent.bind(this));
+    importer.registerEventHandler(
+        'sync_pt',
+        SyncParser.prototype.syncPtEvent.bind(this));
+    this.model_ = importer.model_;
+  }
+
+  var syncTimelineRE = /name=(\S+) value=(\S*)/;
+  var syncWaitRE = /(\S+) name=(\S+) state=(\d+)/;
+  var syncPtRE = /name=(\S+) value=(\S*)/;
+
+  SyncParser.prototype = {
+    __proto__: Parser.prototype,
+
+    /**
+     * Parses sync events and sets up state in the importer.
+     */
+    timelineEvent: function(eventName, cpuNumber, pid,
+                            ts, eventBase) {
+      var event = syncTimelineRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var thread = this.importer.getOrCreatePseudoThread(event[1]);
+
+      if (thread.lastActiveTs !== undefined) {
+        var duration = ts - thread.lastActiveTs;
+        var value = thread.lastActiveValue;
+        if (value == undefined)
+          value = ' ';
+        var slice = new tv.c.trace_model.Slice(
+            '', value,
+            tv.b.ui.getColorIdForGeneralPurposeString(value),
+            thread.lastActiveTs, {},
+            duration);
+        thread.thread.sliceGroup.pushSlice(slice);
+      }
+      thread.lastActiveTs = ts;
+      thread.lastActiveValue = event[2];
+      return true;
+    },
+
+    syncWaitEvent: function(eventName, cpuNumber, pid, ts,
+                            eventBase) {
+      var event = syncWaitRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      if (eventBase.tgid === undefined) {
+        return false;
+      }
+
+      var tgid = parseInt(eventBase.tgid);
+      var thread = this.model_.getOrCreateProcess(tgid)
+        .getOrCreateThread(pid);
+      thread.name = eventBase.threadName;
+      var slices = thread.kernelSliceGroup;
+      if (!slices.isTimestampValidForBeginOrEnd(ts)) {
+        this.model_.importWarning({
+          type: 'parse_error',
+          message: 'Timestamps are moving backward.'
+        });
+        return false;
+      }
+
+      var name = 'fence_wait("' + event[2] + '")';
+      if (event[1] == 'begin') {
+        var slice = slices.beginSlice(null, name, ts, {
+          'Start state': event[3]
+        });
+      } else if (event[1] == 'end') {
+        if (slices.openSliceCount > 0) {
+          slices.endSlice(ts);
+        }
+      } else {
+        return false;
+      }
+
+      return true;
+    },
+
+    syncPtEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = syncPtRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      return true;
+
+      var thread = this.importer.getOrCreateKernelThread(
+          eventBase[1]).thread;
+      thread.syncWaitSyncPts[event[1]] = event[2];
+      return true;
+    }
+  };
+
+  Parser.register(SyncParser);
+
+  return {
+    SyncParser: SyncParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/sync_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/sync_parser_test.html
new file mode 100644
index 0000000..f72e551
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/sync_parser_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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('syncEventImport', function() {
+    var lines = [
+      's3c-fb-92            (     0) [000] ...1  7206.550061: sync_timeline: name=s3c-fb value=7094', // @suppress longLineCheck
+      'TimedEventQueue-2700 (     0) [001] ...1  7206.569027: sync_wait: begin name=SurfaceView:6 state=1', // @suppress longLineCheck
+      'TimedEventQueue-2700 (     0) [001] ...1  7206.569038: sync_pt: name=malitl_124_0x40b6406c value=7289', // @suppress longLineCheck
+      'TimedEventQueue-2700 (     0) [001] ...1  7206.569056: sync_pt: name=exynos-gsc.0-src value=25', // @suppress longLineCheck
+      'TimedEventQueue-2700 (     0) [001] ...1  7206.569068: sync_wait: end name=SurfaceView:6 state=1', // @suppress longLineCheck
+      'irq/128-s5p-mfc-62   (     0) [000] d..3  7206.572402: sync_timeline: name=vb2 value=37', // @suppress longLineCheck
+      'irq/128-s5p-mfc-62   (     0) [000] d..3  7206.572475: sync_timeline: name=vb2 value=33', // @suppress longLineCheck
+      'SurfaceFlinger-225   (     0) [001] ...1  7206.584769: sync_timeline: name=malitl_124_0x40b6406c value=7290', // @suppress longLineCheck
+      'kworker/u:5-2269     (     0) [000] ...1  7206.586745: sync_wait: begin name=display state=1', // @suppress longLineCheck
+      'kworker/u:5-2269     (     0) [000] ...1  7206.586750: sync_pt: name=s3c-fb value=7093', // @suppress longLineCheck
+      'kworker/u:5-2269     (     0) [000] ...1  7206.586760: sync_wait: end name=display state=1', // @suppress longLineCheck
+      's3c-fb-92            (     0) [000] ...1  7206.587193: sync_wait: begin name=vb2 state=0', // @suppress longLineCheck
+      's3c-fb-92            (     0) [000] ...1  7206.587198: sync_pt: name=exynos-gsc.0-dst value=27', // @suppress longLineCheck
+      '<idle>-0             (     0) [000] d.h4  7206.591133: sync_timeline: name=exynos-gsc.0-src value=27', // @suppress longLineCheck
+      '<idle>-0             (     0) [000] d.h4  7206.591152: sync_timeline: name=exynos-gsc.0-dst value=27', // @suppress longLineCheck
+      's3c-fb-92            (     0) [000] ...1  7206.591244: sync_wait: end name=vb2 state=1' // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    var threads = m.getAllThreads();
+    assert.equal(threads.length, 4);
+
+    var threads = m.findAllThreadsNamed('s3c-fb');
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].sliceGroup.length, 1);
+
+    var threads = m.findAllThreadsNamed('kworker/u:5');
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].sliceGroup.length, 1);
+    assert.equal('fence_wait("display")',
+                 threads[0].sliceGroup.slices[0].title);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/workqueue_parser.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/workqueue_parser.html
new file mode 100644
index 0000000..1437011
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/workqueue_parser.html
@@ -0,0 +1,102 @@
+<!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="/extras/importer/linux_perf/parser.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Parses workqueue events in the Linux event trace format.
+ */
+tv.exportTo('tv.e.importer.linux_perf', function() {
+
+  var Parser = tv.e.importer.linux_perf.Parser;
+
+  /**
+   * Parses linux workqueue trace events.
+   * @constructor
+   */
+  function WorkqueueParser(importer) {
+    Parser.call(this, importer);
+
+    importer.registerEventHandler('workqueue_execute_start',
+        WorkqueueParser.prototype.executeStartEvent.bind(this));
+    importer.registerEventHandler('workqueue_execute_end',
+        WorkqueueParser.prototype.executeEndEvent.bind(this));
+    importer.registerEventHandler('workqueue_queue_work',
+        WorkqueueParser.prototype.executeQueueWork.bind(this));
+    importer.registerEventHandler('workqueue_activate_work',
+        WorkqueueParser.prototype.executeActivateWork.bind(this));
+  }
+
+  // Matches the workqueue_execute_start record
+  //  workqueue_execute_start: work struct c7a8a89c: function MISRWrapper
+  var workqueueExecuteStartRE = /work struct (.+): function (\S+)/;
+
+  // Matches the workqueue_execute_start record
+  //  workqueue_execute_end: work struct c7a8a89c
+  var workqueueExecuteEndRE = /work struct (.+)/;
+
+  WorkqueueParser.prototype = {
+    __proto__: Parser.prototype,
+
+    /**
+     * Parses workqueue events and sets up state in the importer.
+     */
+    executeStartEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = workqueueExecuteStartRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var kthread = this.importer.getOrCreateKernelThread(eventBase.threadName,
+          pid, pid);
+      kthread.openSliceTS = ts;
+      kthread.openSlice = event[2];
+      return true;
+    },
+
+    executeEndEvent: function(eventName, cpuNumber, pid, ts, eventBase) {
+      var event = workqueueExecuteEndRE.exec(eventBase.details);
+      if (!event)
+        return false;
+
+      var kthread = this.importer.getOrCreateKernelThread(eventBase.threadName,
+          pid, pid);
+      if (kthread.openSlice) {
+        var slice = new tv.c.trace_model.Slice('', kthread.openSlice,
+            tv.b.ui.getColorIdForGeneralPurposeString(kthread.openSlice),
+            kthread.openSliceTS,
+            {},
+            ts - kthread.openSliceTS);
+
+        kthread.thread.sliceGroup.pushSlice(slice);
+      }
+      kthread.openSlice = undefined;
+      return true;
+    },
+
+    executeQueueWork: function(eventName, cpuNumber, pid, ts, eventBase) {
+      // TODO: Do something with this event?
+      return true;
+    },
+
+    executeActivateWork: function(eventName, cpuNumber, pid, ts, eventBase) {
+      // TODO: Do something with this event?
+      return true;
+    }
+
+  };
+
+  Parser.register(WorkqueueParser);
+
+  return {
+    WorkqueueParser: WorkqueueParser
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/linux_perf/workqueue_parser_test.html b/trace-viewer/trace_viewer/extras/importer/linux_perf/workqueue_parser_test.html
new file mode 100644
index 0000000..f0e2dc6
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/linux_perf/workqueue_parser_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="/extras/importer/linux_perf/linux_perf_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('workQueueImport', function() {
+    var lines = [
+      ' kworker/0:3-6880  [000]  2784.771958: workqueue_execute_start: ' +
+                 'work struct ffff8800a5083a20: function intel_unpin_work_fn',
+      ' kworker/0:3-6880  [000]  2784.771966: workqueue_execute_end: ' +
+                 'work struct ffff8800a5083a20',
+      ' kworker/1:2-7269  [001]  2784.805966: workqueue_execute_start: ' +
+                 'work struct ffff88014fb0f158: function do_dbs_timer',
+      ' kworker/1:2-7269  [001]  2784.805975: workqueue_execute_end: ' +
+                 'work struct ffff88014fb0f158'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isFalse(m.hasImportWarnings);
+
+    assert.equal(m.processes['6880'].threads['6880'].sliceGroup.length, 1);
+    assert.equal(m.processes['7269'].threads['7269'].sliceGroup.length, 1);
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/trace2html_importer.html b/trace-viewer/trace_viewer/extras/importer/trace2html_importer.html
new file mode 100644
index 0000000..6b3830c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/trace2html_importer.html
@@ -0,0 +1,90 @@
+<!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/trace_model/trace_model.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/core/importer/simple_line_reader.html">
+<link rel="import" href="/base/base64.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.importer', function() {
+
+  function Trace2HTMLImporter(model, events) {
+    this.importPriority = 0;
+  }
+
+  Trace2HTMLImporter.subtraces_ = [];
+
+  function _extractEventsFromHTML(text) {
+    // Clear the array before pushing data to it.
+    Trace2HTMLImporter.subtraces_ = [];
+
+    var r = new tv.c.importer.SimpleLineReader(text);
+
+    // Try to find viewer-data...
+    while (true) {
+      if (!r.advanceToLineMatching(
+          /^<\s*script id="viewer-data" type="application\/json">$/))
+        break;
+
+      r.beginSavingLines();
+      if (!r.advanceToLineMatching(/^<\/\s*script>$/))
+        return failure;
+
+      var raw_events = r.endSavingLinesAndGetResult();
+
+      // Drop off first and last event as it contains the end script tag.
+      raw_events = raw_events.slice(1, raw_events.length - 1);
+      var data64 = raw_events.join('\n');
+      var buffer = new ArrayBuffer(
+          tv.b.Base64.getDecodedBufferLength(data64));
+      var len = tv.b.Base64.DecodeToTypedArray(data64, new DataView(buffer));
+      Trace2HTMLImporter.subtraces_.push(buffer.slice(0, len));
+    }
+  }
+
+  function _canImportFromHTML(text) {
+    if (/^<!DOCTYPE HTML>/.test(text) === false)
+      return false;
+
+    // Try to find viewer-data...
+    _extractEventsFromHTML(text);
+    if (Trace2HTMLImporter.subtraces_.length === 0)
+      return false;
+    return true;
+  }
+
+  Trace2HTMLImporter.canImport = function(events) {
+    return _canImportFromHTML(events);
+  };
+
+  Trace2HTMLImporter.prototype = {
+    __proto__: tv.c.importer.Importer.prototype,
+
+    isTraceDataContainer: function() {
+      return true;
+    },
+
+    extractSubtraces: function() {
+      return Trace2HTMLImporter.subtraces_;
+    },
+
+    importEvents: function() {
+    }
+  };
+
+
+  tv.c.importer.Importer.register(Trace2HTMLImporter);
+
+
+  return {
+    Trace2HTMLImporter: Trace2HTMLImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/trace2html_importer_test.html b/trace-viewer/trace_viewer/extras/importer/trace2html_importer_test.html
new file mode 100644
index 0000000..006d9e0
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/trace2html_importer_test.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.
+-->
+
+<link rel="import" href="/extras/importer/trace2html_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  test('simple', function() {
+    var html_lines = [
+      '<!DOCTYPE HTML>',
+      '<script id="viewer-data" type="application/json">',
+      btoa('hello'),
+      '<\/script>',
+      '<script id="viewer-data" type="application/json">',
+      btoa('world'),
+      '<\/script>',
+      '</html>'
+    ];
+    var html_text = html_lines.join('\n');
+    assert.isTrue(tv.e.importer.Trace2HTMLImporter.canImport(html_text));
+
+    var m = new tv.c.TraceModel();
+    var imp = new tv.e.importer.Trace2HTMLImporter(m, html_text);
+    var subTracesAsBuffers = imp.extractSubtraces();
+    var subTracesAsStrings = subTracesAsBuffers.map(function(buffer) {
+      var str = '';
+      var ary = new Uint8Array(buffer);
+      for (var i = 0; i < ary.length; i++)
+        str += String.fromCharCode(ary[i]);
+      return str;
+    });
+    assert.deepEqual(subTracesAsStrings, ['hello', 'world']);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/trace_event_importer.html b/trace-viewer/trace_viewer/extras/importer/trace_event_importer.html
new file mode 100644
index 0000000..acf1951
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/trace_event_importer.html
@@ -0,0 +1,1661 @@
+<!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/ui/color_scheme.html">
+<link rel="import" href="/base/quad.html">
+<link rel="import" href="/base/range.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/importer/importer.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/counter_series.html">
+<link rel="import" href="/core/trace_model/slice_group.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/x_marker_annotation.html">
+<link rel="import" href="/core/trace_model/rect_annotation.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview TraceEventImporter imports TraceEvent-formatted data
+ * into the provided model.
+ */
+tv.exportTo('tv.e.importer', function() {
+
+  var Importer = tv.c.importer.Importer;
+
+  function deepCopy(value) {
+    if (!(value instanceof Object)) {
+      if (value === undefined || value === null)
+        return value;
+      if (typeof value == 'string')
+        return value.substring();
+      if (typeof value == 'boolean')
+        return value;
+      if (typeof value == 'number')
+        return value;
+      throw new Error('Unrecognized: ' + typeof value);
+    }
+
+    var object = value;
+    if (object instanceof Array) {
+      var res = new Array(object.length);
+      for (var i = 0; i < object.length; i++)
+        res[i] = deepCopy(object[i]);
+      return res;
+    }
+
+    if (object.__proto__ != Object.prototype)
+      throw new Error('Can only clone simple types');
+    var res = {};
+    for (var key in object) {
+      res[key] = deepCopy(object[key]);
+    }
+    return res;
+  }
+
+  function TraceEventImporter(model, eventData) {
+    this.importPriority = 1;
+    this.model_ = model;
+    this.events_ = undefined;
+    this.sampleEvents_ = undefined;
+    this.stackFrameEvents_ = undefined;
+    this.systemTraceEvents_ = undefined;
+    this.eventsWereFromString_ = false;
+    this.softwareMeasuredCpuCount_ = undefined;
+    this.allAsyncEvents_ = [];
+    this.allFlowEvents_ = [];
+    this.allObjectEvents_ = [];
+    this.traceEventSampleStackFramesByName_ = {};
+
+    // Dump ID -> {global: (event | undefined), process: [events]}
+    this.allMemoryDumpEvents_ = {};
+
+
+    if (typeof(eventData) === 'string' || eventData instanceof String) {
+      eventData = eventData.trim();
+      // If the event data begins with a [, then we know it should end with a ].
+      // The reason we check for this is because some tracing implementations
+      // cannot guarantee that a ']' gets written to the trace file. So, we are
+      // forgiving and if this is obviously the case, we fix it up before
+      // throwing the string at JSON.parse.
+      if (eventData[0] === '[') {
+        eventData = eventData.replace(/\s*,\s*$/, '');
+        if (eventData[eventData.length - 1] !== ']')
+          eventData = eventData + ']';
+      }
+      this.events_ = JSON.parse(eventData);
+      this.eventsWereFromString_ = true;
+    } else {
+      this.events_ = eventData;
+    }
+
+    this.traceAnnotations_ = this.events_.traceAnnotations;
+
+    // Some trace_event implementations put the actual trace events
+    // inside a container. E.g { ... , traceEvents: [ ] }
+    // If we see that, just pull out the trace events.
+    if (this.events_.traceEvents) {
+      var container = this.events_;
+      this.events_ = this.events_.traceEvents;
+
+      // Some trace_event implementations put linux_perf_importer traces as a
+      // huge string inside container.systemTraceEvents. If we see that, pull it
+      // out. It will be picked up by extractSubtraces later on.
+      this.systemTraceEvents_ = container.systemTraceEvents;
+
+      // Sampling data.
+      this.sampleEvents_ = container.samples;
+      this.stackFrameEvents_ = container.stackFrames;
+
+      // Any other fields in the container should be treated as metadata.
+      for (var fieldName in container) {
+        if (fieldName === 'traceEvents' || fieldName === 'systemTraceEvents' ||
+            fieldName === 'samples' || fieldName === 'stackFrames' ||
+            fieldName === 'traceAnnotations')
+          continue;
+        this.model_.metadata.push({name: fieldName,
+          value: container[fieldName]});
+      }
+    }
+  }
+
+  /**
+   * @return {boolean} Whether obj is a TraceEvent array.
+   */
+  TraceEventImporter.canImport = function(eventData) {
+    // May be encoded JSON. But we dont want to parse it fully yet.
+    // Use a simple heuristic:
+    //   - eventData that starts with [ are probably trace_event
+    //   - eventData that starts with { are probably trace_event
+    // May be encoded JSON. Treat files that start with { as importable by us.
+    if (typeof(eventData) === 'string' || eventData instanceof String) {
+      eventData = eventData.trim();
+      return eventData[0] == '{' || eventData[0] == '[';
+    }
+
+    // Might just be an array of events
+    if (eventData instanceof Array && eventData.length && eventData[0].ph)
+      return true;
+
+    // Might be an object with a traceEvents field in it.
+    if (eventData.traceEvents) {
+      if (eventData.traceEvents instanceof Array) {
+        if (eventData.traceEvents.length && eventData.traceEvents[0].ph)
+          return true;
+        if (eventData.samples.length && eventData.stackFrames !== undefined)
+          return true;
+      }
+    }
+
+    return false;
+  };
+
+  TraceEventImporter.prototype = {
+
+    __proto__: Importer.prototype,
+
+    extractSubtraces: function() {
+      var tmp = this.systemTraceEvents_;
+      this.systemTraceEvents_ = undefined;
+      return tmp ? [tmp] : [];
+    },
+
+    /**
+     * Deep copying is only needed if the trace was given to us as events.
+     */
+    deepCopyIfNeeded_: function(obj) {
+      if (obj === undefined)
+        obj = {};
+      if (this.eventsWereFromString_)
+        return obj;
+      return deepCopy(obj);
+    },
+
+    /**
+     * Helper to process an async event.
+     */
+    processAsyncEvent: function(event) {
+      var thread = this.model_.getOrCreateProcess(event.pid).
+          getOrCreateThread(event.tid);
+      this.allAsyncEvents_.push({
+        sequenceNumber: this.allAsyncEvents_.length,
+        event: event,
+        thread: thread});
+    },
+
+    /**
+     * Helper to process a flow event.
+     */
+    processFlowEvent: function(event) {
+      var thread = this.model_.getOrCreateProcess(event.pid).
+          getOrCreateThread(event.tid);
+      this.allFlowEvents_.push({
+        sequenceNumber: this.allFlowEvents_.length,
+        event: event,
+        thread: thread
+      });
+    },
+
+    /**
+     * Helper that creates and adds samples to a Counter object based on
+     * 'C' phase events.
+     */
+    processCounterEvent: function(event) {
+      var ctr_name;
+      if (event.id !== undefined)
+        ctr_name = event.name + '[' + event.id + ']';
+      else
+        ctr_name = event.name;
+
+      var ctr = this.model_.getOrCreateProcess(event.pid)
+          .getOrCreateCounter(event.cat, ctr_name);
+
+      // Initialize the counter's series fields if needed.
+      if (ctr.numSeries === 0) {
+        for (var seriesName in event.args) {
+          ctr.addSeries(new tv.c.trace_model.CounterSeries(
+              seriesName,
+              tv.b.ui.getColorIdForGeneralPurposeString(
+                  ctr.name + '.' + seriesName)));
+        }
+
+        if (ctr.numSeries === 0) {
+          this.model_.importWarning({
+            type: 'counter_parse_error',
+            message: 'Expected counter ' + event.name +
+                ' to have at least one argument to use as a value.'
+          });
+
+          // Drop the counter.
+          delete ctr.parent.counters[ctr.name];
+          return;
+        }
+      }
+
+      var ts = event.ts / 1000;
+      ctr.series.forEach(function(series) {
+        var val = event.args[series.name] ? event.args[series.name] : 0;
+        series.addCounterSample(ts, val);
+      });
+    },
+
+    processObjectEvent: function(event) {
+      var thread = this.model_.getOrCreateProcess(event.pid).
+          getOrCreateThread(event.tid);
+      this.allObjectEvents_.push({
+        sequenceNumber: this.allObjectEvents_.length,
+        event: event,
+        thread: thread});
+    },
+
+    processDurationEvent: function(event) {
+      var thread = this.model_.getOrCreateProcess(event.pid)
+        .getOrCreateThread(event.tid);
+      if (!thread.sliceGroup.isTimestampValidForBeginOrEnd(event.ts / 1000)) {
+        this.model_.importWarning({
+          type: 'duration_parse_error',
+          message: 'Timestamps are moving backward.'
+        });
+        return;
+      }
+
+      if (event.ph == 'B') {
+        var slice = thread.sliceGroup.beginSlice(
+            event.cat, event.name, event.ts / 1000,
+            this.deepCopyIfNeeded_(event.args),
+            event.tts / 1000);
+        slice.startStackFrame = this.getStackFrameForEvent_(event);
+      } else if (event.ph == 'I' || event.ph == 'i') {
+        if (event.s !== undefined && event.s !== 't')
+          throw new Error('This should never happen');
+
+        thread.sliceGroup.beginSlice(event.cat, event.name, event.ts / 1000,
+                                     this.deepCopyIfNeeded_(event.args),
+                                     event.tts / 1000);
+        var slice = thread.sliceGroup.endSlice(event.ts / 1000,
+                                   event.tts / 1000);
+        slice.startStackFrame = this.getStackFrameForEvent_(event);
+        slice.endStackFrame = undefined;
+      } else {
+        if (!thread.sliceGroup.openSliceCount) {
+          this.model_.importWarning({
+            type: 'duration_parse_error',
+            message: 'E phase event without a matching B phase event.'
+          });
+          return;
+        }
+
+        var slice = thread.sliceGroup.endSlice(event.ts / 1000,
+                                               event.tts / 1000);
+        if (event.name && slice.title != event.name) {
+          this.model_.importWarning({
+            type: 'title_match_error',
+            message: 'Titles do not match. Title is ' +
+                slice.title + ' in openSlice, and is ' +
+                event.name + ' in endSlice'
+          });
+        }
+        slice.endStackFrame = this.getStackFrameForEvent_(event);
+
+        this.mergeArgsInto_(slice.args, event.args, slice.title);
+      }
+    },
+
+    mergeArgsInto_: function(dstArgs, srcArgs, eventName) {
+      for (var arg in srcArgs) {
+        if (dstArgs[arg] !== undefined) {
+          this.model_.importWarning({
+            type: 'arg_merge_error',
+            message: 'Different phases of ' + eventName +
+                ' provided values for argument ' + arg + '.' +
+                ' The last provided value will be used.'
+          });
+        }
+        dstArgs[arg] = this.deepCopyIfNeeded_(srcArgs[arg]);
+      }
+    },
+
+    processCompleteEvent: function(event) {
+      var thread = this.model_.getOrCreateProcess(event.pid)
+          .getOrCreateThread(event.tid);
+      var slice = thread.sliceGroup.pushCompleteSlice(event.cat, event.name,
+          event.ts / 1000,
+          event.dur === undefined ? undefined : event.dur / 1000,
+          event.tts === undefined ? undefined : event.tts / 1000,
+          event.tdur === undefined ? undefined : event.tdur / 1000,
+          this.deepCopyIfNeeded_(event.args));
+      slice.startStackFrame = this.getStackFrameForEvent_(event);
+      slice.endStackFrame = this.getStackFrameForEvent_(event, true);
+    },
+
+    processMetadataEvent: function(event) {
+      if (event.name == 'process_name') {
+        var process = this.model_.getOrCreateProcess(event.pid);
+        process.name = event.args.name;
+      } else if (event.name == 'process_labels') {
+        var process = this.model_.getOrCreateProcess(event.pid);
+        var labels = event.args.labels.split(',');
+        for (var i = 0; i < labels.length; i++)
+          process.addLabelIfNeeded(labels[i]);
+      } else if (event.name == 'process_sort_index') {
+        var process = this.model_.getOrCreateProcess(event.pid);
+        process.sortIndex = event.args.sort_index;
+      } else if (event.name == 'thread_name') {
+        var thread = this.model_.getOrCreateProcess(event.pid).
+            getOrCreateThread(event.tid);
+        thread.name = event.args.name;
+      } else if (event.name == 'thread_sort_index') {
+        var thread = this.model_.getOrCreateProcess(event.pid).
+            getOrCreateThread(event.tid);
+        thread.sortIndex = event.args.sort_index;
+      } else if (event.name == 'num_cpus') {
+        var n = event.args.number;
+        // Not all render processes agree on the cpu count in trace_event. Some
+        // processes will report 1, while others will report the actual cpu
+        // count. To deal with this, take the max of what is reported.
+        if (this.softwareMeasuredCpuCount_ !== undefined)
+          n = Math.max(n, this.softwareMeasuredCpuCount_);
+        this.softwareMeasuredCpuCount_ = n;
+      } else {
+        this.model_.importWarning({
+          type: 'metadata_parse_error',
+          message: 'Unrecognized metadata name: ' + event.name
+        });
+      }
+    },
+
+    processInstantEvent: function(event) {
+      // Thread-level instant events are treated as zero-duration slices.
+      if (event.s == 't' || event.s === undefined) {
+        this.processDurationEvent(event);
+        return;
+      }
+
+      var constructor;
+      switch (event.s) {
+        case 'g':
+          constructor = tv.c.trace_model.GlobalInstantEvent;
+          break;
+        case 'p':
+          constructor = tv.c.trace_model.ProcessInstantEvent;
+          break;
+        default:
+          this.model_.importWarning({
+            type: 'instant_parse_error',
+            message: 'I phase event with unknown "s" field value.'
+          });
+          return;
+      }
+
+      var colorId = tv.b.ui.getColorIdForGeneralPurposeString(event.name);
+      var instantEvent = new constructor(event.cat, event.name,
+          colorId, event.ts / 1000, this.deepCopyIfNeeded_(event.args));
+
+      switch (instantEvent.type) {
+        case tv.c.trace_model.InstantEventType.GLOBAL:
+          this.model_.pushInstantEvent(instantEvent);
+          break;
+
+        case tv.c.trace_model.InstantEventType.PROCESS:
+          var process = this.model_.getOrCreateProcess(event.pid);
+          process.pushInstantEvent(instantEvent);
+          break;
+
+        default:
+          throw new Error('Unknown instant event type: ' + event.s);
+      }
+    },
+
+    processTraceSampleEvent: function(event) {
+      var thread = this.model_.getOrCreateProcess(event.pid)
+        .getOrCreateThread(event.tid);
+
+      var stackFrame = this.getStackFrameForEvent_(event);
+      if (stackFrame === undefined) {
+        stackFrame = this.traceEventSampleStackFramesByName_[
+            event.name];
+      }
+      if (stackFrame === undefined) {
+        var id = 'te-' + tv.b.GUID.allocate();
+        stackFrame = new tv.c.trace_model.StackFrame(
+            undefined, id,
+            event.cat, event.name,
+            tv.b.ui.getColorIdForGeneralPurposeString(event.name));
+        this.model_.addStackFrame(stackFrame);
+        this.traceEventSampleStackFramesByName_[event.name] = stackFrame;
+      }
+
+      var sample = new tv.c.trace_model.Sample(
+          undefined, thread, 'TRACE_EVENT_SAMPLE',
+          event.ts / 1000, stackFrame, 1,
+          this.deepCopyIfNeeded_(event.args));
+      this.model_.samples.push(sample);
+    },
+
+    getOrCreateMemoryDumpEvents_: function(dumpId) {
+      if (this.allMemoryDumpEvents_[dumpId] === undefined) {
+        this.allMemoryDumpEvents_[dumpId] = {
+          global: undefined,
+          process: []
+        };
+      }
+      return this.allMemoryDumpEvents_[dumpId];
+    },
+
+    processMemoryDumpEvent: function(event) {
+      if (event.id === undefined) {
+        this.model_.importWarning({
+          type: 'memory_dump_parse_error',
+          message: event.ph + ' phase event without a dump ID.'
+        });
+        return;
+      }
+      var events = this.getOrCreateMemoryDumpEvents_(event.id);
+
+      if (event.ph === 'v') {
+        // Add a process memory dump.
+        events.process.push(event);
+      } else if (event.ph === 'V') {
+        // Add a global memory dump (unless already present).
+        if (events.global !== undefined) {
+          this.model_.importWarning({
+            type: 'memory_dump_parse_error',
+            message: 'Multiple V phase events with the same dump ID.'
+          });
+          return;
+        }
+        events.global = event;
+      } else {
+        throw new Error('Invalid memory dump event phase "' + event.ph + '".');
+      }
+    },
+
+    /**
+     * Walks through the events_ list and outputs the structures discovered to
+     * model_.
+     */
+    importEvents: function() {
+      var csr = new tv.c.ClockSyncRecord('linux_perf_importer', 0, {});
+      this.model_.clockSyncRecords.push(csr);
+      if (this.stackFrameEvents_)
+        this.importStackFrames_();
+
+      if (this.traceAnnotations_)
+        this.importAnnotations_();
+
+      var events = this.events_;
+      for (var eI = 0; eI < events.length; eI++) {
+        var event = events[eI];
+        if (event.ph === 'B' || event.ph === 'E') {
+          this.processDurationEvent(event);
+
+        } else if (event.ph === 'X') {
+          this.processCompleteEvent(event);
+
+        } else if (event.ph === 'b' || event.ph === 'e' || event.ph === 'n' ||
+                   event.ph === 'S' || event.ph === 'F' || event.ph === 'T' ||
+                   event.ph === 'p') {
+          this.processAsyncEvent(event);
+
+        // Note, I is historic. The instant event marker got changed, but we
+        // want to support loading old trace files so we have both I and i.
+        } else if (event.ph == 'I' || event.ph == 'i') {
+          this.processInstantEvent(event);
+
+        } else if (event.ph == 'P') {
+          this.processTraceSampleEvent(event);
+
+        } else if (event.ph == 'C') {
+          this.processCounterEvent(event);
+
+        } else if (event.ph == 'M') {
+          this.processMetadataEvent(event);
+
+        } else if (event.ph === 'N' || event.ph === 'D' || event.ph === 'O') {
+          this.processObjectEvent(event);
+
+        } else if (event.ph === 's' || event.ph === 't' || event.ph === 'f') {
+          this.processFlowEvent(event);
+
+        } else if (event.ph === 'v' || event.ph === 'V') {
+          this.processMemoryDumpEvent(event);
+
+        } else {
+          this.model_.importWarning({
+            type: 'parse_error',
+            message: 'Unrecognized event phase: ' +
+                event.ph + ' (' + event.name + ')'
+          });
+        }
+      }
+    },
+
+    importStackFrames_: function() {
+      var m = this.model_;
+      var events = this.stackFrameEvents_;
+
+      for (var id in events) {
+        var event = events[id];
+        var textForColor = event.category ? event.category : event.name;
+        var frame = new tv.c.trace_model.StackFrame(
+            undefined, 'g' + id,
+            event.category, event.name,
+            tv.b.ui.getColorIdForGeneralPurposeString(textForColor));
+        m.addStackFrame(frame);
+      }
+      for (var id in events) {
+        var event = events[id];
+        if (event.parent === undefined)
+          continue;
+
+        var frame = m.stackFrames['g' + id];
+        if (frame === undefined)
+          throw new Error('omg');
+        var parentFrame;
+        if (event.parent === undefined) {
+          parentFrame = undefined;
+        } else {
+          parentFrame = m.stackFrames['g' + event.parent];
+          if (parentFrame === undefined)
+            throw new Error('omg');
+        }
+        frame.parentFrame = parentFrame;
+      }
+    },
+
+    importAnnotations_: function() {
+      for (var id in this.traceAnnotations_) {
+        var annotation = tv.c.trace_model.Annotation.fromDictIfPossible(
+           this.traceAnnotations_[id]);
+        if (!annotation) {
+          this.model_.importWarning({
+            type: 'annotation_warning',
+            message: 'Unrecognized traceAnnotation typeName \"' +
+                this.traceAnnotations_[id].typeName + '\"'
+          });
+          continue;
+        }
+        this.model_.addAnnotation(annotation);
+      }
+    },
+
+    /**
+     * Called by the Model after all other importers have imported their
+     * events.
+     */
+    finalizeImport: function() {
+      if (this.softwareMeasuredCpuCount_ !== undefined) {
+        this.model_.kernel.softwareMeasuredCpuCount =
+            this.softwareMeasuredCpuCount_;
+      }
+      this.createAsyncSlices_();
+      this.createFlowSlices_();
+      this.createExplicitObjects_();
+      this.createImplicitObjects_();
+      this.createMemoryDumps_();
+    },
+
+    /* Events can have one or more stack frames associated with them, but
+     * that frame might be encoded either as a stack trace of program counters,
+     * or as a direct stack frame reference. This handles either case and
+     * if found, returns the stackframe.
+     */
+    getStackFrameForEvent_: function(event, opt_lookForEndEvent) {
+      var sf;
+      var stack;
+      if (opt_lookForEndEvent) {
+        sf = event.esf;
+        stack = event.estack;
+      } else {
+        sf = event.sf;
+        stack = event.stack;
+      }
+      if (stack !== undefined && sf !== undefined) {
+        this.model_.importWarning({
+          type: 'stack_frame_and_stack_error',
+          message: 'Event at ' + event.ts +
+              ' cannot have both a stack and a stackframe.'
+        });
+        return undefined;
+      }
+
+      if (stack !== undefined)
+        return this.model_.resolveStackToStackFrame_(event.pid, stack);
+      if (sf === undefined)
+        return undefined;
+
+      var stackFrame = this.model_.stackFrames['g' + sf];
+      if (stackFrame === undefined) {
+        this.model_.importWarning({
+          type: 'sample_import_error',
+          message: 'No frame for ' + sf
+        });
+        return;
+      }
+      return stackFrame;
+    },
+
+    resolveStackToStackFrame_: function(pid, stack) {
+      // TODO(alph,fmeawad): Add codemap resolution code here.
+      return undefined;
+    },
+
+    importSampleData: function() {
+      if (!this.sampleEvents_)
+        return;
+      var m = this.model_;
+
+      // If this is the only importer, then fake-create the threads.
+      var events = this.sampleEvents_;
+      if (this.events_.length === 0) {
+        for (var i = 0; i < events.length; i++) {
+          var event = events[i];
+          m.getOrCreateProcess(event.tid).getOrCreateThread(event.tid);
+        }
+      }
+
+      var threadsByTid = {};
+      m.getAllThreads().forEach(function(t) {
+        threadsByTid[t.tid] = t;
+      });
+
+      for (var i = 0; i < events.length; i++) {
+        var event = events[i];
+        var thread = threadsByTid[event.tid];
+        if (thread === undefined) {
+          m.importWarning({
+            type: 'sample_import_error',
+            message: 'Thread ' + events.tid + 'not found'
+          });
+          continue;
+        }
+
+        var cpu;
+        if (event.cpu !== undefined)
+          cpu = m.kernel.getOrCreateCpu(event.cpu);
+
+        var stackFrame = this.getStackFrameForEvent_(event);
+
+        var sample = new tv.c.trace_model.Sample(
+            cpu, thread,
+            event.name, event.ts / 1000,
+            stackFrame,
+            event.weight);
+        m.samples.push(sample);
+      }
+    },
+
+    /**
+     * Called by the model to join references between objects, after final model
+     * bounds have been computed.
+     */
+    joinRefs: function() {
+      this.joinObjectRefs_();
+    },
+
+    createAsyncSlices_: function() {
+      if (this.allAsyncEvents_.length === 0)
+        return;
+
+      this.allAsyncEvents_.sort(function(x, y) {
+        var d = x.event.ts - y.event.ts;
+        if (d !== 0)
+          return d;
+        return x.sequenceNumber - y.sequenceNumber;
+      });
+
+      var legacyEvents = [];
+      // Group nestable async events by ID. Events with the same ID should
+      // belong to the same parent async event.
+      var nestableAsyncEventsByKey = {};
+      for (var i = 0; i < this.allAsyncEvents_.length; i++) {
+        var asyncEventState = this.allAsyncEvents_[i];
+        var event = asyncEventState.event;
+        if (event.ph === 'S' || event.ph === 'F' || event.ph === 'T' ||
+            event.ph === 'p') {
+          legacyEvents.push(asyncEventState);
+          continue;
+        }
+        if (event.cat === undefined) {
+          this.model_.importWarning({
+            type: 'async_slice_parse_error',
+            message: 'Nestable async events (ph: b, e, or n) require a ' +
+                'cat parameter.'
+          });
+          continue;
+        }
+
+        if (event.name === undefined) {
+          this.model_.importWarning({
+            type: 'async_slice_parse_error',
+            message: 'Nestable async events (ph: b, e, or n) require a ' +
+                'name parameter.'
+          });
+          continue;
+        }
+
+        if (event.id === undefined) {
+          this.model_.importWarning({
+            type: 'async_slice_parse_error',
+            message: 'Nestable async events (ph: b, e, or n) require an ' +
+                'id parameter.'
+          });
+          continue;
+        }
+        var key = event.cat + ':' + event.id;
+        if (nestableAsyncEventsByKey[key] === undefined)
+           nestableAsyncEventsByKey[key] = [];
+        nestableAsyncEventsByKey[key].push(asyncEventState);
+      }
+      // Handle legacy async events.
+      this.createLegacyAsyncSlices_(legacyEvents);
+
+      // Parse nestable async events into AsyncSlices.
+      for (var key in nestableAsyncEventsByKey) {
+        var eventStateEntries = nestableAsyncEventsByKey[key];
+        // Stack of enclosing BEGIN events.
+        var parentStack = [];
+        for (var i = 0; i < eventStateEntries.length; ++i) {
+          var eventStateEntry = eventStateEntries[i];
+          // If this is the end of an event, match it to the start.
+          if (eventStateEntry.event.ph === 'e') {
+            // Walk up the parent stack to find the corresponding BEGIN for
+            // this END.
+            var parentIndex = -1;
+            for (var k = parentStack.length - 1; k >= 0; --k) {
+              if (parentStack[k].event.name === eventStateEntry.event.name) {
+                parentIndex = k;
+                break;
+              }
+            }
+            if (parentIndex === -1) {
+              // Unmatched end.
+              eventStateEntry.finished = false;
+            } else {
+              parentStack[parentIndex].end = eventStateEntry;
+              // Pop off all enclosing unmatched BEGINs util parentIndex.
+              while (parentIndex < parentStack.length) {
+                parentStack.pop();
+              }
+            }
+          }
+          // Inherit the current parent.
+          if (parentStack.length > 0)
+            eventStateEntry.parentEntry = parentStack[parentStack.length - 1];
+          if (eventStateEntry.event.ph === 'b')
+            parentStack.push(eventStateEntry);
+        }
+        var topLevelSlices = [];
+        for (var i = 0; i < eventStateEntries.length; ++i) {
+          var eventStateEntry = eventStateEntries[i];
+          // Skip matched END, as its slice will be created when we
+          // encounter its corresponding BEGIN.
+          if (eventStateEntry.event.ph === 'e' &&
+              eventStateEntry.finished === undefined) {
+            continue;
+          }
+          var startState = undefined;
+          var endState = undefined;
+          var sliceArgs = undefined;
+          var sliceError = undefined;
+          if (eventStateEntry.event.ph === 'n') {
+            startState = eventStateEntry;
+            endState = eventStateEntry;
+            sliceArgs = eventStateEntry.event.args;
+          } else if (eventStateEntry.event.ph === 'b') {
+            if (eventStateEntry.end === undefined) {
+              // Unmatched BEGIN. End it when last event with this ID ends.
+              eventStateEntry.end =
+                  eventStateEntries[eventStateEntries.length - 1];
+              sliceError =
+                  'Slice has no matching END. End time has been adjusted.';
+              this.model_.importWarning({
+                type: 'async_slice_parse_error',
+                message: 'Nestable async BEGIN event at ' +
+                    eventStateEntry.event.ts + ' with name=' +
+                    eventStateEntry.event.name +
+                    ' and id=' + eventStateEntry.event.id + ' was unmatched.'
+              });
+              sliceArgs = eventStateEntry.event.args;
+            } else {
+              // Include args for both END and BEGIN for a matched pair.
+              var concatenateArguments = function(args1, args2) {
+                if (args1.params === undefined || args2.params === undefined)
+                  return tv.b.concatenateObjects(args1, args2);
+                // Make an argument object to hold the combined params.
+                var args3 = {};
+                args3.params = tv.b.concatenateObjects(args1.params,
+                                                       args2.params);
+                return tv.b.concatenateObjects(args1, args2, args3);
+              }
+              sliceArgs = concatenateArguments(eventStateEntry.event.args,
+                                               eventStateEntry.end.event.args);
+            }
+            startState = eventStateEntry;
+            endState = eventStateEntry.end;
+          } else {
+            // Unmatched END. Start it at the first event with this ID starts.
+            sliceError =
+                'Slice has no matching BEGIN. Start time has been adjusted.';
+            this.model_.importWarning({
+              type: 'async_slice_parse_error',
+              message: 'Nestable async END event at ' +
+                  eventStateEntry.event.ts + ' with name=' +
+                  eventStateEntry.event.name +
+                  ' and id=' + eventStateEntry.event.id + ' was unmatched.'
+            });
+            startState = eventStateEntries[0];
+            endState = eventStateEntry;
+            sliceArgs = eventStateEntry.event.args;
+          }
+
+          var isTopLevel = (eventStateEntry.parentEntry === undefined);
+          var asyncSliceConstructor =
+             tv.c.trace_model.AsyncSlice.getConstructor(
+                eventStateEntry.event.cat,
+                eventStateEntry.event.name);
+          var slice = new asyncSliceConstructor(
+              eventStateEntry.event.cat,
+              eventStateEntry.event.name,
+              tv.b.ui.getColorIdForGeneralPurposeString(
+                  eventStateEntry.event.name),
+              startState.event.ts / 1000,
+              sliceArgs,
+              (endState.event.ts - startState.event.ts) / 1000,
+              isTopLevel);
+
+          slice.startThread = startState.thread;
+          slice.endThread = endState.thread;
+          slice.id = key;
+          if (sliceError !== undefined)
+            slice.error = sliceError;
+          eventStateEntry.slice = slice;
+          // Add the slice to the topLevelSlices array if there is no parent.
+          // Otherwise, add the slice to the subSlices of its parent.
+          if (isTopLevel) {
+            topLevelSlices.push(slice);
+          } else if (eventStateEntry.parentEntry.slice !== undefined) {
+            if (eventStateEntry.parentEntry.slice.subSlices === undefined)
+              eventStateEntry.parentEntry.slice.subSlices = [];
+            eventStateEntry.parentEntry.slice.subSlices.push(slice);
+          }
+        }
+        for (var si = 0; si < topLevelSlices.length; si++) {
+          topLevelSlices[si].startThread.asyncSliceGroup.push(
+              topLevelSlices[si]);
+        }
+      }
+    },
+
+    createLegacyAsyncSlices_: function(legacyEvents) {
+      if (legacyEvents.length === 0)
+        return;
+
+      legacyEvents.sort(function(x, y) {
+        var d = x.event.ts - y.event.ts;
+        if (d != 0)
+          return d;
+        return x.sequenceNumber - y.sequenceNumber;
+      });
+
+      var asyncEventStatesByNameThenID = {};
+
+      for (var i = 0; i < legacyEvents.length; i++) {
+        var asyncEventState = legacyEvents[i];
+
+        var event = asyncEventState.event;
+        var name = event.name;
+        if (name === undefined) {
+          this.model_.importWarning({
+            type: 'async_slice_parse_error',
+            message: 'Async events (ph: S, T, p, or F) require a name ' +
+                ' parameter.'
+          });
+          continue;
+        }
+
+        var id = event.id;
+        if (id === undefined) {
+          this.model_.importWarning({
+            type: 'async_slice_parse_error',
+            message: 'Async events (ph: S, T, p, or F) require an id parameter.'
+          });
+          continue;
+        }
+
+        // TODO(simonjam): Add a synchronous tick on the appropriate thread.
+
+        if (event.ph === 'S') {
+          if (asyncEventStatesByNameThenID[name] === undefined)
+            asyncEventStatesByNameThenID[name] = {};
+          if (asyncEventStatesByNameThenID[name][id]) {
+            this.model_.importWarning({
+              type: 'async_slice_parse_error',
+              message: 'At ' + event.ts + ', a slice of the same id ' + id +
+                  ' was alrady open.'
+            });
+            continue;
+          }
+          asyncEventStatesByNameThenID[name][id] = [];
+          asyncEventStatesByNameThenID[name][id].push(asyncEventState);
+        } else {
+          if (asyncEventStatesByNameThenID[name] === undefined) {
+            this.model_.importWarning({
+              type: 'async_slice_parse_error',
+              message: 'At ' + event.ts + ', no slice named ' + name +
+                  ' was open.'
+            });
+            continue;
+          }
+          if (asyncEventStatesByNameThenID[name][id] === undefined) {
+            this.model_.importWarning({
+              type: 'async_slice_parse_error',
+              message: 'At ' + event.ts + ', no slice named ' + name +
+                  ' with id=' + id + ' was open.'
+            });
+            continue;
+          }
+          var events = asyncEventStatesByNameThenID[name][id];
+          events.push(asyncEventState);
+
+          if (event.ph === 'F') {
+            // Create a slice from start to end.
+            var asyncSliceConstructor =
+               tv.c.trace_model.AsyncSlice.getConstructor(
+                  events[0].event.cat,
+                  name);
+            var slice = new asyncSliceConstructor(
+                events[0].event.cat,
+                name,
+                tv.b.ui.getColorIdForGeneralPurposeString(name),
+                events[0].event.ts / 1000,
+                tv.b.concatenateObjects(events[0].event.args,
+                                      events[events.length - 1].event.args),
+                (event.ts - events[0].event.ts) / 1000,
+                true);
+            slice.startThread = events[0].thread;
+            slice.endThread = asyncEventState.thread;
+            slice.id = id;
+            slice.subSlices = [];
+
+            var stepType = events[1].event.ph;
+            var isValid = true;
+
+            // Create subSlices for each step. Skip the start and finish events,
+            // which are always first and last respectively.
+            for (var j = 1; j < events.length - 1; ++j) {
+              if (events[j].event.ph === 'T' || events[j].event.ph === 'p') {
+                isValid = this.assertStepTypeMatches_(stepType, events[j]);
+                if (!isValid)
+                  break;
+              }
+
+              if (events[j].event.ph === 'S') {
+                this.model_.importWarning({
+                  type: 'async_slice_parse_error',
+                  message: 'At ' + event.event.ts + ', a slice named ' +
+                      event.event.name + ' with id=' + event.event.id +
+                      ' had a step before the start event.'
+                });
+                continue;
+              }
+
+              if (events[j].event.ph === 'F') {
+                this.model_.importWarning({
+                  type: 'async_slice_parse_error',
+                  message: 'At ' + event.event.ts + ', a slice named ' +
+                      event.event.name + ' with id=' + event.event.id +
+                      ' had a step after the finish event.'
+                });
+                continue;
+              }
+
+              var startIndex = j + (stepType === 'T' ? 0 : -1);
+              var endIndex = startIndex + 1;
+
+              var subName = events[j].event.name;
+              if (events[j].event.ph === 'T' || events[j].event.ph === 'p')
+                subName = subName + ':' + events[j].event.args.step;
+
+              var asyncSliceConstructor =
+                 tv.c.trace_model.AsyncSlice.getConstructor(
+                    events[0].event.cat,
+                    subName);
+              var subSlice = new asyncSliceConstructor(
+                  events[0].event.cat,
+                  subName,
+                  tv.b.ui.getColorIdForGeneralPurposeString(subName + j),
+                  events[startIndex].event.ts / 1000,
+                  this.deepCopyIfNeeded_(events[j].event.args),
+                  (events[endIndex].event.ts - events[startIndex].event.ts) /
+                      1000);
+              subSlice.startThread = events[startIndex].thread;
+              subSlice.endThread = events[endIndex].thread;
+              subSlice.id = id;
+
+              slice.subSlices.push(subSlice);
+            }
+
+            if (isValid) {
+              // Add |slice| to the start-thread's asyncSlices.
+              slice.startThread.asyncSliceGroup.push(slice);
+            }
+
+            delete asyncEventStatesByNameThenID[name][id];
+          }
+        }
+      }
+    },
+
+    assertStepTypeMatches_: function(stepType, event) {
+      if (stepType != event.event.ph) {
+        this.model_.importWarning({
+          type: 'async_slice_parse_error',
+          message: 'At ' + event.event.ts + ', a slice named ' +
+              event.event.name + ' with id=' + event.event.id +
+              ' had both begin and end steps, which is not allowed.'
+        });
+        return false;
+      }
+      return true;
+    },
+
+    createFlowSlices_: function() {
+      if (this.allFlowEvents_.length === 0)
+        return;
+
+      var that = this;
+
+      function validateFlowEvent() {
+        if (event.name === undefined) {
+          that.model_.importWarning({
+            type: 'flow_slice_parse_error',
+            message: 'Flow events (ph: s, t or f) require a name parameter.'
+          });
+          return false;
+        }
+
+        if (event.id === undefined) {
+          that.model_.importWarning({
+            type: 'flow_slice_parse_error',
+            message: 'Flow events (ph: s, t or f) require an id parameter.'
+          });
+          return false;
+        }
+        return true;
+      }
+
+      function createFlowEvent(thread, event) {
+        // TODO(nduca): Figure out startSlice from ts and binding point,
+        // instead of making a join point.
+        var startSlice = new tv.c.trace_model.ThreadSlice(
+            event.cat,
+            event.name,
+            tv.b.ui.getColorIdForGeneralPurposeString(event.name),
+            event.ts / 1000,
+            that.deepCopyIfNeeded_(event.args));
+        startSlice.duration = 0;
+
+        thread.sliceGroup.pushSlice(startSlice);
+
+        var flowEvent = new tv.c.trace_model.FlowEvent(
+            event.cat,
+            event.id,
+            event.name,
+            tv.b.ui.getColorIdForGeneralPurposeString(event.name),
+            event.ts / 1000,
+            that.deepCopyIfNeeded_(event.args));
+        flowEvent.startSlice = startSlice;
+        return flowEvent;
+      }
+
+      function finishFlowEventWith(flowEvent, thread, event) {
+        // TODO(nduca): Figure out endSlice from ts and binding point.
+        var endSlice = new tv.c.trace_model.ThreadSlice(
+            event.cat,
+            event.name,
+            tv.b.ui.getColorIdForGeneralPurposeString(event.name),
+            event.ts / 1000,
+            that.deepCopyIfNeeded_(event.args));
+        endSlice.duration = 0;
+
+        thread.sliceGroup.pushSlice(endSlice);
+
+        // Modify the flowEvent with the new data.
+        flowEvent.endSlice = endSlice;
+        flowEvent.duration = (event.ts / 1000) - flowEvent.start;
+        that.mergeArgsInto_(flowEvent.args, event.args, flowEvent.title);
+      }
+
+      // Actual import.
+      this.allFlowEvents_.sort(function(x, y) {
+        var d = x.event.ts - y.event.ts;
+        if (d != 0)
+          return d;
+        return x.sequenceNumber - y.sequenceNumber;
+      });
+
+      var flowIdToEvent = {};
+      for (var i = 0; i < this.allFlowEvents_.length; ++i) {
+        var data = this.allFlowEvents_[i];
+        var event = data.event;
+        var thread = data.thread;
+        if (!validateFlowEvent(event))
+          continue;
+
+        var flowEvent;
+        if (event.ph === 's') {
+          if (flowIdToEvent[event.id]) {
+            this.model_.importWarning({
+              type: 'flow_slice_start_error',
+              message: 'event id ' + event.id + ' already seen when ' +
+                  'encountering start of flow event.'});
+            continue;
+          }
+          flowEvent = createFlowEvent(thread, event);
+          flowIdToEvent[event.id] = flowEvent;
+
+        } else if (event.ph === 't' || event.ph === 'f') {
+          flowEvent = flowIdToEvent[event.id];
+          if (flowEvent === undefined) {
+            this.model_.importWarning({
+              type: 'flow_slice_ordering_error',
+              message: 'Found flow phase ' + event.ph + ' for id: ' + event.id +
+                  ' but no flow start found.'
+            });
+            continue;
+          }
+
+          finishFlowEventWith(flowEvent, thread, event);
+
+          that.model_.flowEvents.push(flowEvent);
+
+          flowIdToEvent[event.id] = undefined;
+
+          // If this is a step, then create another flow event.
+          if (event.ph === 't') {
+            flowEvent = createFlowEvent(thread, event);
+            flowIdToEvent[event.id] = flowEvent;
+          }
+        }
+      }
+    },
+
+    /**
+     * This function creates objects described via the N, D, and O phase
+     * events.
+     */
+    createExplicitObjects_: function() {
+      if (this.allObjectEvents_.length == 0)
+        return;
+
+      function processEvent(objectEventState) {
+        var event = objectEventState.event;
+        var thread = objectEventState.thread;
+        if (event.name === undefined) {
+          this.model_.importWarning({
+            type: 'object_parse_error',
+            message: 'While processing ' + JSON.stringify(event) + ': ' +
+                'Object events require an name parameter.'
+          });
+        }
+
+        if (event.id === undefined) {
+          this.model_.importWarning({
+            type: 'object_parse_error',
+            message: 'While processing ' + JSON.stringify(event) + ': ' +
+                'Object events require an id parameter.'
+          });
+        }
+        var process = thread.parent;
+        var ts = event.ts / 1000;
+        var instance;
+        if (event.ph == 'N') {
+          try {
+            instance = process.objects.idWasCreated(
+                event.id, event.cat, event.name, ts);
+          } catch (e) {
+            this.model_.importWarning({
+              type: 'object_parse_error',
+              message: 'While processing create of ' +
+                  event.id + ' at ts=' + ts + ': ' + e
+            });
+            return;
+          }
+        } else if (event.ph == 'O') {
+          if (event.args.snapshot === undefined) {
+            this.model_.importWarning({
+              type: 'object_parse_error',
+              message: 'While processing ' + event.id + ' at ts=' + ts + ': ' +
+                  'Snapshots must have args: {snapshot: ...}'
+            });
+            return;
+          }
+          var snapshot;
+          try {
+            var args = this.deepCopyIfNeeded_(event.args.snapshot);
+            var cat;
+            if (args.cat) {
+              cat = args.cat;
+              delete args.cat;
+            } else {
+              cat = event.cat;
+            }
+
+            var baseTypename;
+            if (args.base_type) {
+              baseTypename = args.base_type;
+              delete args.base_type;
+            } else {
+              baseTypename = undefined;
+            }
+            snapshot = process.objects.addSnapshot(
+                event.id, cat, event.name, ts,
+                args, baseTypename);
+            snapshot.snapshottedOnThread = thread;
+          } catch (e) {
+            this.model_.importWarning({
+              type: 'object_parse_error',
+              message: 'While processing snapshot of ' +
+                  event.id + ' at ts=' + ts + ': ' + e
+            });
+            return;
+          }
+          instance = snapshot.objectInstance;
+        } else if (event.ph == 'D') {
+          try {
+            instance = process.objects.idWasDeleted(
+                event.id, event.cat, event.name, ts);
+          } catch (e) {
+            this.model_.importWarning({
+              type: 'object_parse_error',
+              message: 'While processing delete of ' +
+                  event.id + ' at ts=' + ts + ': ' + e
+            });
+            return;
+          }
+        }
+
+        if (instance) {
+          instance.colorId = tv.b.ui.getColorIdForGeneralPurposeString(
+              instance.typeName);
+        }
+      }
+
+      this.allObjectEvents_.sort(function(x, y) {
+        var d = x.event.ts - y.event.ts;
+        if (d != 0)
+          return d;
+        return x.sequenceNumber - y.sequenceNumber;
+      });
+
+      var allObjectEvents = this.allObjectEvents_;
+      for (var i = 0; i < allObjectEvents.length; i++) {
+        var objectEventState = allObjectEvents[i];
+        try {
+          processEvent.call(this, objectEventState);
+        } catch (e) {
+          this.model_.importWarning({
+            type: 'object_parse_error',
+            message: e.message
+          });
+        }
+      }
+    },
+
+    createImplicitObjects_: function() {
+      tv.b.iterItems(this.model_.processes, function(pid, process) {
+        this.createImplicitObjectsForProcess_(process);
+      }, this);
+    },
+
+    // Here, we collect all the snapshots that internally contain a
+    // Javascript-level object inside their args list that has an "id" field,
+    // and turn that into a snapshot of the instance referred to by id.
+    createImplicitObjectsForProcess_: function(process) {
+
+      function processField(referencingObject,
+                            referencingObjectFieldName,
+                            referencingObjectFieldValue,
+                            containingSnapshot) {
+        if (!referencingObjectFieldValue)
+          return;
+
+        if (referencingObjectFieldValue instanceof
+            tv.c.trace_model.ObjectSnapshot)
+          return null;
+        if (referencingObjectFieldValue.id === undefined)
+          return;
+
+        var implicitSnapshot = referencingObjectFieldValue;
+
+        var rawId = implicitSnapshot.id;
+        var m = /(.+)\/(.+)/.exec(rawId);
+        if (!m)
+          throw new Error('Implicit snapshots must have names.');
+        delete implicitSnapshot.id;
+        var name = m[1];
+        var id = m[2];
+        var res;
+
+        var cat;
+        if (implicitSnapshot.cat !== undefined)
+          cat = implicitSnapshot.cat;
+        else
+          cat = containingSnapshot.objectInstance.category;
+
+        var baseTypename;
+        if (implicitSnapshot.base_type)
+          baseTypename = implicitSnapshot.base_type;
+        else
+          baseTypename = undefined;
+
+        try {
+          res = process.objects.addSnapshot(
+              id, cat,
+              name, containingSnapshot.ts,
+              implicitSnapshot, baseTypename);
+        } catch (e) {
+          this.model_.importWarning({
+            type: 'object_snapshot_parse_error',
+            message: 'While processing implicit snapshot of ' +
+                rawId + ' at ts=' + containingSnapshot.ts + ': ' + e
+          });
+          return;
+        }
+        res.objectInstance.hasImplicitSnapshots = true;
+        res.containingSnapshot = containingSnapshot;
+        res.snapshottedOnThread = containingSnapshot.snapshottedOnThread;
+        referencingObject[referencingObjectFieldName] = res;
+        if (!(res instanceof tv.c.trace_model.ObjectSnapshot))
+          throw new Error('Created object must be instanceof snapshot');
+        return res.args;
+      }
+
+      /**
+       * Iterates over the fields in the object, calling func for every
+       * field/value found.
+       *
+       * @return {object} If the function does not want the field's value to be
+       * iterated, return null. If iteration of the field value is desired, then
+       * return either undefined (if the field value did not change) or the new
+       * field value if it was changed.
+       */
+      function iterObject(object, func, containingSnapshot, thisArg) {
+        if (!(object instanceof Object))
+          return;
+
+        if (object instanceof Array) {
+          for (var i = 0; i < object.length; i++) {
+            var res = func.call(thisArg, object, i, object[i],
+                                containingSnapshot);
+            if (res === null)
+              continue;
+            if (res)
+              iterObject(res, func, containingSnapshot, thisArg);
+            else
+              iterObject(object[i], func, containingSnapshot, thisArg);
+          }
+          return;
+        }
+
+        for (var key in object) {
+          var res = func.call(thisArg, object, key, object[key],
+                              containingSnapshot);
+          if (res === null)
+            continue;
+          if (res)
+            iterObject(res, func, containingSnapshot, thisArg);
+          else
+            iterObject(object[key], func, containingSnapshot, thisArg);
+        }
+      }
+
+      // TODO(nduca): We may need to iterate the instances in sorted order by
+      // creationTs.
+      process.objects.iterObjectInstances(function(instance) {
+        instance.snapshots.forEach(function(snapshot) {
+          if (snapshot.args.id !== undefined)
+            throw new Error('args cannot have an id field inside it');
+          iterObject(snapshot.args, processField, snapshot, this);
+        }, this);
+      }, this);
+    },
+
+    createMemoryDumps_: function() {
+      tv.b.iterItems(this.allMemoryDumpEvents_, function(id, events) {
+        // Calculate the range of the global memory dump.
+        var range = new tv.b.Range();
+        if (events.global !== undefined)
+          range.addValue(events.global.ts);
+        for (var i = 0; i < events.process.length; i++)
+          range.addValue(events.process[i].ts);
+
+        // Create the global memory dump.
+        var globalMemoryDump = new tv.c.trace_model.GlobalMemoryDump(
+            this.model_, range.min, this.deepCopyIfNeeded_(events.global));
+        globalMemoryDump.duration = range.range;
+        this.model_.globalMemoryDumps.push(globalMemoryDump);
+
+        // Create individual process memory dumps.
+        if (events.process.length === 0) {
+          this.model_.importWarning({
+              type: 'memory_dump_parse_error',
+              message: 'No process memory dumps associated with global memory' +
+                  ' dump ' + id + '.'
+          });
+        }
+
+        for (var i = 0; i < events.process.length; i++) {
+          var processEvent = events.process[i];
+          var pid = processEvent.pid;
+          if (pid in globalMemoryDump.processMemoryDumps) {
+            this.model_.importWarning({
+              type: 'memory_dump_parse_error',
+              message: 'Multiple process memory dumps with pid=' + pid +
+                  ' for dump id ' + id + '.'
+            });
+            continue;
+          }
+
+          var dumps = processEvent.args.dumps;
+          if (dumps === undefined) {
+            this.model_.importWarning({
+                type: 'memory_dump_parse_error',
+                message: 'dumps not found in process memory dump for ' +
+                    'pid=' + pid + ' and dump id=' + id + '.'
+            });
+            continue;
+          }
+
+          var process = this.model_.getOrCreateProcess(pid);
+          var processMemoryDump = new tv.c.trace_model.ProcessMemoryDump(
+              globalMemoryDump, process, processEvent.ts);
+
+          // Parse the totals, which are mandatory.
+          if (dumps.process_totals === undefined ||
+              dumps.process_totals.resident_set_bytes === undefined) {
+            this.model_.importWarning({
+                type: 'memory_dump_parse_error',
+                message: 'Mandatory field resident_set_bytes not found in' +
+                    ' process memory dump for pid=' + pid +
+                    ' and dump id=' + id + '.'
+            });
+            continue;
+          }
+          processMemoryDump.totalResidentBytes = parseInt(
+              dumps.process_totals.resident_set_bytes, 16);
+
+          // Populate the vmRegions, if present.
+          if (dumps.process_mmaps && dumps.process_mmaps.vm_regions) {
+            processMemoryDump.vmRegions = dumps.process_mmaps.vm_regions.map(
+              function(rawRegion) {
+                // See //base/trace_event/process_memory_maps.cc in Chromium.
+                var byteStats = new tv.c.trace_model.VMRegionByteStats(
+                    parseInt(rawRegion.bs.prv, 16),  // privateResident
+                    parseInt(rawRegion.bs.shr, 16),  // sharedResident
+                    parseInt(rawRegion.bs.pss, 16)  // proportionalResident
+                );
+                return new tv.c.trace_model.VMRegion(
+                    parseInt(rawRegion.sa, 16),  // startAddress
+                    parseInt(rawRegion.sz, 16),  // sizeInBytes
+                    rawRegion.pf,  // protectionFlags
+                    rawRegion.mf,  // mappedFile
+                    byteStats
+                );
+              }
+            );
+          }
+
+          // Populate the allocator dumps, if present, in two passes.
+          if (dumps.allocators !== undefined) {
+            // 1) Construct the MemoryAllocatorDump objects without parent links
+            //    and add them to the memoryAllocatorDumpsByFullName index.
+            tv.b.iterItems(dumps.allocators, function(fullName, rawDump) {
+              var allocatorDump = new tv.c.trace_model.MemoryAllocatorDump(
+                  fullName,
+                  parseInt(rawDump.physical_size_in_bytes, 16),
+                  parseInt(rawDump.allocated_objects_count, 16),
+                  parseInt(rawDump.allocated_objects_size_in_bytes, 16)
+              );
+              processMemoryDump.memoryAllocatorDumpsByFullName[fullName] =
+                  allocatorDump;
+            }, this);
+
+            // 2) Find the roots allocator dumps and establish the parent links.
+            var fullNames = Object.keys(dumps.allocators);
+            fullNames.sort();
+            fullNames.forEach(function(fullName) {
+              var allocatorDump =
+                  processMemoryDump.memoryAllocatorDumpsByFullName[fullName];
+              var rawDump = dumps.allocators[fullName];
+              var parentFullName = rawDump.parent;
+              if (parentFullName === undefined) {
+                // If the dump is a root, add it to the top-level
+                // memoryAllocatorDumps list of the processMemoryDump.
+                processMemoryDump.memoryAllocatorDumps.push(allocatorDump);
+                return;
+              }
+              // If the dump is not a root, find its parent and setup the
+              // parent <-> children relationships.
+              var parentAllocatorDump =
+                  processMemoryDump.memoryAllocatorDumpsByFullName[
+                      parentFullName];
+              if (parentAllocatorDump === undefined) {
+                this.model_.importWarning({
+                    type: 'memory_dump_parse_error',
+                    message: 'The allocator dump ' + rawDump.fullName +
+                        ' refers to an invalid parent ' + parentFullName +
+                        ' in the process memory dump id=' + id +
+                        ' for pid=' + pid + '.'
+                });
+                return;
+              }
+              allocatorDump.parent = parentAllocatorDump;
+              allocatorDump.parent.children.push(allocatorDump);
+            }, this);
+          }
+
+          process.memoryDumps.push(processMemoryDump);
+          globalMemoryDump.processMemoryDumps[pid] = processMemoryDump;
+        }
+      }, this);
+    },
+
+    joinObjectRefs_: function() {
+      tv.b.iterItems(this.model_.processes, function(pid, process) {
+        this.joinObjectRefsForProcess_(process);
+      }, this);
+    },
+
+    joinObjectRefsForProcess_: function(process) {
+      // Iterate the world, looking for id_refs
+      var patchupsToApply = [];
+      tv.b.iterItems(process.threads, function(tid, thread) {
+        thread.asyncSliceGroup.slices.forEach(function(item) {
+          this.searchItemForIDRefs_(
+              patchupsToApply, process.objects, 'start', item);
+        }, this);
+        thread.sliceGroup.slices.forEach(function(item) {
+          this.searchItemForIDRefs_(
+              patchupsToApply, process.objects, 'start', item);
+        }, this);
+      }, this);
+      process.objects.iterObjectInstances(function(instance) {
+        instance.snapshots.forEach(function(item) {
+          this.searchItemForIDRefs_(
+              patchupsToApply, process.objects, 'ts', item);
+        }, this);
+      }, this);
+
+      // Change all the fields pointing at id_refs to their real values.
+      patchupsToApply.forEach(function(patchup) {
+        patchup.object[patchup.field] = patchup.value;
+      });
+    },
+
+    searchItemForIDRefs_: function(patchupsToApply, objectCollection,
+                                   itemTimestampField, item) {
+      if (!item.args)
+        throw new Error('item is missing its args');
+
+      function handleField(object, fieldName, fieldValue) {
+        if (!fieldValue || (!fieldValue.id_ref && !fieldValue.idRef))
+          return;
+
+        var id = fieldValue.id_ref || fieldValue.idRef;
+        var ts = item[itemTimestampField];
+        var snapshot = objectCollection.getSnapshotAt(id, ts);
+        if (!snapshot)
+          return;
+
+        // We have to delay the actual change to the new value until after all
+        // refs have been located. Otherwise, we could end up recursing in
+        // ways we definitely didn't intend.
+        patchupsToApply.push({object: object,
+          field: fieldName,
+          value: snapshot});
+      }
+      function iterObjectFieldsRecursively(object) {
+        if (!(object instanceof Object))
+          return;
+
+        if ((object instanceof tv.c.trace_model.ObjectSnapshot) ||
+            (object instanceof Float32Array) ||
+            (object instanceof tv.b.Quad))
+          return;
+
+        if (object instanceof Array) {
+          for (var i = 0; i < object.length; i++) {
+            handleField(object, i, object[i]);
+            iterObjectFieldsRecursively(object[i]);
+          }
+          return;
+        }
+
+        for (var key in object) {
+          var value = object[key];
+          handleField(object, key, value);
+          iterObjectFieldsRecursively(value);
+        }
+      }
+
+      iterObjectFieldsRecursively(item.args);
+    }
+  };
+
+  tv.c.importer.Importer.register(TraceEventImporter);
+
+  return {
+    TraceEventImporter: TraceEventImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/trace_event_importer_perf_test.html b/trace-viewer/trace_viewer/extras/importer/trace_event_importer_perf_test.html
new file mode 100644
index 0000000..ed609d8
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/trace_event_importer_perf_test.html
@@ -0,0 +1,74 @@
+<!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="/extras/full_config.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var eventStrings = {};
+
+  // @const
+  var TEST_NAMES = ['simple_trace', 'lthi_cats'];
+  // @const
+  var TEST_FILES_PATHS = ['/test_data/simple_trace.json',
+                          '/test_data/lthi_cats.json.gz'];
+
+  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;
+  }
+
+  function getEvents(url) {
+    if (url in eventStrings)
+      return eventStrings[url];
+    eventStrings[url] = getSynchronous(url);
+    return eventStrings[url];
+  }
+
+  function timedPerfTestWithEvents(name, testFn, initialOptions) {
+    if (initialOptions.setUp)
+      throw new Error(
+          'Per-test setUp not supported. Trivial to fix if needed.');
+
+    var options = {};
+    for (var k in initialOptions)
+      options[k] = initialOptions[k];
+    options.setUp = function() {
+      TEST_FILES_PATHS.forEach(
+          function warmup(url) {
+            getEvents(url);
+          });
+    };
+    timedPerfTest(name, testFn, options);
+  }
+
+  var n110100 = [1, 10, 100];
+  n110100.forEach(function(val) {
+    timedPerfTestWithEvents(TEST_NAMES[0] + '_' + val, function() {
+      var events = getEvents(TEST_FILES_PATHS[0]);
+      var m = new tv.c.TraceModel();
+      m.importTraces([events], false, false);
+    }, {iterations: val});
+  });
+
+  timedPerfTestWithEvents(TEST_NAMES[1] + '_1', function() {
+    var events = getEvents(TEST_FILES_PATHS[1]);
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+  }, {iterations: 1});
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/trace_event_importer_test.html b/trace-viewer/trace_viewer/extras/importer/trace_event_importer_test.html
new file mode 100644
index 0000000..bd9c360
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/trace_event_importer_test.html
@@ -0,0 +1,2622 @@
+<!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="/extras/importer/trace_event_importer.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var findSliceNamed = tv.c.test_utils.findSliceNamed;
+
+  test('canImportEmpty', function() {
+    assert.isFalse(tv.e.importer.TraceEventImporter.canImport([]));
+    assert.isFalse(tv.e.importer.TraceEventImporter.canImport(''));
+  });
+
+  test('basicSingleThreadNonnestedParsing', 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: 'E'},
+      {name: 'b', args: {}, pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'B'},
+      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'bar', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 2);
+    assert.equal(t.tid, 53);
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.closeTo((560 - 520) / 1000, slice.duration, 1e-5);
+    assert.equal(slice.subSlices.length, 0);
+
+    slice = t.sliceGroup.slices[1];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'bar');
+    assert.closeTo((629 - 520) / 1000, slice.start, 1e-5);
+    assert.closeTo((631 - 629) / 1000, slice.duration, 1e-5);
+    assert.equal(slice.subSlices.length, 0);
+  });
+
+  test('basicSingleThreadNonnestedParsingWithCpuDuration', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B', tts: 221}, // @suppress longLineCheck
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E', tts: 259}, // @suppress longLineCheck
+      {name: 'b', args: {}, pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'B', tts: 329}, // @suppress longLineCheck
+      {name: 'b', args: {}, pid: 52, ts: 631, cat: 'bar', tid: 53, ph: 'E', tts: 331}  // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 2);
+    assert.equal(t.tid, 53);
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.closeTo((560 - 520) / 1000, slice.duration, 1e-5);
+    assert.closeTo((259 - 221) / 1000, slice.cpuDuration, 1e-5);
+    assert.equal(slice.subSlices.length, 0);
+
+    slice = t.sliceGroup.slices[1];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'bar');
+    assert.closeTo((629 - 520) / 1000, slice.start, 1e-5);
+    assert.closeTo((631 - 629) / 1000, slice.duration, 1e-5);
+    assert.closeTo((331 - 329) / 1000, slice.cpuDuration, 1e-5);
+    assert.equal(slice.subSlices.length, 0);
+  });
+
+  test('argumentDupeCreatesNonFailingImportError', function() {
+    var events = [
+      {name: 'a',
+        args: {'x': 1},
+        pid: 1,
+        ts: 520,
+        cat: 'foo',
+        tid: 1,
+        ph: 'B'},
+      {name: 'a',
+        args: {'x': 2},
+        pid: 1,
+        ts: 560,
+        cat: 'foo',
+        tid: 1,
+        ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[1].threads[1];
+    var sA = findSliceNamed(t.sliceGroup, 'a');
+
+    assert.equal(sA.args.x, 2);
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(1, m.importWarnings.length);
+  });
+
+  test('importMissingArgs', function() {
+    var events = [
+      {name: 'a', pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'},
+      {name: 'b', pid: 52, ts: 629, cat: 'bar', tid: 53, ph: 'I'}
+    ];
+
+    // This should not throw an exception.
+    new tv.c.TraceModel(events);
+  });
+
+  test('importDoesNotChokeOnNulls', function() {
+    var events = [
+      {name: 'a', args: { foo: null }, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'}, // @suppress longLineCheck
+      {name: 'a', pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    // This should not throw an exception.
+    new tv.c.TraceModel(events);
+  });
+
+  test('categoryBeginEndMismatchPrefersBegin', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'bar', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.tid, 53);
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+  });
+
+  test('beginEndNameMismatch', 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: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(m.importWarnings.length, 1);
+  });
+
+  test('nestedParsing', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, tts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 2, tts: 2, cat: 'bar', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 3, tts: 3, cat: 'bar', tid: 1, ph: 'E'},
+      {name: 'a', args: {}, pid: 1, ts: 4, tts: 3, cat: 'foo', tid: 1, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var t = m.processes[1].threads[1];
+
+    var sA = findSliceNamed(t.sliceGroup, 'a');
+    var sB = findSliceNamed(t.sliceGroup, 'b');
+
+    assert.equal(sA.title, 'a');
+    assert.equal(sA.category, 'foo');
+    assert.equal(sA.start, 0.001);
+    assert.equal(sA.duration, 0.003);
+    assert.equal(sA.selfTime, 0.002);
+    assert.equal(sA.cpuSelfTime, 0.001);
+
+    assert.equal(sB.title, 'b');
+    assert.equal(sB.category, 'bar');
+    assert.equal(sB.start, 0.002);
+    assert.equal(sB.duration, 0.001);
+
+    assert.equal(1, sA.subSlices.length);
+    assert.equal(sB, sA.subSlices[0]);
+    assert.equal(sA, sB.parentSlice);
+  });
+
+  test('nestedParsingWithTwoSubSlices', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, tts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 2, tts: 2, cat: 'bar', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 3, tts: 3, cat: 'bar', tid: 1, ph: 'E'},
+      {name: 'c', args: {}, pid: 1, ts: 5, tts: 5, cat: 'baz', tid: 1, ph: 'B'},
+      {name: 'c', args: {}, pid: 1, ts: 7, tts: 6, cat: 'baz', tid: 1, ph: 'E'},
+      {name: 'a', args: {}, pid: 1, ts: 8, tts: 8, cat: 'foo', tid: 1, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var t = m.processes[1].threads[1];
+
+    var sA = findSliceNamed(t.sliceGroup, 'a');
+    var sB = findSliceNamed(t.sliceGroup, 'b');
+    var sC = findSliceNamed(t.sliceGroup, 'c');
+
+    assert.equal(sA.title, 'a');
+    assert.equal(sA.category, 'foo');
+    assert.equal(sA.start, 0.001);
+    assert.equal(sA.duration, 0.007);
+    assert.equal(sA.selfTime, 0.004);
+    assert.equal(sA.cpuSelfTime, 0.005);
+
+    assert.equal(sB.title, 'b');
+    assert.equal(sB.category, 'bar');
+    assert.equal(sB.start, 0.002);
+    assert.equal(sB.duration, 0.001);
+
+    assert.equal(sC.title, 'c');
+    assert.equal(sC.category, 'baz');
+    assert.equal(sC.start, 0.005);
+    assert.equal(sC.duration, 0.002);
+
+    assert.equal(sA.subSlices.length, 2);
+    assert.equal(sA.subSlices[0], sB);
+    assert.equal(sA.subSlices[1], sC);
+    assert.equal(sB.parentSlice, sA);
+    assert.equal(sC.parentSlice, sA);
+  });
+
+  test('nestedParsingWithDoubleNesting', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 1, ph: 'B'},
+      {name: 'c', args: {}, pid: 1, ts: 3, cat: 'baz', tid: 1, ph: 'B'},
+      {name: 'c', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 1, ph: 'E'},
+      {name: 'b', args: {}, pid: 1, ts: 7, cat: 'bar', tid: 1, ph: 'E'},
+      {name: 'a', args: {}, pid: 1, ts: 8, cat: 'foo', tid: 1, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var t = m.processes[1].threads[1];
+
+    var sA = findSliceNamed(t.sliceGroup, 'a');
+    var sB = findSliceNamed(t.sliceGroup, 'b');
+    var sC = findSliceNamed(t.sliceGroup, 'c');
+
+    assert.equal(sA.title, 'a');
+    assert.equal(sA.category, 'foo');
+    assert.equal(sA.start, 0.001);
+    assert.equal(sA.duration, 0.007);
+    assert.equal(sA.selfTime, 0.002);
+
+    assert.equal(sB.title, 'b');
+    assert.equal(sB.category, 'bar');
+    assert.equal(sB.start, 0.002);
+    assert.equal(sB.duration, 0.005);
+    assert.equal(sA.selfTime, 0.002);
+
+    assert.equal(sC.title, 'c');
+    assert.equal(sC.category, 'baz');
+    assert.equal(sC.start, 0.003);
+    assert.equal(sC.duration, 0.002);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.equal(sA.subSlices[0], sB);
+    assert.equal(sB.parentSlice, sA);
+
+    assert.equal(sB.subSlices.length, 1);
+    assert.equal(sB.subSlices[0], sC);
+    assert.equal(sC.parentSlice, sB);
+  });
+
+
+  test('autoclosing', function() {
+    var events = [
+      // Slice that doesn't finish.
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+
+      // Slice that does finish to give an 'end time' to make autoclosing work.
+      {name: 'b', args: {}, pid: 1, ts: 1, cat: 'bar', tid: 2, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 2, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[1];
+    var t = p.threads[1];
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.isTrue(slice.didNotFinish);
+    assert.equal(slice.start, 0);
+    assert.equal(slice.duration, (2 - 1) / 1000);
+  });
+
+  test('autoclosingLoneBegin', function() {
+    var events = [
+      // Slice that doesn't finish.
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[1];
+    var t = p.threads[1];
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.isTrue(slice.didNotFinish);
+    assert.equal(slice.start, 0);
+    assert.equal(slice.duration, 0);
+  });
+
+  test('autoclosingWithSubTasks', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'b1', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'b1', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'E'},
+      {name: 'b2', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var t = m.processes[1].threads[1];
+
+    var sA = findSliceNamed(t.sliceGroup, 'a');
+    var sB1 = findSliceNamed(t.sliceGroup, 'b1');
+    var sB2 = findSliceNamed(t.sliceGroup, 'b2');
+
+    assert.equal(sA.end, 0.003);
+    assert.equal(sB1.end, 0.003);
+    assert.equal(sB2.end, 0.003);
+  });
+
+  test('autoclosingWithEventsOutsideBounds', function() {
+    var events = [
+      // Slice that begins before min and ends after max of the other threads.
+      {name: 'a', args: {}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 3, cat: 'foo', tid: 1, ph: 'B'},
+
+      // Slice that does finish to give an 'end time' to establish a basis
+      {name: 'c', args: {}, pid: 1, ts: 1, cat: 'bar', tid: 2, ph: 'B'},
+      {name: 'c', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 2, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p = m.processes[1];
+    var t = p.threads[1];
+    assert.equal(t.sliceGroup.length, 2);
+
+    var slice = findSliceNamed(t.sliceGroup, 'a');
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.equal(slice.duration, 0.003);
+
+    var t2 = p.threads[2];
+    var slice2 = findSliceNamed(t2.sliceGroup, 'c');
+    assert.equal(slice2.title, 'c');
+    assert.equal(slice2.category, 'bar');
+    assert.equal(slice2.start, 0.001);
+    assert.equal(slice2.duration, 0.001);
+
+    assert.equal(m.bounds.min, 0.000);
+    assert.equal(m.bounds.max, 0.003);
+  });
+
+  test('nestedAutoclosing', function() {
+    var events = [
+      // Tasks that don't finish.
+      {name: 'a1', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'a2', args: {}, pid: 1, ts: 1.5, cat: 'foo', tid: 1, ph: 'B'},
+
+      // Slice that does finish to give an 'end time' to make autoclosing work.
+      {name: 'b', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 2, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 2, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var t1 = m.processes[1].threads[1];
+    var t2 = m.processes[1].threads[2];
+
+    var sA1 = findSliceNamed(t1.sliceGroup, 'a1');
+    var sA2 = findSliceNamed(t1.sliceGroup, 'a2');
+    var sB = findSliceNamed(t2.sliceGroup, 'b');
+
+    assert.equal(sA1.end, 0.002);
+    assert.equal(sA2.end, 0.002);
+  });
+
+  test('taskColoring', function() {
+    // The test below depends on hashing of 'a' != 'b'. Fail early if that
+    // assumption is incorrect.
+    assert.notEqual(tv.b.ui.getStringHash('a'), tv.b.ui.getStringHash('b'));
+
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+      {name: 'b', args: {}, pid: 1, ts: 3, cat: 'bar', tid: 1, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 4, cat: 'bar', tid: 1, ph: 'E'},
+      {name: 'a', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 1, ph: 'B'},
+      {name: 'a', args: {}, pid: 1, ts: 6, cat: 'baz', tid: 1, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[1];
+    var t = p.threads[1];
+    var a1 = t.sliceGroup.slices[0];
+    assert.equal(a1.title, 'a');
+    assert.equal(a1.category, 'foo');
+    var b = t.sliceGroup.slices[1];
+    assert.equal(b.title, 'b');
+    assert.equal(b.category, 'bar');
+    assert.notEqual(b.colorId, a1.colorId);
+    var a2 = t.sliceGroup.slices[2];
+    assert.equal(a2.title, 'a');
+    assert.equal(a2.category, 'baz');
+    assert.equal(a1.colorId, a2.colorId);
+  });
+
+  test('multipleThreadParsing', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+      {name: 'b', args: {}, pid: 1, ts: 3, cat: 'bar', tid: 2, ph: 'B'},
+      {name: 'b', args: {}, pid: 1, ts: 4, cat: 'bar', tid: 2, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[1];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 2);
+
+    // Check thread 1.
+    var t = p.threads[1];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.tid, 1);
+
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.equal(slice.duration, (2 - 1) / 1000);
+    assert.equal(slice.subSlices.length, 0);
+
+    // Check thread 2.
+    var t = p.threads[2];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.tid, 2);
+
+    slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'bar');
+    assert.equal(slice.start, (3 - 1) / 1000);
+    assert.equal(slice.duration, (4 - 3) / 1000);
+    assert.equal(slice.subSlices.length, 0);
+  });
+
+  test('multiplePidParsing', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'a', args: {}, pid: 1, ts: 2, cat: 'foo', tid: 1, ph: 'E'},
+      {name: 'b', args: {}, pid: 2, ts: 3, cat: 'bar', tid: 2, ph: 'B'},
+      {name: 'b', args: {}, pid: 2, ts: 4, cat: 'bar', tid: 2, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 2);
+    var p = m.processes[1];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+
+    // Check process 1 thread 1.
+    var t = p.threads[1];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.tid, 1);
+
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.equal(slice.duration, (2 - 1) / 1000);
+    assert.equal(slice.subSlices.length, 0);
+
+    // Check process 2 thread 2.
+    var p = m.processes[2];
+    assert.isDefined(p);
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[2];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.tid, 2);
+
+    slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'bar');
+    assert.equal(slice.start, (3 - 1) / 1000);
+    assert.equal(slice.duration, (4 - 3) / 1000);
+    assert.equal(slice.subSlices.length, 0);
+
+    // Check getAllThreads.
+    assert.deepEqual(m.getAllThreads(),
+                      [m.processes[1].threads[1], m.processes[2].threads[2]]);
+  });
+
+  // Process names.
+  test('processNames', function() {
+    var events = [
+      {name: 'process_name', args: {name: 'SomeProcessName'},
+        pid: 1, ts: 0, tid: 1, ph: 'M'},
+      {name: 'process_name', args: {name: 'SomeProcessName'},
+        pid: 2, ts: 0, tid: 1, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+    assert.equal(m.processes[1].name, 'SomeProcessName');
+  });
+
+  // Process labels.
+  test('processLabels', function() {
+    var events = [
+      {name: 'process_labels', args: {labels: 'foo,bar,bar,foo,baz'},
+        pid: 1, ts: 0, tid: 1, ph: 'M'},
+      {name: 'process_labels', args: {labels: 'baz'},
+        pid: 2, ts: 0, tid: 1, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+    assert.deepEqual(m.processes[1].labels, ['foo', 'bar', 'baz']);
+    assert.deepEqual(m.processes[2].labels, ['baz']);
+  });
+
+  // Process sort index.
+  test('processSortIndex', function() {
+    var events = [
+      {name: 'process_name', args: {name: 'First'},
+        pid: 2, ts: 0, tid: 1, ph: 'M'},
+      {name: 'process_name', args: {name: 'Second'},
+        pid: 2, ts: 0, tid: 1, ph: 'M'},
+      {name: 'process_sort_index', args: {sort_index: 1},
+        pid: 1, ts: 0, tid: 1, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+
+    // By name, p1 is before p2. But, its sort index overrides that.
+    assert.isAbove(m.processes[1].compareTo(m.processes[2]), 0);
+  });
+
+  // Thread names.
+  test('threadNames', function() {
+    var events = [
+      {name: 'thread_name', args: {name: 'Thread 1'},
+        pid: 1, ts: 0, tid: 1, ph: 'M'},
+      {name: 'thread_name', args: {name: 'Thread 2'},
+        pid: 2, ts: 0, tid: 2, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    m.importTraces([events], false, false);
+    assert.equal(m.processes[1].threads[1].name, 'Thread 1');
+    assert.equal(m.processes[2].threads[2].name, 'Thread 2');
+  });
+
+  // Thread sort index.
+  test('threadSortIndex', function() {
+    var events = [
+      {name: 'thread_name', args: {name: 'Thread 1'},
+        pid: 1, ts: 0, tid: 1, ph: 'M'},
+      {name: 'thread_name', args: {name: 'Thread 2'},
+        pid: 1, ts: 0, tid: 2, ph: 'M'},
+      {name: 'thread_sort_index', args: {sort_index: 1},
+        pid: 1, ts: 0, tid: 1, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+
+    // By name, t1 is before t2. But, its sort index overrides that.
+    var t1 = m.processes[1].threads[1];
+    var t2 = m.processes[1].threads[2];
+    assert.isAbove(t1.compareTo(t2), 0);
+  });
+
+  // CPU counts.
+  test('cpuCounts', function() {
+    var events = [
+      {name: 'num_cpus', args: {number: 4},
+        pid: 7, ts: 0, tid: 0, ph: 'M'},
+      {name: 'num_cpus', args: {number: 4},
+        pid: 14, ts: 0, tid: 0, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+    assert.equal(m.kernel.softwareMeasuredCpuCount, 4);
+    assert.equal(m.kernel.bestGuessAtCpuCount, 4);
+  });
+
+  test('cpuCountsWithSandboxBeingConfused', function() {
+    var events = [
+      {name: 'num_cpus', args: {number: 4},
+        pid: 7, ts: 0, tid: 0, ph: 'M'},
+      {name: 'num_cpus', args: {number: 1},
+        pid: 14, ts: 0, tid: 0, ph: 'M'}
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false, false);
+    assert.equal(m.kernel.softwareMeasuredCpuCount, 4);
+    assert.equal(m.kernel.bestGuessAtCpuCount, 4);
+  });
+
+  test('parsingWhenEndComesFirst', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'E'},
+      {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'a', args: {}, pid: 1, ts: 5, cat: 'foo', tid: 1, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p = m.processes[1];
+    var t = p.threads[1];
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.sliceGroup.slices[0].title, 'a');
+    assert.equal(t.sliceGroup.slices[0].category, 'foo');
+    assert.equal(t.sliceGroup.slices[0].start, 0.004);
+    assert.equal(t.sliceGroup.slices[0].duration, 0.001);
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(m.importWarnings.length, 1);
+  });
+
+  test('immediateParsing', function() {
+    var events = [
+      // Need to include immediates inside a task so the timeline
+      // recentering/zeroing doesn't clobber their timestamp.
+      {name: 'a', args: {}, pid: 1, ts: 1, cat: 'foo', tid: 1, ph: 'B'},
+      {name: 'immediate', args: {}, pid: 1, ts: 2, cat: 'bar', tid: 1, ph: 'I'},
+      {name: 'slower', args: {}, pid: 1, ts: 4, cat: 'baz', tid: 1, ph: 'i'},
+      {name: 'a', args: {}, pid: 1, ts: 4, cat: 'foo', tid: 1, ph: 'E'}
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p = m.processes[1];
+    var t = p.threads[1];
+
+    assert.equal(t.sliceGroup.length, 3);
+    assert.equal(t.sliceGroup.slices[0].start, 0.001);
+    assert.equal(t.sliceGroup.slices[0].duration, 0.003);
+    assert.equal(t.sliceGroup.slices[1].start, 0.002);
+    assert.equal(t.sliceGroup.slices[1].duration, 0);
+    assert.equal(t.sliceGroup.slices[2].start, 0.004);
+
+    var slice = findSliceNamed(t.sliceGroup, 'a');
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.duration, 0.003);
+
+    var immed = findSliceNamed(t.sliceGroup, 'immediate');
+    assert.equal(immed.title, 'immediate');
+    assert.equal(immed.category, 'bar');
+    assert.equal(immed.start, 0.002);
+    assert.equal(immed.duration, 0);
+
+    var slower = findSliceNamed(t.sliceGroup, 'slower');
+    assert.equal(slower.title, 'slower');
+    assert.equal(slower.category, 'baz');
+    assert.equal(slower.start, 0.004);
+    assert.equal(slower.duration, 0);
+  });
+
+  test('simpleCounter', function() {
+    var events = [
+      {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
+        ph: 'C'},
+      {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+        ph: 'C'},
+      {name: 'ctr', args: {'value': 0}, pid: 1, ts: 20, cat: 'foo', tid: 1,
+        ph: 'C'}
+
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[1];
+    var ctr = m.processes[1].counters['foo.ctr'];
+
+    assert.equal(ctr.name, 'ctr');
+    assert.equal(ctr.category, 'foo');
+    assert.equal(ctr.numSamples, 3);
+    assert.equal(ctr.numSeries, 1);
+
+    assert.equal(ctr.series[0].name, 'value');
+    assert.equal(ctr.series[0].color,
+                 tv.b.ui.getColorIdForGeneralPurposeString('ctr.value'));
+
+    assert.deepEqual(ctr.timestamps, [0, 0.01, 0.02]);
+
+    var samples = [];
+    ctr.series[0].samples.forEach(function(sample) {
+      samples.push(sample.value);
+    });
+    assert.deepEqual(samples, [0, 10, 0]);
+
+    assert.deepEqual(ctr.totals, [0, 10, 0]);
+    assert.equal(ctr.maxTotal, 10);
+  });
+
+  test('instanceCounter', function() {
+    var events = [
+      {name: 'ctr', args: {'value': 0}, pid: 1, ts: 0, cat: 'foo', tid: 1,
+        ph: 'C', id: 0},
+      {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+        ph: 'C', id: 0},
+      {name: 'ctr', args: {'value': 10}, pid: 1, ts: 10, cat: 'foo', tid: 1,
+        ph: 'C', id: 1},
+      {name: 'ctr', args: {'value': 20}, pid: 1, ts: 15, cat: 'foo', tid: 1,
+        ph: 'C', id: 1},
+      {name: 'ctr', args: {'value': 30}, pid: 1, ts: 18, cat: 'foo', tid: 1,
+        ph: 'C', id: 1},
+      {name: 'ctr', args: {'value': 40}, pid: 1, ts: 20, cat: 'bar', tid: 1,
+        ph: 'C', id: 2}
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[1];
+    var ctr = m.processes[1].counters['foo.ctr[0]'];
+    assert.equal(ctr.name, 'ctr[0]');
+    assert.equal(ctr.category, 'foo');
+    assert.equal(ctr.numSamples, 2);
+    assert.equal(ctr.numSeries, 1);
+
+    assert.deepEqual(ctr.timestamps, [0, 0.01]);
+    var samples = [];
+    ctr.series[0].samples.forEach(function(sample) {
+      samples.push(sample.value);
+    });
+    assert.deepEqual(samples, [0, 10]);
+
+    ctr = m.processes[1].counters['foo.ctr[1]'];
+    assert.equal(ctr.name, 'ctr[1]');
+    assert.equal(ctr.category, 'foo');
+    assert.equal(ctr.numSamples, 3);
+    assert.equal(ctr.numSeries, 1);
+    assert.deepEqual(ctr.timestamps, [0.01, 0.015, 0.018]);
+
+    samples = [];
+    ctr.series[0].samples.forEach(function(sample) {
+      samples.push(sample.value);
+    });
+    assert.deepEqual(samples, [10, 20, 30]);
+
+    ctr = m.processes[1].counters['bar.ctr[2]'];
+    assert.equal(ctr.name, 'ctr[2]');
+    assert.equal(ctr.category, 'bar');
+    assert.equal(ctr.numSamples, 1);
+    assert.equal(ctr.numSeries, 1);
+    assert.deepEqual(ctr.timestamps, [0.02]);
+    var samples = [];
+    ctr.series[0].samples.forEach(function(sample) {
+      samples.push(sample.value);
+    });
+    assert.deepEqual(samples, [40]);
+  });
+
+  test('multiCounterUpdateBounds', function() {
+    var ctr = new tv.c.trace_model.Counter(undefined, 'testBasicCounter',
+        '', 'testBasicCounter');
+    var value1Series = new tv.c.trace_model.CounterSeries(
+        'value1', 'testBasicCounter.value1');
+    var value2Series = new tv.c.trace_model.CounterSeries(
+        'value2', 'testBasicCounter.value2');
+    ctr.addSeries(value1Series);
+    ctr.addSeries(value2Series);
+
+    value1Series.addCounterSample(0, 0);
+    value1Series.addCounterSample(1, 1);
+    value1Series.addCounterSample(2, 1);
+    value1Series.addCounterSample(3, 2);
+    value1Series.addCounterSample(4, 3);
+    value1Series.addCounterSample(5, 1);
+    value1Series.addCounterSample(6, 3);
+    value1Series.addCounterSample(7, 3.1);
+
+    value2Series.addCounterSample(0, 0);
+    value2Series.addCounterSample(1, 0);
+    value2Series.addCounterSample(2, 1);
+    value2Series.addCounterSample(3, 1.1);
+    value2Series.addCounterSample(4, 0);
+    value2Series.addCounterSample(5, 7);
+    value2Series.addCounterSample(6, 0);
+    value2Series.addCounterSample(7, 0.5);
+
+    ctr.updateBounds();
+
+    assert.equal(ctr.bounds.min, 0);
+    assert.equal(ctr.bounds.max, 7);
+    assert.equal(ctr.maxTotal, 8);
+    assert.deepEqual([0, 0,
+                       1, 1,
+                       1, 2,
+                       2, 3.1,
+                       3, 3,
+                       1, 8,
+                       3, 3,
+                       3.1, 3.6], ctr.totals);
+  });
+
+  test('multiCounter', function() {
+    var events = [
+      {name: 'ctr', args: {'value1': 0, 'value2': 7}, pid: 1, ts: 0, cat: 'foo', tid: 1, ph: 'C'}, // @suppress longLineCheck
+      {name: 'ctr', args: {'value1': 10, 'value2': 4}, pid: 1, ts: 10, cat: 'foo', tid: 1, ph: 'C'}, // @suppress longLineCheck
+      {name: 'ctr', args: {'value1': 0, 'value2': 1 }, pid: 1, ts: 20, cat: 'foo', tid: 1, ph: 'C'} // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[1];
+    var ctr = m.processes[1].counters['foo.ctr'];
+    assert.equal(ctr.name, 'ctr');
+
+    assert.equal(ctr.name, 'ctr');
+    assert.equal(ctr.category, 'foo');
+    assert.equal(ctr.numSamples, 3);
+    assert.equal(ctr.numSeries, 2);
+
+    assert.equal(ctr.series[0].name, 'value1');
+    assert.equal(ctr.series[1].name, 'value2');
+    assert.equal(ctr.series[0].color,
+                 tv.b.ui.getColorIdForGeneralPurposeString('ctr.value1'));
+    assert.equal(ctr.series[1].color,
+                 tv.b.ui.getColorIdForGeneralPurposeString('ctr.value2'));
+
+    assert.deepEqual(ctr.timestamps, [0, 0.01, 0.02]);
+    var samples = [];
+    ctr.series[0].samples.forEach(function(sample) {
+      samples.push(sample.value);
+    });
+    assert.deepEqual(samples, [0, 10, 0]);
+
+    var samples1 = [];
+    ctr.series[1].samples.forEach(function(sample) {
+      samples1.push(sample.value);
+    });
+    assert.deepEqual(samples1, [7, 4, 1]);
+    assert.deepEqual([0, 7,
+                       10, 14,
+                       0, 1], ctr.totals);
+    assert.equal(ctr.maxTotal, 14);
+  });
+
+  test('importObjectInsteadOfArray', function() {
+    var events = { traceEvents: [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ] };
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+  });
+
+  test('importString', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(JSON.stringify(events));
+    assert.equal(m.numProcesses, 1);
+  });
+
+  test('importStringWithLeadingSpaces', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(' ' + JSON.stringify(events));
+    assert.equal(m.numProcesses, 1);
+  });
+
+  test('importStringWithTrailingNewLine', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(JSON.stringify(events) + '\n');
+    assert.equal(m.numProcesses, 1);
+  });
+
+  test('importStringWithMissingCloseSquareBracket', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var tmp = JSON.stringify(events);
+    assert.equal(tmp[tmp.length - 1], ']');
+
+    // Drop off the trailing ]
+    var dropped = tmp.substring(0, tmp.length - 1);
+    var m = new tv.c.TraceModel(dropped);
+    assert.equal(m.numProcesses, 1);
+  });
+
+  test('importStringWithEndingCommaButMissingCloseSquareBracket', function() {
+    var lines = [
+      '[',
+      '{"name": "a", "args": {}, "pid": 52, "ts": 524, "cat": "foo", "tid": 53, "ph": "B"},', // @suppress longLineCheck
+      '{"name": "a", "args": {}, "pid": 52, "ts": 560, "cat": "foo", "tid": 53, "ph": "E"},' // @suppress longLineCheck
+    ];
+    var text = lines.join('\n');
+
+    var m = new tv.c.TraceModel(text);
+    assert.equal(m.numProcesses, 1);
+    assert.equal(m.processes[52].threads[53].sliceGroup.length, 1);
+  });
+
+  test('importStringWithMissingCloseSquareBracketAndNewline', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var tmp = JSON.stringify(events);
+    assert.equal(tmp[tmp.length - 1], ']');
+
+    // Drop off the trailing ] and add a newline
+    var dropped = tmp.substring(0, tmp.length - 1);
+    var m = new tv.c.TraceModel(dropped + '\n');
+    assert.equal(m.numProcesses, 1);
+  });
+
+  test('ImportStringEndingCommaButMissingCloseSquareBracketCRLF', function() {
+    var lines = [
+      '[',
+      '{"name": "a", "args": {}, "pid": 52, "ts": 524, "cat": "foo", "tid": 53, "ph": "B"},', // @suppress longLineCheck
+      '{"name": "a", "args": {}, "pid": 52, "ts": 560, "cat": "foo", "tid": 53, "ph": "E"},' // @suppress longLineCheck
+    ];
+    var text = lines.join('\r\n');
+
+    var m = new tv.c.TraceModel(text);
+    assert.equal(m.numProcesses, 1);
+    assert.equal(m.processes[52].threads[53].sliceGroup.length, 1);
+  });
+
+  test('importOldFormat', function() {
+    var lines = [
+      '[',
+      '{"cat":"a","pid":9,"tid":8,"ts":194,"ph":"E","name":"I","args":{}},',
+      '{"cat":"b","pid":9,"tid":8,"ts":194,"ph":"B","name":"I","args":{}}',
+      ']'
+    ];
+    var text = lines.join('\n');
+    var m = new tv.c.TraceModel(text);
+    assert.equal(m.numProcesses, 1);
+    assert.equal(m.processes[9].threads[8].sliceGroup.length, 1);
+  });
+
+  test('startFinishOneSliceOneThread', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'a', args: {}, pid: 52, ts: 560, cat: 'cat', tid: 53,
+        ph: 'F', id: 72},
+      {name: 'a', pid: 52, ts: 524, cat: 'cat', tid: 53,
+        ph: 'S', id: 72, args: {'foo': 'bar'}}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    assert.equal(t.asyncSliceGroup.slices[0].title, 'a');
+    assert.equal(t.asyncSliceGroup.slices[0].category, 'cat');
+    assert.isTrue(t.asyncSliceGroup.slices[0].isTopLevel);
+    assert.equal(t.asyncSliceGroup.slices[0].id, 72);
+    assert.equal(t.asyncSliceGroup.slices[0].args.foo, 'bar');
+    assert.equal(t.asyncSliceGroup.slices[0].start, 0);
+    assert.closeTo(
+        (60 - 24) / 1000, t.asyncSliceGroup.slices[0].duration, 1e-5);
+    assert.equal(t.asyncSliceGroup.slices[0].startThread, t);
+    assert.equal(t.asyncSliceGroup.slices[0].endThread, t);
+  });
+
+  test('endArgsAddedToSlice', function() {
+    var events = [
+      {name: 'a', args: {x: 1}, pid: 52, ts: 520, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'a', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.length, 1);
+    assert.equal(t.tid, 53);
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.equal(slice.subSlices.length, 0);
+    assert.equal(slice.args['x'], 1);
+    assert.equal(slice.args['y'], 2);
+  });
+
+  test('endArgOverrwritesOriginalArgValueIfDuplicated', function() {
+    var events = [
+      {name: 'b', args: {z: 3}, pid: 52, ts: 629, cat: 'foo', tid: 53, ph: 'B'},
+      {name: 'b', args: {z: 4}, pid: 52, ts: 631, cat: 'foo', tid: 53, ph: 'E'}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'foo');
+    assert.equal(slice.start, 0);
+    assert.equal(slice.subSlices.length, 0);
+    assert.equal(slice.args['z'], 4);
+  });
+
+  test('asyncEndArgsAddedToSlice', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'c', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'F', id: 72},
+      {name: 'c', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'S', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'c');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.equal(parentSlice.args['x'], 1);
+    assert.equal(parentSlice.args['y'], 2);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 0);
+  });
+
+  test('asyncEndArgOverwritesOriginalArgValueIfDuplicated', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'd', args: {z: 4}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'F', id: 72},
+      {name: 'd', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'S', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'd');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.equal(parentSlice.args['z'], 4);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 0);
+  });
+
+  test('asyncStepsInOneThread', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53, ph: 'F', id: 72}, // @suppress longLineCheck
+      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo', tid: 53, ph: 'T', id: 72}, // @suppress longLineCheck
+      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53, ph: 'S', id: 72} // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.equal(parentSlice.start, 0);
+    assert.equal(parentSlice.args['x'], 1);
+    assert.isUndefined(parentSlice.args['y']);
+    assert.equal(parentSlice.args['z'], 3);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 1);
+
+    var subSlice = parentSlice.subSlices[0];
+    assert.equal(subSlice.title, 'a:s1');
+    assert.equal(subSlice.category, 'foo');
+    assert.isFalse(subSlice.isTopLevel);
+    assert.closeTo((548 - 524) / 1000, subSlice.start, 1e-5);
+    assert.closeTo((560 - 548) / 1000, subSlice.duration, 1e-5);
+    assert.isUndefined(subSlice.args['x']);
+    assert.equal(subSlice.args['y'], 2);
+    assert.isUndefined(subSlice.args['z']);
+  });
+
+  test('asyncStepsMissingStart', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'F', id: 72},
+      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
+        tid: 53, ph: 'T', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isUndefined(t);
+  });
+
+  test('asyncStepsMissingFinish', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
+        tid: 53, ph: 'T', id: 72},
+      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'S', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isUndefined(t);
+  });
+
+  test('asyncStepEndEvent', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'F', id: 72},
+      {name: 'a', args: {step: 's1', y: 2}, pid: 52, ts: 548, cat: 'foo',
+        tid: 53, ph: 'p', id: 72},
+      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'S', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.equal(parentSlice.start, 0);
+    assert.equal(parentSlice.args['x'], 1);
+    assert.isUndefined(parentSlice.args['y']);
+    assert.equal(parentSlice.args['z'], 3);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 1);
+    var subSlice = parentSlice.subSlices[0];
+    assert.equal(subSlice.title, 'a:s1');
+    assert.equal(subSlice.category, 'foo');
+    assert.isFalse(subSlice.isTopLevel);
+    assert.equal(subSlice.start, 0);
+    assert.closeTo((548 - 524) / 1000, subSlice.duration, 1e-5);
+    assert.isUndefined(subSlice.args['x']);
+    assert.equal(subSlice.args['y'], 2);
+    assert.isUndefined(subSlice.args['z']);
+  });
+
+  test('asyncStepMismatch', function() {
+    var events = [
+      // Time is intentionally out of order.
+      {name: 'a', args: {z: 3}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'F', id: 72},
+      {name: 'a', args: {step: 's2'}, pid: 52, ts: 548, cat: 'foo', tid: 53,
+        ph: 'T', id: 72},
+      {name: 'a', args: {step: 's1'}, pid: 52, ts: 548, cat: 'foo', tid: 53,
+        ph: 'p', id: 72},
+      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'S', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isUndefined(t);
+    assert.isTrue(m.hasImportWarnings);
+  });
+
+  test('nestableAsyncBasic', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 1);
+    var subSlice = parentSlice.subSlices[0];
+    assert.isFalse(subSlice.isTopLevel);
+    // Arguments should include both BEGIN and END event.
+    assert.equal(subSlice.args['x'], 1);
+    assert.equal(subSlice.args['y'], 2);
+    assert.isUndefined(subSlice.subSlices);
+  });
+
+  test('nestableAsyncCombinedParams', function() {
+    var events = [
+      {name: 'a', args: {x: 1, params: {p1: 'hello', p2: 123}},
+        pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 72},
+      {name: 'a', args: {y: 2, params: {p3: 'hi'}}, pid: 52, ts: 560,
+        cat: 'foo', tid: 53, ph: 'e', id: 72},
+      {name: 'b', args: {params: {p4: 'foo'}},
+        pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 73},
+      {name: 'b', args: {params: ''}, pid: 52, ts: 560,
+        cat: 'foo', tid: 53, ph: 'e', id: 73},
+      {name: 'c', args: {params: {p5: 'bar'}},
+        pid: 52, ts: 525, cat: 'foo', tid: 53, ph: 'b', id: 74},
+      {name: 'c', args: {}, pid: 52, ts: 560,
+        cat: 'foo', tid: 53, ph: 'e', id: 74}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 3);
+
+    var sliceA = t.asyncSliceGroup.slices[0];
+    // Arguments should include both BEGIN and END event.
+    assert.equal(sliceA.args['x'], 1);
+    assert.equal(sliceA.args['y'], 2);
+    var paramsA = sliceA.args['params'];
+    assert.isDefined(paramsA);
+    assert.equal(paramsA.p1, 'hello');
+    assert.equal(paramsA.p2, 123);
+    assert.equal(paramsA.p3, 'hi');
+    assert.isTrue(sliceA.isTopLevel);
+
+    var sliceB = t.asyncSliceGroup.slices[1];
+    // Arguments should include both BEGIN and END event.
+    var paramsB = sliceB.args['params'];
+    assert.isDefined(paramsB);
+    assert.equal(paramsB.p4, 'foo');
+    assert.isTrue(sliceB.isTopLevel);
+
+    var sliceC = t.asyncSliceGroup.slices[2];
+    // Arguments should include both BEGIN and END event.
+    var paramsC = sliceC.args['params'];
+    assert.isDefined(paramsC);
+    assert.equal(paramsC.p5, 'bar');
+    assert.isTrue(sliceC.isTopLevel);
+  });
+
+  test('nestableAsyncManyLevels', function() {
+    // There are 5 nested levels.
+    var events = [
+      {name: 'l1', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'l2', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'l3', args: {}, pid: 52, ts: 526, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'l4', args: {}, pid: 52, ts: 527, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'l5', args: {}, pid: 52, ts: 528, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'l5', args: {}, pid: 52, ts: 529, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'l4', args: {}, pid: 52, ts: 530, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'l3', args: {}, pid: 52, ts: 531, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'l2', args: {}, pid: 52, ts: 532, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'l1', args: {}, pid: 52, ts: 533, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    // Perfectly matched events should not produce a warning.
+    assert.isFalse(m.hasImportWarnings);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+
+    var l1Slice = t.asyncSliceGroup.slices[0];
+    assert.equal(l1Slice.title, 'l1');
+    assert.closeTo(0, l1Slice.start, 1e-5);
+    assert.closeTo(9 / 1000, l1Slice.duration, 1e-5);
+    assert.isTrue(l1Slice.isTopLevel);
+
+    assert.isDefined(l1Slice.subSlices);
+    assert.equal(l1Slice.subSlices.length, 1);
+    var l2Slice = l1Slice.subSlices[0];
+    assert.equal(l2Slice.title, 'l2');
+    assert.closeTo(1 / 1000, l2Slice.start, 1e-5);
+    assert.closeTo(7 / 1000, l2Slice.duration, 1e-5);
+    assert.isFalse(l2Slice.isTopLevel);
+
+    assert.isDefined(l2Slice.subSlices);
+    assert.equal(l2Slice.subSlices.length, 1);
+    var l3Slice = l2Slice.subSlices[0];
+    assert.equal(l3Slice.title, 'l3');
+    assert.closeTo(2 / 1000, l3Slice.start, 1e-5);
+    assert.closeTo(5 / 1000, l3Slice.duration, 1e-5);
+    assert.isFalse(l3Slice.isTopLevel);
+
+    assert.isDefined(l3Slice.subSlices);
+    assert.equal(l3Slice.subSlices.length, 1);
+    var l4Slice = l3Slice.subSlices[0];
+    assert.equal(l4Slice.title, 'l4');
+    assert.closeTo(3 / 1000, l4Slice.start, 1e-5);
+    assert.closeTo(3 / 1000, l4Slice.duration, 1e-5);
+    assert.isFalse(l4Slice.isTopLevel);
+
+    assert.isDefined(l4Slice.subSlices);
+    assert.equal(l4Slice.subSlices.length, 1);
+    var l5Slice = l4Slice.subSlices[0];
+    assert.equal(l5Slice.title, 'l5');
+    assert.closeTo(4 / 1000, l5Slice.start, 1e-5);
+    assert.closeTo(1 / 1000, l5Slice.duration, 1e-5);
+    assert.isFalse(l5Slice.isTopLevel);
+  });
+
+  test('nestableAsyncInstantEvent', function() {
+    var events = [
+      {name: 'c', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'n', id: 71},
+      {name: 'a', args: {}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'd', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'n', id: 72},
+      {name: 'a', args: {}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 2);
+    var instantSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(instantSlice.title, 'c');
+    assert.closeTo(0, instantSlice.start, 1e-5);
+    assert.closeTo(0, instantSlice.duration, 1e-5);
+    assert.isUndefined(instantSlice.subSlices);
+    assert.isTrue(instantSlice.isTopLevel);
+
+    var nestedSlice = t.asyncSliceGroup.slices[1];
+    assert.equal(nestedSlice.title, 'a');
+    assert.closeTo(0, nestedSlice.start, 1e-5);
+    assert.closeTo((565 - 524) / 1000, nestedSlice.duration, 1e-5);
+    assert.isTrue(nestedSlice.isTopLevel);
+    assert.isDefined(nestedSlice.subSlices);
+    assert.equal(nestedSlice.subSlices.length, 1);
+    var nestedInstantSlice = nestedSlice.subSlices[0];
+    assert.isUndefined(nestedInstantSlice.subSlices);
+    assert.equal(nestedInstantSlice.title, 'd');
+    assert.isFalse(nestedInstantSlice.isTopLevel);
+  });
+
+  test('nestableAsyncUnmatchedOuterBeginEvent', function() {
+    var events = [
+      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'b', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    // Unmatched BEGIN should produce a warning.
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.closeTo(0, parentSlice.start, 0.0001);
+    // Unmatched BEGIN event ends at the last event of that ID.
+    assert.closeTo(36 / 1000, parentSlice.duration, 0.0001);
+    // Arguments should include only include its arguments.
+    assert.isUndefined(parentSlice.args['y']);
+    assert.equal(parentSlice.args['x'], 1);
+    assert.isDefined(parentSlice.error);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 1);
+    var subSlice = parentSlice.subSlices[0];
+    assert.isFalse(subSlice.isTopLevel);
+    assert.closeTo(1 / 1000, subSlice.start, 1e-5);
+    assert.closeTo(35 / 1000, subSlice.duration, 1e-5);
+    assert.isUndefined(subSlice.subSlices);
+    // Arguments should include those of the END event.
+    assert.equal(subSlice.args['y'], 2);
+    assert.isUndefined(subSlice.subSlices);
+  });
+
+  test('nestableAsyncUnmatchedInnerBeginEvent', function() {
+    var events = [
+      {name: 'a', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'c', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'n', id: 72},
+      {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'a', args: {y: 2}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    // Unmatched BEGIN should produce a warning.
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.closeTo(0, parentSlice.start, 1e-5);
+    assert.closeTo(41 / 1000, parentSlice.duration, 1e-5);
+    // Arguments should include both BEGIN and END event.
+    assert.equal(parentSlice.args['y'], 2);
+    assert.equal(parentSlice.args['z'], 3);
+    assert.isUndefined(parentSlice.args['x']);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 2);
+    var subSliceInstant = parentSlice.subSlices[0];
+    var subSliceUnmatched = parentSlice.subSlices[1];
+    assert.equal(subSliceInstant.title, 'c');
+    assert.isFalse(subSliceInstant.isTopLevel);
+    assert.equal(subSliceUnmatched.title, 'b');
+    assert.isFalse(subSliceUnmatched.isTopLevel);
+    // Unmatched BEGIN ends at the last event of that ID.
+    assert.closeTo(1 / 1000, subSliceUnmatched.start, 1e-5);
+    assert.closeTo(40 / 1000, subSliceUnmatched.duration, 1e-5);
+    assert.isUndefined(subSliceUnmatched.subSlices);
+    assert.equal(subSliceUnmatched.args['x'], 1);
+    assert.isUndefined(subSliceUnmatched['y']);
+    assert.isDefined(subSliceUnmatched.error);
+    assert.closeTo(1 / 1000, subSliceInstant.start, 1e-5);
+    assert.closeTo(0, subSliceInstant.duration, 1e-5);
+    assert.isUndefined(subSliceInstant.subSlices);
+  });
+
+  test('nestableAsyncUnmatchedOuterEndEvent', function() {
+    // Events are intentionally out-of-order.
+    var events = [
+      {name: 'b', args: {x: 1}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'b', args: {y: 2}, pid: 52, ts: 560, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'a', args: {z: 3}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    // Unmatched END should produce a warning.
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(t.asyncSliceGroup.slices.length, 2);
+    var unmatchedSlice = t.asyncSliceGroup.slices[0];
+    var slice = t.asyncSliceGroup.slices[1];
+    assert.equal(unmatchedSlice.title, 'a');
+    assert.closeTo(0, unmatchedSlice.start, 1e-5);
+    assert.isTrue(unmatchedSlice.isTopLevel);
+    // Unmatched END event begins at the first event of that ID. In this
+    // case, the first event happens to be the same unmatched event.
+    assert.closeTo(0 / 1000, unmatchedSlice.duration, 1e-5);
+    assert.isUndefined(unmatchedSlice.args['x']);
+    assert.isUndefined(unmatchedSlice.args['y']);
+    assert.equal(unmatchedSlice.args['z'], 3);
+    assert.isDefined(unmatchedSlice.error);
+    assert.isUndefined(unmatchedSlice.subSlices);
+
+    assert.equal(slice.title, 'b');
+    assert.isTrue(slice.isTopLevel);
+    assert.closeTo(1 / 1000, slice.start, 1e-5);
+    assert.closeTo(35 / 1000, slice.duration, 1e-5);
+    // Arguments should include both BEGIN and END event.
+    assert.equal(slice.args['x'], 1);
+    assert.equal(slice.args['y'], 2);
+    assert.isUndefined(slice.subSlices);
+  });
+
+  test('nestableAsyncUnmatchedInnerEndEvent', function() {
+    var events = [
+      {name: 'a', args: {x: 1}, pid: 52, ts: 524, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'c', args: {}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'n', id: 72},
+      {name: 'b', args: {z: 3}, pid: 52, ts: 525, cat: 'foo', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'a', args: {y: 2}, pid: 52, ts: 565, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    // Unmatched END should produce a warning.
+    assert.isTrue(m.hasImportWarnings);
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.isTrue(parentSlice.isTopLevel);
+    assert.closeTo(0, parentSlice.start, 1e-5);
+    assert.closeTo(41 / 1000, parentSlice.duration, 1e-5);
+    // Arguments should include both BEGIN and END event.
+    assert.equal(parentSlice.args['x'], 1);
+    assert.equal(parentSlice.args['y'], 2);
+
+    assert.isDefined(parentSlice.subSlices);
+    assert.equal(parentSlice.subSlices.length, 2);
+    var subSliceInstant = parentSlice.subSlices[0];
+    var subSliceUnmatched = parentSlice.subSlices[1];
+    assert.equal(subSliceInstant.title, 'c');
+    assert.isFalse(subSliceInstant.isTopLevel);
+    assert.equal(subSliceUnmatched.title, 'b');
+    assert.isFalse(subSliceUnmatched.isTopLevel);
+    // Unmatched END begins at the first event of that ID.
+    assert.closeTo(0 / 1000, subSliceUnmatched.start, 1e-5);
+    assert.closeTo(1 / 1000, subSliceUnmatched.duration, 1e-5);
+    // Arguments should include both BEGIN and END event.
+    assert.isUndefined(subSliceUnmatched.args['x']);
+    assert.isUndefined(subSliceUnmatched.args['y']);
+    assert.equal(subSliceUnmatched.args['z'], 3);
+    assert.isDefined(subSliceUnmatched.error);
+
+    assert.isUndefined(subSliceUnmatched.subSlices);
+    assert.closeTo(1 / 1000, subSliceInstant.start, 1e-5);
+    assert.closeTo(0, subSliceInstant.duration, 1e-5);
+    assert.isUndefined(subSliceInstant.subSlices);
+  });
+
+  test('nestableAsyncSameIDDifferentCategory', function() {
+    // Events with the same ID, but different categories should not be
+    // considered as nested.
+    var events = [
+      {name: 'EVENT_A', args: {}, pid: 52, ts: 500, cat: 'foo', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'EVENT_B', args: {y: 2}, pid: 52, ts: 550, cat: 'bar', tid: 53,
+        ph: 'b', id: 72},
+      {name: 'EVENT_B', args: {}, pid: 52, ts: 600, cat: 'bar', tid: 53,
+        ph: 'e', id: 72},
+      {name: 'EVENT_A', args: {x: 1}, pid: 52, ts: 650, cat: 'foo', tid: 53,
+        ph: 'e', id: 72}
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+    assert.isDefined(t);
+    assert.equal(t.asyncSliceGroup.slices.length, 2);
+    var eventASlice = t.asyncSliceGroup.slices[0];
+    assert.equal(eventASlice.title, 'EVENT_A');
+    assert.equal(eventASlice.category, 'foo');
+    assert.equal(eventASlice.id, 'foo:72');
+    assert.isTrue(eventASlice.isTopLevel);
+    assert.equal(eventASlice.args['x'], 1);
+    assert.isUndefined(eventASlice.subSlices);
+
+    var eventBSlice = t.asyncSliceGroup.slices[1];
+    assert.equal(eventBSlice.title, 'EVENT_B');
+    assert.equal(eventBSlice.category, 'bar');
+    assert.equal(eventBSlice.id, 'bar:72');
+    assert.isTrue(eventBSlice.isTopLevel);
+    assert.equal(eventBSlice.args['y'], 2);
+    assert.isUndefined(eventBSlice.subSlices);
+  });
+
+  test('importSamples', function() {
+    var events = [
+      {name: 'a', args: {}, pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+      {name: 'b', args: {}, pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+      {name: 'c', args: {}, pid: 52, ts: 558, cat: 'test', tid: 53, ph: 'P'},
+      {name: 'a', args: {}, pid: 52, ts: 568, cat: 'test', tid: 53, ph: 'P'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[52];
+    assert.isDefined(p);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.samples_.length, 4);
+    assert.equal(t.samples_[0].start, 0.0);
+    assert.equal(t.samples_[1].start, 0.0);
+    assert.closeTo(0.01, t.samples_[2].start, 1e-5);
+    assert.equal(t.samples_[0].leafStackFrame.title, 'a');
+    assert.equal(t.samples_[1].leafStackFrame.title, 'b');
+    assert.equal(t.samples_[2].leafStackFrame.title, 'c');
+    assert.equal(t.samples_[3].leafStackFrame, t.samples[0].leafStackFrame);
+    assert.isFalse(m.hasImportWarnings);
+  });
+
+  test('importSamplesWithStackFrames', function() {
+    var eventData = {
+      traceEvents: [
+        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'test', tid: 2, ph: 'P', sf: 7 } // @suppress longLineCheck
+      ],
+      stackFrames: {
+        '1': {
+          category: 'm1',
+          name: 'main'
+        },
+        '7': {
+          category: 'm2',
+          name: 'frame7',
+          parent: '1'
+        }
+      }
+    };
+
+    var options = new tv.c.ImportOptions();
+    options.shiftWorldToZero = false;
+    var m = new tv.c.TraceModel(eventData, options);
+
+    var p = m.processes[1];
+    var t = p.threads[2];
+
+    assert.equal(t.samples.length, 1);
+    assert.equal(t.samples_[0].start, 0.0);
+    assert.equal(t.samples_[0].leafStackFrame.title, 'frame7');
+    assert.isFalse(m.hasImportWarnings);
+  });
+
+  test('importSamplesMissingArgs', function() {
+    var events = [
+      {name: 'a', pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+      {name: 'b', pid: 52, ts: 548, cat: 'test', tid: 53, ph: 'P'},
+      {name: 'c', pid: 52, ts: 549, cat: 'test', tid: 53, ph: 'P'}
+    ];
+    var m = new tv.c.TraceModel(events);
+    var p = m.processes[52];
+    assert.isDefined(p);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.isDefined(t);
+    assert.equal(t.samples_.length, 3);
+    assert.isFalse(m.hasImportWarnings);
+  });
+
+  test('importSimpleObject', function() {
+    var events = [
+      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 15}}, // @suppress longLineCheck
+      {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 20}}, // @suppress longLineCheck
+      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}} // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    assert.equal(m.bounds.min, 10);
+    assert.equal(m.bounds.max, 50);
+    assert.isFalse(m.hasImportWarnings);
+
+    var p = m.processes[1];
+    assert.isDefined(p);
+
+    var i10 = p.objects.getObjectInstanceAt('0x1000', 10);
+    assert.equal(i10.category, 'c');
+    assert.equal(i10.creationTs, 10);
+    assert.equal(i10.deletionTs, 50);
+    assert.equal(i10.snapshots.length, 2);
+
+    var s15 = i10.snapshots[0];
+    assert.equal(s15.ts, 15);
+    assert.equal(s15.args, 15);
+
+    var s20 = i10.snapshots[1];
+    assert.equal(s20.ts, 20);
+    assert.equal(s20.args, 20);
+  });
+
+  test('importImplicitObjects', function() {
+    var events = [
+      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
+        args: { snapshot: [
+          { id: 'subObject/0x1',
+            foo: 1
+          }
+        ]}},
+      {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a',
+        args: { snapshot: [
+          { id: 'subObject/0x1',
+            foo: 2
+          },
+          { id: 'subObject/0x2',
+            foo: 1
+          }
+        ]}}
+    ];
+
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    var p1 = m.processes[1];
+
+    var iA = p1.objects.getObjectInstanceAt('0x1000', 10);
+    var subObjectInstances = p1.objects.getAllInstancesByTypeName()[
+        'subObject'];
+
+    assert.equal(subObjectInstances.length, 2);
+    var subObject1 = p1.objects.getObjectInstanceAt('0x1', 15);
+    assert.equal(subObject1.name, 'subObject');
+    assert.equal(subObject1.creationTs, 15);
+
+    assert.equal(subObject1.snapshots.length, 2);
+    assert.equal(subObject1.snapshots[0].ts, 15);
+    assert.equal(subObject1.snapshots[0].args.foo, 1);
+    assert.equal(subObject1.snapshots[1].ts, 20);
+    assert.equal(subObject1.snapshots[1].args.foo, 2);
+
+    var subObject2 = p1.objects.getObjectInstanceAt('0x2', 20);
+    assert.equal(subObject2.name, 'subObject');
+    assert.equal(subObject2.creationTs, 20);
+    assert.equal(subObject2.snapshots.length, 1);
+    assert.equal(subObject2.snapshots[0].ts, 20);
+  });
+
+  test('importImplicitObjectWithCategoryOverride', function() {
+    var events = [
+      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'cat', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'otherCat', id: '0x1000', name: 'a', // @suppress longLineCheck
+        args: { snapshot: [
+          { id: 'subObject/0x1',
+            cat: 'cat',
+            foo: 1
+          }
+        ]}}
+    ];
+
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    var p1 = m.processes[1];
+
+    var iA = p1.objects.getObjectInstanceAt('0x1000', 10);
+    var subObjectInstances = p1.objects.getAllInstancesByTypeName()[
+        'subObject'];
+
+    assert.equal(subObjectInstances.length, 1);
+  });
+
+  test('importImplicitObjectWithBaseTypeOverride', function() {
+    var events = [
+      {ts: 10000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'PictureLayerImpl', args: { // @suppress longLineCheck
+        snapshot: {
+          base_type: 'LayerImpl'
+        }
+      }},
+      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'LayerImpl', args: {}} // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    var p1 = m.processes[1];
+    assert.equal(m.importWarnings.length, 0);
+
+    var iA = p1.objects.getObjectInstanceAt('0x1000', 10);
+    assert.equal(iA.snapshots.length, 1);
+  });
+
+  test('importIDRefs', function() {
+    var events = [
+      // An object with two snapshots.
+      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 15}}, // @suppress longLineCheck
+      {ts: 20000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: {snapshot: 20}}, // @suppress longLineCheck
+      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+
+      // A slice that references the object.
+      {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {my_object: {id_ref: '0x1000'}}}, // @suppress longLineCheck
+      {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    var p1 = m.processes[1];
+
+    var iA = p1.objects.getObjectInstanceAt('0x1000', 10);
+    var s15 = iA.getSnapshotAt(15);
+
+    var taskSlice = p1.threads[1].sliceGroup.slices[0];
+    assert.equal(taskSlice.args.my_object, s15);
+  });
+
+  test('importIDRefsThatPointAtEachOther', function() {
+    var events = [
+      // An object.
+      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
+        snapshot: { x: {
+          id: 'foo/0x1001',
+          value: 'bar'
+        }}}},
+      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+
+      // A slice that references the object.
+      {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {my_object: {id_ref: '0x1001'}}}, // @suppress longLineCheck
+      {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    var p1 = m.processes[1];
+
+    var iA = p1.objects.getObjectInstanceAt('0x1000', 15);
+    var iFoo = p1.objects.getObjectInstanceAt('0x1001', 15);
+    assert.isDefined(iA);
+    assert.isDefined(iFoo);
+
+    var a15 = iA.getSnapshotAt(15);
+    var foo15 = iFoo.getSnapshotAt(15);
+
+    var taskSlice = p1.threads[1].sliceGroup.slices[0];
+    assert.equal(taskSlice.args.my_object, foo15);
+  });
+
+  test('importArrayWithIDs', function() {
+    var events = [
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
+        snapshot: { x: [
+          {id: 'foo/0x1001', value: 'bar1'},
+          {id: 'foo/0x1002', value: 'bar2'},
+          {id: 'foo/0x1003', value: 'bar3'}
+        ]}}}
+    ];
+
+    var m = new tv.c.TraceModel();
+    m.importTraces([events], false);
+    var p1 = m.processes[1];
+
+    var sA = p1.objects.getSnapshotAt('0x1000', 15);
+    assert.isTrue(sA.args.x instanceof Array);
+    assert.equal(sA.args.x.length, 3);
+    assert.isTrue(sA.args.x[0] instanceof tv.c.trace_model.ObjectSnapshot);
+    assert.isTrue(sA.args.x[1] instanceof tv.c.trace_model.ObjectSnapshot);
+    assert.isTrue(sA.args.x[2] instanceof tv.c.trace_model.ObjectSnapshot);
+  });
+
+  test('importDoesNotMutateEventList', function() {
+    var events = [
+      // An object.
+      {ts: 10000, pid: 1, tid: 1, ph: 'N', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+      {ts: 15000, pid: 1, tid: 1, ph: 'O', cat: 'c', id: '0x1000', name: 'a', args: { // @suppress longLineCheck
+        snapshot: {foo: 15}}},
+      {ts: 50000, pid: 1, tid: 1, ph: 'D', cat: 'c', id: '0x1000', name: 'a', args: {}}, // @suppress longLineCheck
+
+      // A slice that references the object.
+      {ts: 17000, pid: 1, tid: 1, ph: 'B', cat: 'c', name: 'taskSlice', args: {
+        my_object: {id_ref: '0x1000'}}
+      },
+      {ts: 17500, pid: 1, tid: 1, ph: 'E', cat: 'c', name: 'taskSlice', args: {}} // @suppress longLineCheck
+    ];
+
+    // The A type family exists to mutate the args list provided to
+    // snapshots.
+    function ASnapshot() {
+      tv.c.trace_model.ObjectSnapshot.apply(this, arguments);
+      this.args.foo = 7;
+    }
+    ASnapshot.prototype = {
+      __proto__: tv.c.trace_model.ObjectSnapshot.prototype
+    };
+
+    // Import event while the A types are registered, causing the
+    // arguments of the snapshots to be mutated.
+    var m = new tv.c.TraceModel();
+    try {
+      tv.c.trace_model.ObjectSnapshot.register(ASnapshot, {typeName: 'a'});
+      m.importTraces([events], false);
+    } finally {
+      tv.c.trace_model.ObjectSnapshot.unregister(ASnapshot);
+    }
+    assert.isFalse(m.hasImportWarnings);
+
+    // Verify that the events array wasn't modified.
+    assert.deepEqual(
+        events[1].args,
+        {snapshot: {foo: 15}});
+    assert.deepEqual(
+        events[3].args,
+        {my_object: {id_ref: '0x1000'}});
+  });
+
+  test('importFlowEvent', function() {
+    var events = [
+      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} },  // @suppress longLineCheck
+      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} },  // @suppress longLineCheck
+      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} }   // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+
+    assert.isDefined(t);
+    assert.equal(m.flowEvents.length, 2);
+    assert.equal(m.flowIntervalTree.size, 2);
+
+    var f0 = m.flowEvents[0];
+    assert.equal(f0.title, 'a');
+    assert.equal(f0.category, 'foo');
+    assert.equal(f0.id, 72);
+    assert.equal(f0.start, 0);
+    assert.closeTo(12 / 1000, f0.duration, 1e-5);
+
+    // TODO(nduca): Replace this assertion with something better when
+    // flow events don't create synthetic slices on their own.
+    assert.isDefined(f0.startSlice);
+    assert.isDefined(f0.endSlice);
+
+    var f1 = m.flowEvents[1];
+    assert.equal(f1.title, f0.title);
+    assert.equal(f1.category, f0.category);
+    assert.equal(f1.id, f0.id);
+    assert.closeTo(20 / 1000, f1.duration, 1e-5);
+
+    // TODO(nduca): Replace this assertion with something better when
+    // flow events don't create synthetic slices on their own.
+    assert.isDefined(f1.startSlice);
+    assert.isDefined(f1.endSlice);
+  });
+
+  // This test creates a flow event that stops on the same timestamp that
+  // the 'X' event which it triggers begins.
+  test('importFlowEventOverlaps', function() {
+    var events = [
+      { name: 'PostTask', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {}},  // @suppress longLineCheck
+      { name: 'PostTask', cat: 'foo', id: 72, pid: 70, tid: 71, ts: 580, ph: 'f', args: { 'queue_duration': 0}},   // @suppress longLineCheck
+      // Note that RunTask has the same time-stamp as PostTask 'f'
+      { name: 'RunTask', cat: 'foo', pid: 70, tid: 71, ts: 580, ph: 'X', args: {'src_func': 'PostRunTask'}, 'dur': 1000}   // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(events, false, false);
+    var startT = m.processes[52].threads[53];
+    var endT = m.processes[70].threads[71];
+
+    assert.isDefined(startT);
+    assert.equal(startT.sliceGroup.slices.length, 1);
+
+    assert.isDefined(endT);
+    assert.equal(endT.sliceGroup.slices.length, 2);
+
+    assert.equal(m.flowEvents.length, 1);
+
+    // f0 represents 's' to 'f'
+    var f0 = m.flowEvents[0];
+
+    assert.equal(f0.title, 'PostTask');
+    assert.equal(f0.category, 'foo');
+    assert.equal(f0.id, 72);
+    assert.equal(f0.start, .548);
+    assert.closeTo(32 / 1000, f0.duration, 1e-5);
+
+    // TODO(nduca): Add assertions about the flow slices, esp that they were
+    // found correctly.
+  });
+
+  test('importOutOfOrderFlowEvent', function() {
+    var events = [
+      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 548, ph: 's', args: {} },  // @suppress longLineCheck
+      { name: 'b', cat: 'foo', id: 73, pid: 52, tid: 53, ts: 148, ph: 's', args: {} },  // @suppress longLineCheck
+      { name: 'b', cat: 'foo', id: 73, pid: 52, tid: 53, ts: 570, ph: 'f', args: {} },   // @suppress longLineCheck
+      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 560, ph: 't', args: {} },  // @suppress longLineCheck
+      { name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 580, ph: 'f', args: {} }   // @suppress longLineCheck
+    ];
+
+    var expected = [0.4, 0.0, 0.412];
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.flowIntervalTree.size, 3);
+
+    var order = m.flowEvents.map(function(x) { return x.start });
+    for (var i = 0; i < expected.length; ++i)
+      assert.closeTo(expected[i], order[i], 1e-5);
+  });
+
+  test('importCompleteEvent', function() {
+    var events = [
+      { name: 'a', args: {}, pid: 52, ts: 629, dur: 1, cat: 'baz', tid: 53, ph: 'X' },  // @suppress longLineCheck
+      { name: 'b', args: {}, pid: 52, ts: 730, dur: 20, cat: 'foo', tid: 53, ph: 'X' },  // @suppress longLineCheck
+      { name: 'c', args: {}, pid: 52, ts: 740, cat: 'baz', tid: 53, ph: 'X' }
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.slices.length, 3);
+    assert.equal(t.tid, 53);
+
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'baz');
+    assert.closeTo(0, slice.start, 1e-5);
+    assert.closeTo(1 / 1000, slice.duration, 1e-5);
+    assert.equal(slice.subSlices.length, 0);
+
+    slice = t.sliceGroup.slices[1];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'foo');
+    assert.closeTo((730 - 629) / 1000, slice.start, 1e-5);
+    assert.closeTo(20 / 1000, slice.duration, 1e-5);
+    assert.equal(slice.subSlices.length, 1);
+
+    slice = t.sliceGroup.slices[2];
+    assert.equal(slice.title, 'c');
+    assert.isTrue(slice.didNotFinish);
+    assert.closeTo(10 / 1000, slice.duration, 1e-5);
+  });
+
+  test('importCompleteEventWithCpuDuration', function() {
+    var events = [
+      { name: 'a', args: {}, pid: 52, ts: 629, dur: 1, cat: 'baz', tid: 53, ph: 'X', tts: 12, tdur: 1 },  // @suppress longLineCheck
+      { name: 'b', args: {}, pid: 52, ts: 730, dur: 20, cat: 'foo', tid: 53, ph: 'X', tts: 110, tdur: 16 },  // @suppress longLineCheck
+      { name: 'c', args: {}, pid: 52, ts: 740, cat: 'baz', tid: 53, ph: 'X', tts: 115 }  // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(events);
+    assert.equal(m.numProcesses, 1);
+    var p = m.processes[52];
+    assert.isDefined(p);
+
+    assert.equal(p.numThreads, 1);
+    var t = p.threads[53];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.slices.length, 3);
+    assert.equal(t.tid, 53);
+
+    var slice = t.sliceGroup.slices[0];
+    assert.equal(slice.title, 'a');
+    assert.equal(slice.category, 'baz');
+    assert.closeTo(0, slice.start, 1e-5);
+    assert.closeTo(1 / 1000, slice.duration, 1e-5);
+    assert.closeTo(12 / 1000, slice.cpuStart, 1e-5);
+    assert.closeTo(1 / 1000, slice.cpuDuration, 1e-5);
+    assert.equal(slice.subSlices.length, 0);
+
+    slice = t.sliceGroup.slices[1];
+    assert.equal(slice.title, 'b');
+    assert.equal(slice.category, 'foo');
+    assert.closeTo((730 - 629) / 1000, slice.start, 1e-5);
+    assert.closeTo(20 / 1000, slice.duration, 1e-5);
+    assert.closeTo(110 / 1000, slice.cpuStart, 1e-5);
+    assert.closeTo(16 / 1000, slice.cpuDuration, 1e-5);
+    assert.equal(slice.subSlices.length, 1);
+
+    slice = t.sliceGroup.slices[2];
+    assert.equal(slice.title, 'c');
+    assert.isTrue(slice.didNotFinish);
+    assert.closeTo(10 / 1000, slice.duration, 1e-5);
+  });
+
+  test('importNestedCompleteEventWithTightBounds', function() {
+    var events = [
+      { name: 'a', args: {}, pid: 52, ts: 244654227065, dur: 36075, cat: 'baz', tid: 53, ph: 'X' },  // @suppress longLineCheck
+      { name: 'b', args: {}, pid: 52, ts: 244654227095, dur: 36045, cat: 'foo', tid: 53, ph: 'X' }  // @suppress longLineCheck
+    ];
+
+    var m = new tv.c.TraceModel(events, false);
+    var t = m.processes[52].threads[53];
+
+    var sA = findSliceNamed(t.sliceGroup, 'a');
+    var sB = findSliceNamed(t.sliceGroup, 'b');
+
+    assert.equal(sA.title, 'a');
+    assert.equal(sA.category, 'baz');
+    assert.equal(sA.start, 244654227.065);
+    assert.equal(sA.duration, 36.075);
+    assert.closeTo(0.03, sA.selfTime, 1e-5);
+
+    assert.equal(sB.title, 'b');
+    assert.equal(sB.category, 'foo');
+    assert.equal(sB.start, 244654227.095);
+    assert.equal(sB.duration, 36.045);
+
+    assert.equal(sA.subSlices.length, 1);
+    assert.equal(sA.subSlices[0], sB);
+    assert.equal(sB.parentSlice, sA);
+  });
+
+
+  test('importCompleteEventWithStackFrame', function() {
+    var eventData = {
+      traceEvents: [
+        { name: 'a', args: {}, pid: 1, ts: 0, dur: 1, cat: 'baz', tid: 2, ph: 'X', sf: 7 }, // @suppress longLineCheck
+        { name: 'b', args: {}, pid: 1, ts: 5, dur: 1, cat: 'baz', tid: 2, ph: 'X', sf: 8, esf: 9 } // @suppress longLineCheck
+      ],
+      stackFrames: {
+        '1': {
+          category: 'm1',
+          name: 'main'
+        },
+        '7': {
+          category: 'm2',
+          name: 'frame7',
+          parent: '1'
+        },
+        '8': {
+          category: 'm2',
+          name: 'frame8',
+          parent: '1'
+        },
+        '9': {
+          category: 'm2',
+          name: 'frame9',
+          parent: '1'
+        }
+      }
+    };
+
+    var options = new tv.c.ImportOptions();
+    options.shiftWorldToZero = false;
+    var m = new tv.c.TraceModel(eventData, options);
+
+    var p = m.processes[1];
+    var t = p.threads[2];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.slices.length, 2);
+
+    var s0 = t.sliceGroup.slices[0];
+    assert.equal(s0.startStackFrame.title, 'frame7');
+    assert.isUndefined(s0.endStackFrame);
+
+    var s1 = t.sliceGroup.slices[1];
+    assert.equal(s1.startStackFrame.title, 'frame8');
+    assert.equal(s1.endStackFrame.title, 'frame9');
+  });
+
+  test('importAsyncEventWithSameTimestamp', function() {
+    var events = [];
+    // Events are added with ts 0, 1, 1, 2, 2, 3, 3 ...500, 500, 1000
+    // and use 'seq' to track the order of when the event is recorded.
+    events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 0, ph: 'S', args: {'seq': 0}});  // @suppress longLineCheck
+
+    for (var i = 1; i <= 1000; i++)
+      events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: Math.round(i / 2) , ph: 'T', args: {'seq': i}});  // @suppress longLineCheck
+
+    events.push({name: 'a', cat: 'foo', id: 72, pid: 52, tid: 53, ts: 1000, ph: 'F', args: {'seq': 1001}});  // @suppress longLineCheck
+
+    var m = new tv.c.TraceModel(events);
+    var t = m.processes[52].threads[53];
+
+    assert.equal(t.asyncSliceGroup.slices.length, 1);
+    var parentSlice = t.asyncSliceGroup.slices[0];
+    assert.equal(parentSlice.title, 'a');
+    assert.equal(parentSlice.category, 'foo');
+    assert.isTrue(parentSlice.isTopLevel);
+
+    assert.isDefined(parentSlice.subSlices);
+    var subSlices = parentSlice.subSlices;
+    assert.equal(subSlices.length, 1000);
+    // Slices should be sorted according to 'ts'. And if 'ts' is the same,
+    // slices should keep the order that they were recorded.
+    for (var i = 0; i < 1000; i++) {
+      assert.equal(i + 1, subSlices[i].args['seq']);
+      assert.isFalse(subSlices[i].isTopLevel);
+    }
+  });
+
+  test('sampleDataSimple', function() {
+    var events = {
+      'traceEvents': [],
+      'stackFrames': {
+        '1': {
+          'category': 'mod',
+          'name': 'main'
+        },
+        '2': {
+          'category': 'mod',
+          'name': 'a',
+          'parent': 1
+        },
+        '3': {
+          'category': 'mod',
+          'name': 'a_sub',
+          'parent': 2
+        },
+        '4': {
+          'category': 'mod',
+          'name': 'b',
+          'parent': 1
+        }
+      },
+      'samples': [
+        {
+          'cpu': 0, 'tid': 1, 'ts': 1000.0,
+          'name': 'cycles:HG', 'sf': 3, 'weight': 1
+        },
+        {
+          'cpu': 0, 'tid': 1, 'ts': 2000.0,
+          'name': 'cycles:HG', 'sf': 2, 'weight': 1
+        },
+        {
+          'cpu': 1, 'tid': 1, 'ts': 3000.0,
+          'name': 'cycles:HG', 'sf': 3, 'weight': 1
+        }
+      ]
+    };
+    var m = new tv.c.TraceModel(events, false);
+    assert.isDefined(m.kernel.cpus[0]);
+    assert.equal(m.getAllThreads().length, 1);
+
+    assert.equal(tv.b.dictionaryKeys(m.stackFrames).length, 4);
+    assert.equal(m.samples.length, 3);
+
+    var t1 = m.processes[1].threads[1];
+    assert.equal(t1.samples.length, 3);
+
+    var c0 = m.kernel.cpus[0];
+    var c1 = m.kernel.cpus[1];
+    assert.equal(c0.samples.length, 2);
+    assert.equal(c1.samples.length, 1);
+
+    assert.equal(m.samples[0].cpu, c0);
+    assert.equal(m.samples[0].thread, t1);
+    assert.equal(m.samples[0].title, 'cycles:HG');
+    assert.equal(m.samples[0].start, 1);
+    assert.deepEqual(
+        ['main', 'a', 'a_sub'],
+        m.samples[0].stackTrace.map(function(x) { return x.title; }));
+    assert.equal(m.samples[0].weight, 1);
+  });
+
+  test('importMemoryDumps_verifyProcessAndGlobalDumpLinks', function() {
+    var events = [
+      // 2 process memory dump events without a global memory dump event.
+      {
+        name: 'a',
+        pid: 42,
+        ts: 10,
+        cat: 'test',
+        tid: 53,
+        ph: 'v',
+        id: '0x0001',
+        args: {
+          dumps: {
+            process_totals: {
+              resident_set_bytes: '100'
+            }
+          }
+        }
+      },
+      {
+        name: 'b',
+        pid: 43,
+        ts: 11,
+        cat: 'test',
+        tid: 54,
+        ph: 'v',
+        id: '0x0001',
+        args: {
+          dumps: {
+            process_totals: {
+              resident_set_bytes: '200'
+            }
+          }
+        }
+      },
+      // 1 global memory dump event and 1 process memory dump event.
+      {
+        name: 'c',
+        pid: 44,
+        ts: 12,
+        cat: 'test',
+        tid: 55,
+        ph: 'V',
+        id: '0xfffffff12345678',
+        args: {}
+      },
+      {
+        name: 'd',
+        pid: 42,
+        ts: 13,
+        cat: 'test',
+        tid: 56,
+        ph: 'v',
+        id: '0xfffffff12345678',
+        args: {
+          dumps: {
+            process_totals: {
+              resident_set_bytes: '300'
+            }
+          }
+        }
+      }
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p1 = m.getProcess(42);
+    var p2 = m.getProcess(43);
+    assert.isDefined(p1);
+    assert.isDefined(p2);
+
+    // Check that TraceModel and Process objects contain the right dumps.
+    assert.equal(m.globalMemoryDumps.length, 2);
+    assert.equal(p1.memoryDumps.length, 2);
+    assert.equal(p2.memoryDumps.length, 1);
+
+    assert.equal(m.globalMemoryDumps[0].start, 10);
+    assert.equal(p1.memoryDumps[0].start, 10);
+    assert.equal(p2.memoryDumps[0].start, 11);
+    assert.equal(m.globalMemoryDumps[0].duration, 1);
+    assert.equal(p1.memoryDumps[0].duration, 0);
+    assert.equal(p2.memoryDumps[0].duration, 0);
+
+    assert.equal(m.globalMemoryDumps[1].start, 12);
+    assert.equal(p1.memoryDumps[1].start, 13);
+    assert.equal(m.globalMemoryDumps[1].duration, 1);
+    assert.equal(p1.memoryDumps[1].duration, 0);
+
+    // Check that GlobalMemoryDump and ProcessMemoryDump objects are
+    // interconnected correctly.
+    assert.equal(p1.memoryDumps[0],
+        m.globalMemoryDumps[0].processMemoryDumps[42]);
+    assert.equal(p2.memoryDumps[0],
+        m.globalMemoryDumps[0].processMemoryDumps[43]);
+    assert.equal(p1.memoryDumps[0].globalMemoryDump, m.globalMemoryDumps[0]);
+    assert.equal(p2.memoryDumps[0].globalMemoryDump, m.globalMemoryDumps[0]);
+
+    assert.equal(p1.memoryDumps[1],
+        m.globalMemoryDumps[1].processMemoryDumps[42]);
+    assert.equal(p1.memoryDumps[1].globalMemoryDump, m.globalMemoryDumps[1]);
+  });
+
+  test('importMemoryDumps_totalResidentBytesOnly', function() {
+    var events = [
+      {
+        name: 'some_dump_name',
+        pid: 42,
+        ts: 10,
+        cat: 'test',
+        tid: 53,
+        ph: 'v',
+        id: '0x01',
+        args: {
+          dumps: {
+            process_totals: {
+              resident_set_bytes: '1fffffffffffff'
+            }
+          }
+        }
+      }
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p = m.getProcess(42);
+    var d = p.memoryDumps[0];
+
+    assert.equal(d.totalResidentBytes, 9007199254740991);
+    assert.isUndefined(d.vmRegions);
+    assert.equal(d.memoryAllocatorDumps.length, 0);
+    assert.equal(Object.keys(d.memoryAllocatorDumpsByFullName).length, 0);
+  });
+
+  test('importMemoryDumps_vmRegions', function() {
+    var events = [
+      {
+        name: 'some_dump_name',
+        pid: 42,
+        ts: 10,
+        cat: 'test',
+        tid: 53,
+        ph: 'v',
+        id: '000',
+        args: {
+          dumps: {
+            process_totals: {
+              resident_set_bytes: '0'
+            },
+            process_mmaps: {
+              vm_regions: [
+                {
+                  sa: 'f0',
+                  sz: '150',
+                  pf: 6,
+                  mf: '[stack:20310]',
+                  bs: {
+                    pss: '9e',
+                    prv: '60',
+                    shr: '50'
+                  }
+                },
+                {
+                  sa: '350',
+                  sz: '250',
+                  pf: 5,
+                  mf: '/dev/ashmem/dalvik',
+                  bs: {
+                    pss: 'cd',
+                    prv: 'cd',
+                    shr: '0'
+                  }
+                }
+              ]
+            }
+          }
+        }
+      }
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p = m.getProcess(42);
+    var d = p.memoryDumps[0];
+
+    assert.equal(d.vmRegions.length, 2);
+
+    var vr1 = d.vmRegions[0];
+    assert.equal(vr1.startAddress, 240);
+    assert.equal(vr1.sizeInBytes, 336);
+    assert.equal(vr1.protectionFlags, 6);
+    assert.equal(vr1.protectionFlagsToString, 'rw-');
+    assert.equal(vr1.mappedFile, '[stack:20310]');
+    assert.equal(vr1.byteStats.privateResident, 96);
+    assert.equal(vr1.byteStats.sharedResident, 80);
+    assert.equal(vr1.byteStats.proportionalResident, 158);
+    assert.equal(vr1.byteStats.totalResident, 176);
+
+    var vr2 = d.vmRegions[1];
+    assert.equal(vr2.startAddress, 848);
+    assert.equal(vr2.sizeInBytes, 592);
+    assert.equal(vr2.protectionFlags, 5);
+    assert.equal(vr2.protectionFlagsToString, 'r-x');
+    assert.equal(vr2.mappedFile, '/dev/ashmem/dalvik');
+    assert.equal(vr2.byteStats.privateResident, 205);
+    assert.equal(vr2.byteStats.sharedResident, 0);
+    assert.equal(vr2.byteStats.proportionalResident, 205);
+    assert.equal(vr2.byteStats.totalResident, 205);
+
+    assert.equal(d.totalResidentBytes, 0);
+    assert.equal(d.memoryAllocatorDumps.length, 0);
+    assert.equal(Object.keys(d.memoryAllocatorDumpsByFullName).length, 0);
+  });
+
+  test('importMemoryDumps_allocatorMemoryDumps', function() {
+    var events = [
+      {
+        name: 'a',
+        pid: 42,
+        ts: 10,
+        cat: 'test',
+        tid: 53,
+        ph: 'v',
+        id: '0x0001',
+        args: {
+          dumps: {
+            process_totals: {
+              resident_set_bytes: '100'
+            },
+            allocators: {
+              'oilpan': {
+                allocated_objects_count: '2f',
+                allocated_objects_size_in_bytes: '1000',
+                physical_size_in_bytes: '4000',
+                args: {}
+              },
+              'oilpan/heap1': {
+                parent: 'oilpan',
+                allocated_objects_count: '3f',
+                allocated_objects_size_in_bytes: '3000',
+                physical_size_in_bytes: '4000',
+                args: {}
+              },
+              'oilpan/heap2': {
+                parent: 'oilpan',
+                allocated_objects_count: '4f',
+                allocated_objects_size_in_bytes: '4000',
+                physical_size_in_bytes: '4000',
+                args: {}
+              },
+              'oilpan/heap2/bucket1': {
+                parent: 'oilpan/heap2',
+                allocated_objects_count: '1f',
+                allocated_objects_size_in_bytes: '2000',
+                physical_size_in_bytes: '2000',
+                args: {}
+              },
+              'v8': {
+                allocated_objects_count: '5f',
+                allocated_objects_size_in_bytes: '5000',
+                physical_size_in_bytes: '6000',
+                args: {}
+              }
+            }
+          }
+        }
+      }
+    ];
+    var m = new tv.c.TraceModel(events, false);
+    var p = m.getProcess(42);
+    var d = p.memoryDumps[0];
+
+    assert.equal(Object.keys(d.memoryAllocatorDumpsByFullName).length, 5);
+    assert.equal(d.memoryAllocatorDumps.length, 2);
+
+    var oilpanRoot = d.memoryAllocatorDumpsByFullName['oilpan'];
+    var v8Root = d.memoryAllocatorDumpsByFullName['v8'];
+    assert.isDefined(oilpanRoot);
+    assert.isDefined(v8Root);
+    assert.include(d.memoryAllocatorDumps, oilpanRoot);
+    assert.include(d.memoryAllocatorDumps, v8Root);
+
+    assert.equal(oilpanRoot.allocatedObjectsCount, 47);
+    assert.equal(oilpanRoot.physicalSizeInBytes, 16384);
+    assert.equal(oilpanRoot.allocatedObjectsSizeInBytes, 4096);
+    assert.equal(oilpanRoot.children.length, 2);
+
+    var oilpanBucket1 = d.memoryAllocatorDumpsByFullName[
+        'oilpan/heap2/bucket1'];
+    assert.isDefined(oilpanBucket1);
+    assert.equal(oilpanBucket1.fullName, 'oilpan/heap2/bucket1');
+    assert.equal(oilpanBucket1.name, 'bucket1');
+    assert.equal(oilpanBucket1.allocatedObjectsCount, 31);
+    assert.equal(oilpanBucket1.physicalSizeInBytes, 8192);
+    assert.equal(oilpanBucket1.allocatedObjectsSizeInBytes, 8192);
+    assert.equal(oilpanBucket1.children.length, 0);
+
+    assert.isDefined(oilpanBucket1.parent);
+    assert.equal(oilpanBucket1.parent.fullName, 'oilpan/heap2');
+    assert.equal(oilpanBucket1.parent.name, 'heap2');
+    assert.include(oilpanBucket1.parent.children, oilpanBucket1);
+
+    assert.isDefined(oilpanBucket1.parent.parent);
+    assert.strictEqual(oilpanBucket1.parent.parent, oilpanRoot);
+
+    assert.equal(d.totalResidentBytes, 256);
+    assert.isUndefined(d.vmRegions);
+  });
+
+  test('importThreadInstantSliceWithStackFrame', function() {
+    var eventData = {
+      traceEvents: [
+        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'I', s: 't', sf: 7 } // @suppress longLineCheck
+      ],
+      stackFrames: {
+        '1': {
+          category: 'm1',
+          name: 'main'
+        },
+        '7': {
+          category: 'm2',
+          name: 'frame7',
+          parent: '1'
+        }
+      }
+    };
+
+    var options = new tv.c.ImportOptions();
+    options.shiftWorldToZero = false;
+    var m = new tv.c.TraceModel(eventData, options);
+
+    var p = m.processes[1];
+    var t = p.threads[2];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.slices.length, 1);
+
+    var s0 = t.sliceGroup.slices[0];
+    assert.equal(s0.startStackFrame.title, 'frame7');
+    assert.isUndefined(s0.endStackFrame);
+  });
+
+  test('importDurationEventsWithStackFrames', function() {
+    var eventData = {
+      traceEvents: [
+        { name: 'a', args: {}, pid: 1, ts: 0, cat: 'baz', tid: 2, ph: 'B', sf: 7 }, // @suppress longLineCheck
+        { name: 'b', args: {}, pid: 1, ts: 5, cat: 'baz', tid: 2, ph: 'E', sf: 8 } // @suppress longLineCheck
+      ],
+      stackFrames: {
+        '1': {
+          category: 'm1',
+          name: 'main'
+        },
+        '7': {
+          category: 'm2',
+          name: 'frame7',
+          parent: '1'
+        },
+        '8': {
+          category: 'm2',
+          name: 'frame8',
+          parent: '1'
+        }
+      }
+    };
+
+    var options = new tv.c.ImportOptions();
+    options.shiftWorldToZero = false;
+    var m = new tv.c.TraceModel(eventData, options);
+
+    var p = m.processes[1];
+    var t = p.threads[2];
+    assert.isDefined(t);
+    assert.equal(t.sliceGroup.slices.length, 1);
+
+    var s0 = t.sliceGroup.slices[0];
+    assert.equal(s0.startStackFrame.title, 'frame7');
+    assert.equal(s0.endStackFrame.title, 'frame8');
+  });
+
+  // TODO(nduca): one slice, two threads
+  // TODO(nduca): one slice, two pids
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/v8/codemap.html b/trace-viewer/trace_viewer/extras/importer/v8/codemap.html
new file mode 100644
index 0000000..2c46f63
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/v8/codemap.html
@@ -0,0 +1,265 @@
+<!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="/extras/importer/v8/splaytree.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview Map addresses to dynamically created functions.
+ */
+tv.exportTo('tv.e.importer.v8', function() {
+  /**
+   * Constructs a mapper that maps addresses into code entries.
+   *
+   * @constructor
+   */
+  function CodeMap() {
+    /**
+     * Dynamic code entries. Used for JIT compiled code.
+     */
+    this.dynamics_ = new tv.e.importer.v8.SplayTree();
+
+    /**
+     * Name generator for entries having duplicate names.
+     */
+    this.dynamicsNameGen_ = new tv.e.importer.v8.CodeMap.NameGenerator();
+
+    /**
+     * Static code entries. Used for statically compiled code.
+     */
+    this.statics_ = new tv.e.importer.v8.SplayTree();
+
+    /**
+     * Libraries entries. Used for the whole static code libraries.
+     */
+    this.libraries_ = new tv.e.importer.v8.SplayTree();
+
+    /**
+     * Map of memory pages occupied with static code.
+     */
+    this.pages_ = [];
+  };
+
+  /**
+   * The number of alignment bits in a page address.
+   */
+  CodeMap.PAGE_ALIGNMENT = 12;
+
+  /**
+   * Page size in bytes.
+   */
+  CodeMap.PAGE_SIZE = 1 << CodeMap.PAGE_ALIGNMENT;
+
+  /**
+   * Adds a dynamic (i.e. moveable and discardable) code entry.
+   *
+   * @param {number} start The starting address.
+   * @param {CodeMap.CodeEntry} codeEntry Code entry object.
+   */
+  CodeMap.prototype.addCode = function(start, codeEntry) {
+    this.deleteAllCoveredNodes_(this.dynamics_, start, start + codeEntry.size);
+    this.dynamics_.insert(start, codeEntry);
+  };
+
+  /**
+   * Moves a dynamic code entry. Throws an exception if there is no dynamic
+   * code entry with the specified starting address.
+   *
+   * @param {number} from The starting address of the entry being moved.
+   * @param {number} to The destination address.
+   */
+  CodeMap.prototype.moveCode = function(from, to) {
+    var removedNode = this.dynamics_.remove(from);
+    this.deleteAllCoveredNodes_(this.dynamics_, to,
+                                to + removedNode.value.size);
+    this.dynamics_.insert(to, removedNode.value);
+  };
+
+  /**
+   * Discards a dynamic code entry. Throws an exception if there is no dynamic
+   * code entry with the specified starting address.
+   *
+   * @param {number} start The starting address of the entry being deleted.
+   */
+  CodeMap.prototype.deleteCode = function(start) {
+    var removedNode = this.dynamics_.remove(start);
+  };
+
+  /**
+   * Adds a library entry.
+   *
+   * @param {number} start The starting address.
+   * @param {CodeMap.CodeEntry} codeEntry Code entry object.
+   */
+  CodeMap.prototype.addLibrary = function(
+      start, codeEntry) {
+    this.markPages_(start, start + codeEntry.size);
+    this.libraries_.insert(start, codeEntry);
+  };
+
+  /**
+   * Adds a static code entry.
+   *
+   * @param {number} start The starting address.
+   * @param {CodeMap.CodeEntry} codeEntry Code entry object.
+   */
+  CodeMap.prototype.addStaticCode = function(
+      start, codeEntry) {
+    this.statics_.insert(start, codeEntry);
+  };
+
+  /**
+   * @private
+   */
+  CodeMap.prototype.markPages_ = function(start, end) {
+    for (var addr = start; addr <= end;
+         addr += CodeMap.PAGE_SIZE) {
+      this.pages_[addr >>> CodeMap.PAGE_ALIGNMENT] = 1;
+    }
+  };
+
+  /**
+   * @private
+   */
+  CodeMap.prototype.deleteAllCoveredNodes_ = function(tree, start, end) {
+    var to_delete = [];
+    var addr = end - 1;
+    while (addr >= start) {
+      var node = tree.findGreatestLessThan(addr);
+      if (!node) break;
+      var start2 = node.key, end2 = start2 + node.value.size;
+      if (start2 < end && start < end2) to_delete.push(start2);
+      addr = start2 - 1;
+    }
+    for (var i = 0, l = to_delete.length; i < l; ++i) tree.remove(to_delete[i]);
+  };
+
+  /**
+   * @private
+   */
+  CodeMap.prototype.isAddressBelongsTo_ = function(addr, node) {
+    return addr >= node.key && addr < (node.key + node.value.size);
+  };
+
+  /**
+   * @private
+   */
+  CodeMap.prototype.findInTree_ = function(tree, addr) {
+    var node = tree.findGreatestLessThan(addr);
+    return node && this.isAddressBelongsTo_(addr, node) ? node.value : null;
+  };
+
+  /**
+   * Finds a code entry that contains the specified address. Both static and
+   * dynamic code entries are considered.
+   *
+   * @param {number} addr Address.
+   */
+  CodeMap.prototype.findEntry = function(addr) {
+    var pageAddr = addr >>> CodeMap.PAGE_ALIGNMENT;
+    if (pageAddr in this.pages_) {
+      // Static code entries can contain "holes" of unnamed code.
+      // In this case, the whole library is assigned to this address.
+      return this.findInTree_(this.statics_, addr) ||
+          this.findInTree_(this.libraries_, addr);
+    }
+    var min = this.dynamics_.findMin();
+    var max = this.dynamics_.findMax();
+    if (max != null && addr < (max.key + max.value.size) && addr >= min.key) {
+      var dynaEntry = this.findInTree_(this.dynamics_, addr);
+      if (dynaEntry == null) return null;
+      // Dedupe entry name.
+      if (!dynaEntry.nameUpdated_) {
+        dynaEntry.name = this.dynamicsNameGen_.getName(dynaEntry.name);
+        dynaEntry.nameUpdated_ = true;
+      }
+      return dynaEntry;
+    }
+    return null;
+  };
+
+  /**
+   * Returns a dynamic code entry using its starting address.
+   *
+   * @param {number} addr Address.
+   */
+  CodeMap.prototype.findDynamicEntryByStartAddress =
+      function(addr) {
+    var node = this.dynamics_.find(addr);
+    return node ? node.value : null;
+  };
+
+  /**
+   * Returns an array of all dynamic code entries.
+   */
+  CodeMap.prototype.getAllDynamicEntries = function() {
+    return this.dynamics_.exportValues();
+  };
+
+  /**
+   * Returns an array of pairs of all dynamic code entries and their addresses.
+   */
+  CodeMap.prototype.getAllDynamicEntriesWithAddresses = function() {
+    return this.dynamics_.exportKeysAndValues();
+  };
+
+  /**
+   * Returns an array of all static code entries.
+   */
+  CodeMap.prototype.getAllStaticEntries = function() {
+    return this.statics_.exportValues();
+  };
+
+  /**
+   * Returns an array of all libraries entries.
+   */
+  CodeMap.prototype.getAllLibrariesEntries = function() {
+    return this.libraries_.exportValues();
+  };
+
+  /**
+   * Creates a code entry object.
+   *
+   * @param {number} size Code entry size in bytes.
+   * @param {string=} opt_name Code entry name.
+   * @constructor
+   */
+  CodeMap.CodeEntry = function(size, opt_name) {
+    this.id = tv.b.GUID.allocate();
+    this.size = size;
+    this.name = opt_name || '';
+    this.nameUpdated_ = false;
+  };
+
+  CodeMap.CodeEntry.prototype.getName = function() {
+    return this.name;
+  };
+
+  CodeMap.CodeEntry.prototype.toString = function() {
+    return this.name + ': ' + this.size.toString(16);
+  };
+
+  CodeMap.NameGenerator = function() {
+    this.knownNames_ = {};
+  };
+
+  CodeMap.NameGenerator.prototype.getName = function(name) {
+    if (!(name in this.knownNames_)) {
+      this.knownNames_[name] = 0;
+      return name;
+    }
+    var count = ++this.knownNames_[name];
+    return name + ' {' + count + '}';
+  };
+  return {
+    CodeMap: CodeMap
+  };
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/v8/log_reader.html b/trace-viewer/trace_viewer/extras/importer/v8/log_reader.html
new file mode 100644
index 0000000..a6c987f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/v8/log_reader.html
@@ -0,0 +1,213 @@
+<!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 Log Reader is used to process log file produced by V8.
+ */
+tv.exportTo('tv.e.importer.v8', function() {
+  /**
+   * Creates a CSV lines parser.
+   */
+  function CsvParser() { };
+
+  /**
+   * A regex for matching a CSV field.
+   * @private
+   */
+  CsvParser.CSV_FIELD_RE_ = /^"((?:[^"]|"")*)"|([^,]*)/;
+
+  /**
+   * A regex for matching a double quote.
+   * @private
+   */
+  CsvParser.DOUBLE_QUOTE_RE_ = /""/g;
+
+  /**
+   * Parses a line of CSV-encoded values. Returns an array of fields.
+   *
+   * @param {string} line Input line.
+   */
+  CsvParser.prototype.parseLine = function(line) {
+    var fieldRe = CsvParser.CSV_FIELD_RE_;
+    var doubleQuoteRe = CsvParser.DOUBLE_QUOTE_RE_;
+    var pos = 0;
+    var endPos = line.length;
+    var fields = [];
+    if (endPos > 0) {
+      do {
+        var fieldMatch = fieldRe.exec(line.substr(pos));
+        if (typeof fieldMatch[1] === 'string') {
+          var field = fieldMatch[1];
+          pos += field.length + 3;  // Skip comma and quotes.
+          fields.push(field.replace(doubleQuoteRe, '"'));
+        } else {
+          // The second field pattern will match anything, thus
+          // in the worst case the match will be an empty string.
+          var field = fieldMatch[2];
+          pos += field.length + 1;  // Skip comma.
+          fields.push(field);
+        }
+      } while (pos <= endPos);
+    }
+    return fields;
+  };
+
+  /**
+   * Base class for processing log files.
+   *
+   * @param {Array.<Object>} dispatchTable A table used for parsing and
+   * processing log records.
+   *
+   * @constructor
+   */
+  function LogReader(dispatchTable) {
+    /**
+     * @type {Array.<Object>}
+     */
+    this.dispatchTable_ = dispatchTable;
+
+    /**
+     * Current line.
+     * @type {number}
+     */
+    this.lineNum_ = 0;
+
+    /**
+     * CSV lines parser.
+     * @type {CsvParser}
+     */
+    this.csvParser_ = new CsvParser();
+  };
+
+  /**
+   * Used for printing error messages.
+   *
+   * @param {string} str Error message.
+   */
+  LogReader.prototype.printError = function(str) {
+    // Do nothing.
+  };
+
+  /**
+   * Processes a portion of V8 profiler event log.
+   *
+   * @param {string} chunk A portion of log.
+   */
+  LogReader.prototype.processLogChunk = function(chunk) {
+    this.processLog_(chunk.split('\n'));
+  };
+
+  /**
+   * Processes a line of V8 profiler event log.
+   *
+   * @param {string} line A line of log.
+   */
+  LogReader.prototype.processLogLine = function(line) {
+    this.processLog_([line]);
+  };
+
+  /**
+   * Processes stack record.
+   *
+   * @param {number} pc Program counter.
+   * @param {number} func JS Function.
+   * @param {Array.<string>} stack String representation of a stack.
+   * @return {Array.<number>} Processed stack.
+   */
+  LogReader.prototype.processStack = function(pc, func, stack) {
+    var fullStack = func ? [pc, func] : [pc];
+    var prevFrame = pc;
+    for (var i = 0, n = stack.length; i < n; ++i) {
+      var frame = stack[i];
+      var firstChar = frame.charAt(0);
+      if (firstChar == '+' || firstChar == '-') {
+        // An offset from the previous frame.
+        prevFrame += parseInt(frame, 16);
+        fullStack.push(prevFrame);
+      // Filter out possible 'overflow' string.
+      } else if (firstChar != 'o') {
+        fullStack.push(parseInt(frame, 16));
+      }
+    }
+    return fullStack;
+  };
+
+  /**
+   * Returns whether a particular dispatch must be skipped.
+   *
+   * @param {!Object} dispatch Dispatch record.
+   * @return {boolean} True if dispatch must be skipped.
+   */
+  LogReader.prototype.skipDispatch = function(dispatch) {
+    return false;
+  };
+
+  /**
+   * Does a dispatch of a log record.
+   *
+   * @param {Array.<string>} fields Log record.
+   * @private
+   */
+  LogReader.prototype.dispatchLogRow_ = function(fields) {
+    // Obtain the dispatch.
+    var command = fields[0];
+    if (!(command in this.dispatchTable_)) return;
+
+    var dispatch = this.dispatchTable_[command];
+
+    if (dispatch === null || this.skipDispatch(dispatch)) {
+      return;
+    }
+
+    // Parse fields.
+    var parsedFields = [];
+    for (var i = 0; i < dispatch.parsers.length; ++i) {
+      var parser = dispatch.parsers[i];
+      if (parser === null) {
+        parsedFields.push(fields[1 + i]);
+      } else if (typeof parser == 'function') {
+        parsedFields.push(parser(fields[1 + i]));
+      } else {
+        // var-args
+        parsedFields.push(fields.slice(1 + i));
+        break;
+      }
+    }
+
+    // Run the processor.
+    dispatch.processor.apply(this, parsedFields);
+  };
+
+  /**
+   * Processes log lines.
+   *
+   * @param {Array.<string>} lines Log lines.
+   * @private
+   */
+  LogReader.prototype.processLog_ = function(lines) {
+    for (var i = 0, n = lines.length; i < n; ++i, ++this.lineNum_) {
+      var line = lines[i];
+      if (!line) {
+        continue;
+      }
+      try {
+        var fields = this.csvParser_.parseLine(line);
+        this.dispatchLogRow_(fields);
+      } catch (e) {
+        this.printError('line ' + (this.lineNum_ + 1) + ': ' +
+                        (e.message || e));
+      }
+    }
+  };
+  return {
+    LogReader: LogReader
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/v8/splaytree.html b/trace-viewer/trace_viewer/extras/importer/v8/splaytree.html
new file mode 100644
index 0000000..61df71a
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/v8/splaytree.html
@@ -0,0 +1,306 @@
+<!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 Splay tree used by CodeMap.
+ */
+tv.exportTo('tv.e.importer.v8', function() {
+  /**
+   * Constructs a Splay tree.  A splay tree is a self-balancing binary
+   * search tree with the additional property that recently accessed
+   * elements are quick to access again. It performs basic operations
+   * such as insertion, look-up and removal in O(log(n)) amortized time.
+   *
+   * @constructor
+   */
+  function SplayTree() { };
+
+  /**
+   * Pointer to the root node of the tree.
+   *
+   * @type {SplayTree.Node}
+   * @private
+   */
+  SplayTree.prototype.root_ = null;
+
+  /**
+   * @return {boolean} Whether the tree is empty.
+   */
+  SplayTree.prototype.isEmpty = function() {
+    return !this.root_;
+  };
+
+  /**
+   * Inserts a node into the tree with the specified key and value if
+   * the tree does not already contain a node with the specified key. If
+   * the value is inserted, it becomes the root of the tree.
+   *
+   * @param {number} key Key to insert into the tree.
+   * @param {*} value Value to insert into the tree.
+   */
+  SplayTree.prototype.insert = function(key, value) {
+    if (this.isEmpty()) {
+      this.root_ = new SplayTree.Node(key, value);
+      return;
+    }
+    // Splay on the key to move the last node on the search path for
+    // the key to the root of the tree.
+    this.splay_(key);
+    if (this.root_.key == key) {
+      return;
+    }
+    var node = new SplayTree.Node(key, value);
+    if (key > this.root_.key) {
+      node.left = this.root_;
+      node.right = this.root_.right;
+      this.root_.right = null;
+    } else {
+      node.right = this.root_;
+      node.left = this.root_.left;
+      this.root_.left = null;
+    }
+    this.root_ = node;
+  };
+
+
+  /**
+   * Removes a node with the specified key from the tree if the tree
+   * contains a node with this key. The removed node is returned. If the
+   * key is not found, an exception is thrown.
+   *
+   * @param {number} key Key to find and remove from the tree.
+   * @return {SplayTree.Node} The removed node.
+   */
+  SplayTree.prototype.remove = function(key) {
+    if (this.isEmpty()) {
+      throw Error('Key not found: ' + key);
+    }
+    this.splay_(key);
+    if (this.root_.key != key) {
+      throw Error('Key not found: ' + key);
+    }
+    var removed = this.root_;
+    if (!this.root_.left) {
+      this.root_ = this.root_.right;
+    } else {
+      var right = this.root_.right;
+      this.root_ = this.root_.left;
+      // Splay to make sure that the new root has an empty right child.
+      this.splay_(key);
+      // Insert the original right child as the right child of the new
+      // root.
+      this.root_.right = right;
+    }
+    return removed;
+  };
+
+
+  /**
+   * Returns the node having the specified key or null if the tree doesn't
+   * contain a node with the specified key.
+   *
+   *
+   * @param {number} key Key to find in the tree.
+   * @return {SplayTree.Node} Node having the specified key.
+   */
+  SplayTree.prototype.find = function(key) {
+    if (this.isEmpty()) {
+      return null;
+    }
+    this.splay_(key);
+    return this.root_.key == key ? this.root_ : null;
+  };
+
+  /**
+   * @return {SplayTree.Node} Node having the minimum key value.
+   */
+  SplayTree.prototype.findMin = function() {
+    if (this.isEmpty()) {
+      return null;
+    }
+    var current = this.root_;
+    while (current.left) {
+      current = current.left;
+    }
+    return current;
+  };
+
+  /**
+   * @return {SplayTree.Node} Node having the maximum key value.
+   */
+  SplayTree.prototype.findMax = function(opt_startNode) {
+    if (this.isEmpty()) {
+      return null;
+    }
+    var current = opt_startNode || this.root_;
+    while (current.right) {
+      current = current.right;
+    }
+    return current;
+  };
+
+  /**
+   * @return {SplayTree.Node} Node having the maximum key value that
+   *     is less or equal to the specified key value.
+   */
+  SplayTree.prototype.findGreatestLessThan = function(key) {
+    if (this.isEmpty()) {
+      return null;
+    }
+    // Splay on the key to move the node with the given key or the last
+    // node on the search path to the top of the tree.
+    this.splay_(key);
+    // Now the result is either the root node or the greatest node in
+    // the left subtree.
+    if (this.root_.key <= key) {
+      return this.root_;
+    } else if (this.root_.left) {
+      return this.findMax(this.root_.left);
+    } else {
+      return null;
+    }
+  };
+
+  /**
+   * @return {Array<*>} An array containing all the values of tree's nodes
+   * paired with keys.
+   *
+   */
+  SplayTree.prototype.exportKeysAndValues = function() {
+    var result = [];
+    this.traverse_(function(node) { result.push([node.key, node.value]); });
+    return result;
+  };
+
+  /**
+   * @return {Array<*>} An array containing all the values of tree's nodes.
+   */
+  SplayTree.prototype.exportValues = function() {
+    var result = [];
+    this.traverse_(function(node) { result.push(node.value); });
+    return result;
+  };
+
+  /**
+   * Perform the splay operation for the given key. Moves the node with
+   * the given key to the top of the tree.  If no node has the given
+   * key, the last node on the search path is moved to the top of the
+   * tree. This is the simplified top-down splaying algorithm from:
+   * "Self-adjusting Binary Search Trees" by Sleator and Tarjan
+   *
+   * @param {number} key Key to splay the tree on.
+   * @private
+   */
+  SplayTree.prototype.splay_ = function(key) {
+    if (this.isEmpty()) {
+      return;
+    }
+    // Create a dummy node.  The use of the dummy node is a bit
+    // counter-intuitive: The right child of the dummy node will hold
+    // the L tree of the algorithm.  The left child of the dummy node
+    // will hold the R tree of the algorithm.  Using a dummy node, left
+    // and right will always be nodes and we avoid special cases.
+    var dummy, left, right;
+    dummy = left = right = new SplayTree.Node(null, null);
+    var current = this.root_;
+    while (true) {
+      if (key < current.key) {
+        if (!current.left) {
+          break;
+        }
+        if (key < current.left.key) {
+          // Rotate right.
+          var tmp = current.left;
+          current.left = tmp.right;
+          tmp.right = current;
+          current = tmp;
+          if (!current.left) {
+            break;
+          }
+        }
+        // Link right.
+        right.left = current;
+        right = current;
+        current = current.left;
+      } else if (key > current.key) {
+        if (!current.right) {
+          break;
+        }
+        if (key > current.right.key) {
+          // Rotate left.
+          var tmp = current.right;
+          current.right = tmp.left;
+          tmp.left = current;
+          current = tmp;
+          if (!current.right) {
+            break;
+          }
+        }
+        // Link left.
+        left.right = current;
+        left = current;
+        current = current.right;
+      } else {
+        break;
+      }
+    }
+    // Assemble.
+    left.right = current.left;
+    right.left = current.right;
+    current.left = dummy.right;
+    current.right = dummy.left;
+    this.root_ = current;
+  };
+
+  /**
+   * Performs a preorder traversal of the tree.
+   *
+   * @param {function(SplayTree.Node)} f Visitor function.
+   * @private
+   */
+  SplayTree.prototype.traverse_ = function(f) {
+    var nodesToVisit = [this.root_];
+    while (nodesToVisit.length > 0) {
+      var node = nodesToVisit.shift();
+      if (node == null) {
+        continue;
+      }
+      f(node);
+      nodesToVisit.push(node.left);
+      nodesToVisit.push(node.right);
+    }
+  };
+
+  /**
+   * Constructs a Splay tree node.
+   *
+   * @param {number} key Key.
+   * @param {*} value Value.
+   */
+  SplayTree.Node = function(key, value) {
+    this.key = key;
+    this.value = value;
+  };
+
+  /**
+   * @type {SplayTree.Node}
+   */
+  SplayTree.Node.prototype.left = null;
+
+  /**
+   * @type {SplayTree.Node}
+   */
+  SplayTree.Node.prototype.right = null;
+
+  return {
+    SplayTree: SplayTree
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/v8/v8_log_importer.html b/trace-viewer/trace_viewer/extras/importer/v8/v8_log_importer.html
new file mode 100644
index 0000000..7390821
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/v8/v8_log_importer.html
@@ -0,0 +1,288 @@
+<!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="/extras/importer/v8/log_reader.html">
+<link rel="import" href="/extras/importer/v8/codemap.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/core/trace_model/slice.html">
+
+<script>
+
+'use strict';
+
+/**
+ * @fileoverview V8LogImporter imports v8.log files into the provided model.
+ */
+tv.exportTo('tv.e.importer.v8', function() {
+  var Importer = tv.c.importer.Importer;
+
+  function V8LogImporter(model, eventData) {
+    this.importPriority = 3;
+    this.model_ = model;
+
+    this.logData_ = eventData;
+
+    this.code_map_ = new tv.e.importer.v8.CodeMap();
+    this.v8_timer_thread_ = undefined;
+    this.v8_thread_ = undefined;
+
+    this.root_stack_frame_ = new tv.c.trace_model.StackFrame(
+        undefined, 'v8-root-stack-frame',
+        'v8-root-stack-frame', 'v8-root-stack-frame', 0);
+
+    // We reconstruct the stack timeline from ticks.
+    this.v8_stack_timeline_ = new Array();
+  }
+
+  var kV8BinarySuffixes = ['/d8', '/libv8.so'];
+
+
+  var TimerEventDefaultArgs = {
+    'V8.Execute': { pause: false, no_execution: false},
+    'V8.External': { pause: false, no_execution: true},
+    'V8.CompileFullCode': { pause: true, no_execution: true},
+    'V8.RecompileSynchronous': { pause: true, no_execution: true},
+    'V8.RecompileParallel': { pause: false, no_execution: false},
+    'V8.CompileEval': { pause: true, no_execution: true},
+    'V8.Parse': { pause: true, no_execution: true},
+    'V8.PreParse': { pause: true, no_execution: true},
+    'V8.ParseLazy': { pause: true, no_execution: true},
+    'V8.GCScavenger': { pause: true, no_execution: true},
+    'V8.GCCompactor': { pause: true, no_execution: true},
+    'V8.GCContext': { pause: true, no_execution: true}
+  };
+
+  /**
+   * @return {boolean} Whether obj is a V8 log string.
+   */
+  V8LogImporter.canImport = function(eventData) {
+    if (typeof(eventData) !== 'string' && !(eventData instanceof String))
+      return false;
+
+    return eventData.substring(0, 12) == 'timer-event,' ||
+           eventData.substring(0, 5) == 'tick,' ||
+           eventData.substring(0, 15) == 'shared-library,' ||
+           eventData.substring(0, 9) == 'profiler,' ||
+           eventData.substring(0, 14) == 'code-creation,';
+  };
+
+  V8LogImporter.prototype = {
+
+    __proto__: Importer.prototype,
+
+    processTimerEvent_: function(name, start, length) {
+      var args = TimerEventDefaultArgs[name];
+      if (args === undefined) return;
+      start /= 1000;  // Convert to milliseconds.
+      length /= 1000;
+      var colorId = tv.b.ui.getColorIdForGeneralPurposeString(name);
+      var slice = new tv.c.trace_model.Slice('v8', name, colorId, start,
+          args, length);
+      this.v8_timer_thread_.sliceGroup.pushSlice(slice);
+    },
+
+    processTimerEventStart_: function(name, start) {
+      var args = TimerEventDefaultArgs[name];
+      if (args === undefined) return;
+      start /= 1000;  // Convert to milliseconds.
+      this.v8_timer_thread_.sliceGroup.beginSlice('v8', name, start, args);
+    },
+
+    processTimerEventEnd_: function(name, end) {
+      end /= 1000;  // Convert to milliseconds.
+      this.v8_timer_thread_.sliceGroup.endSlice(end);
+    },
+
+    processCodeCreateEvent_: function(type, kind, address, size, name) {
+      var code_entry = new tv.e.importer.v8.CodeMap.CodeEntry(size, name);
+      code_entry.kind = kind;
+      this.code_map_.addCode(address, code_entry);
+    },
+
+    processCodeMoveEvent_: function(from, to) {
+      this.code_map_.moveCode(from, to);
+    },
+
+    processCodeDeleteEvent_: function(address) {
+      this.code_map_.deleteCode(address);
+    },
+
+    processSharedLibrary_: function(name, start, end) {
+      var code_entry = new tv.e.importer.v8.CodeMap.CodeEntry(
+          end - start, name);
+      code_entry.kind = -3;  // External code kind.
+      for (var i = 0; i < kV8BinarySuffixes.length; i++) {
+        var suffix = kV8BinarySuffixes[i];
+        if (name.indexOf(suffix, name.length - suffix.length) >= 0) {
+          code_entry.kind = -1;  // V8 runtime code kind.
+          break;
+        }
+      }
+      this.code_map_.addLibrary(start, code_entry);
+    },
+
+    findCodeKind_: function(kind) {
+      for (name in CodeKinds) {
+        if (CodeKinds[name].kinds.indexOf(kind) >= 0) {
+          return CodeKinds[name];
+        }
+      }
+    },
+
+    processTickEvent_: function(pc, start, unused_x, unused_y, vmstate, stack) {
+      start /= 1000;
+
+      function findChildWithEntryID(stackFrame, entryID) {
+        for (var i = 0; i < stackFrame.children.length; i++) {
+          if (stackFrame.children[i].entryID == entryID)
+            return stackFrame.children[i];
+        }
+        return undefined;
+      }
+
+      var lastStackFrame;
+      if (stack && stack.length) {
+
+        lastStackFrame = this.root_stack_frame_;
+        // v8 log stacks are inverted, leaf first and the root at the end.
+        stack = stack.reverse();
+        for (var i = 0; i < stack.length; i++) {
+          if (!stack[i])
+            break;
+          var entry = this.code_map_.findEntry(stack[i]);
+
+          var entryID = entry ? entry.id : 'Unknown';
+          var childFrame = findChildWithEntryID(lastStackFrame, entryID);
+          if (childFrame === undefined) {
+            var entryName = entry ? entry.name : 'Unknown';
+            lastStackFrame = new tv.c.trace_model.StackFrame(
+                lastStackFrame, 'v8sf-' + tv.b.GUID.allocate(),
+                'v8', entryName,
+                tv.b.ui.getColorIdForGeneralPurposeString(entryName));
+            lastStackFrame.entryID = entryID;
+            this.model_.addStackFrame(lastStackFrame);
+          } else {
+            lastStackFrame = childFrame;
+          }
+        }
+      } else {
+        var pcEntry = this.code_map_.findEntry(pc);
+        var pcEntryID = 'v8pc-' + (pcEntry ? pcEntry.id : 'Unknown');
+        if (this.model_.stackFrames[pcEntryID] === undefined) {
+          var pcEntryName = pcEntry ? pcEntry.name : 'Unknown';
+          lastStackFrame = new tv.c.trace_model.StackFrame(
+              undefined, pcEntryID,
+              'v8', pcEntryName,
+              tv.b.ui.getColorIdForGeneralPurposeString(pcEntryName));
+          this.model_.addStackFrame(lastStackFrame);
+        }
+        lastStackFrame = this.model_.stackFrames[pcEntryID];
+      }
+
+      var sample = new tv.c.trace_model.Sample(
+          undefined, this.v8_thread_, 'V8 PC',
+          start, lastStackFrame, 1);
+      this.model_.samples.push(sample);
+    },
+
+    processDistortion_: function(distortion_in_picoseconds) {
+      distortion_per_entry = distortion_in_picoseconds / 1000000;
+    },
+
+    processPlotRange_: function(start, end) {
+      xrange_start_override = start;
+      xrange_end_override = end;
+    },
+
+    /**
+     * Walks through the events_ list and outputs the structures discovered to
+     * model_.
+     */
+    importEvents: function() {
+      var logreader = new tv.e.importer.v8.LogReader(
+          { 'timer-event' : {
+            parsers: [null, parseInt, parseInt],
+            processor: this.processTimerEvent_.bind(this)
+          },
+          'shared-library': {
+            parsers: [null, parseInt, parseInt],
+            processor: this.processSharedLibrary_.bind(this)
+          },
+          'timer-event-start' : {
+            parsers: [null, parseInt],
+            processor: this.processTimerEventStart_.bind(this)
+          },
+          'timer-event-end' : {
+            parsers: [null, parseInt],
+            processor: this.processTimerEventEnd_.bind(this)
+          },
+          'code-creation': {
+            parsers: [null, parseInt, parseInt, parseInt, null],
+            processor: this.processCodeCreateEvent_.bind(this)
+          },
+          'code-move': {
+            parsers: [parseInt, parseInt],
+            processor: this.processCodeMoveEvent_.bind(this)
+          },
+          'code-delete': {
+            parsers: [parseInt],
+            processor: this.processCodeDeleteEvent_.bind(this)
+          },
+          'tick': {
+            parsers: [parseInt, parseInt, null, null, parseInt, 'var-args'],
+            processor: this.processTickEvent_.bind(this)
+          },
+          'distortion': {
+            parsers: [parseInt],
+            processor: this.processDistortion_.bind(this)
+          },
+          'plot-range': {
+            parsers: [parseInt, parseInt],
+            processor: this.processPlotRange_.bind(this)
+          }
+          });
+
+      this.v8_timer_thread_ =
+          this.model_.getOrCreateProcess(-32).getOrCreateThread(1);
+      this.v8_timer_thread_.name = 'V8 Timers';
+      this.v8_thread_ =
+          this.model_.getOrCreateProcess(-32).getOrCreateThread(2);
+      this.v8_thread_.name = 'V8';
+
+      var lines = this.logData_.split('\n');
+      for (var i = 0; i < lines.length; i++) {
+        logreader.processLogLine(lines[i]);
+      }
+
+      // The processing of samples adds all the root stack frame to
+      // this.root_stack_frame_. But, we don't want that stack frame in the real
+      // model. So get rid of it.
+      this.root_stack_frame_.removeAllChildren();
+
+      function addSlices(slices, thread) {
+        for (var i = 0; i < slices.length; i++) {
+          var duration = slices[i].end - slices[i].start;
+          var slice = new tv.c.trace_model.Slice('v8', slices[i].name,
+              tv.b.ui.getColorIdForGeneralPurposeString(slices[i].name),
+              slices[i].start, {}, duration);
+          thread.sliceGroup.pushSlice(slice);
+          addSlices(slices[i].children, thread);
+        }
+      }
+      addSlices(this.v8_stack_timeline_, this.v8_thread_);
+    }
+  };
+
+  tv.c.importer.Importer.register(V8LogImporter);
+
+  return {
+    V8LogImporter: V8LogImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/importer/v8/v8_log_importer_test.html b/trace-viewer/trace_viewer/extras/importer/v8/v8_log_importer_test.html
new file mode 100644
index 0000000..d000121
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/v8/v8_log_importer_test.html
@@ -0,0 +1,247 @@
+<!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="/extras/importer/v8/v8_log_importer.html">
+
+<script>
+
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var V8LogImporter = tv.e.importer.v8.V8LogImporter;
+
+  test('tickEventInSharedLibrary', function() {
+    var lines = [
+      'shared-library,"/usr/lib/libc++.1.dylib",0x99d8aae0,0x99dce729',
+      'tick,0x99d8aae4,12158,0,0x0,5'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var t = p.findAllThreadsNamed('V8')[0];
+    assert.equal(t.samples.length, 1);
+    assert.equal(t.samples[0].title, 'V8 PC');
+    assert.equal(t.samples[0].start, 12158 / 1000);
+    assert.equal(t.samples[0].leafStackFrame.title, '/usr/lib/libc++.1.dylib');
+  });
+
+  test('tickEventInGeneratedCode', function() {
+    var lines = [
+      'shared-library,"/usr/lib/libc++.1.dylib",0x99d8aae0,0x99dce729',
+      'code-creation,Stub,2,0x5b60ce80,1259,"StringAddStub"',
+      'tick,0x5b60ce84,12158,0,0x0,5'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var threads = p.findAllThreadsNamed('V8');
+    var t = threads[0];
+    assert.equal(t.samples.length, 1);
+    assert.equal(t.samples[0].leafStackFrame.title, 'StringAddStub');
+  });
+
+  test('tickEventInUknownCode', function() {
+    var lines = [
+      'shared-library,"/usr/lib/libc++.1.dylib",0x99d8aae0,0x99dce729',
+      'code-creation,Stub,2,0x5b60ce80,1259,"StringAddStub"',
+      'tick,0x4,0xbff02f08,12158,0,0x0,5'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var threads = p.findAllThreadsNamed('V8');
+    var t = threads[0];
+    assert.equal(t.samples.length, 1);
+    assert.equal(t.samples[0].leafStackFrame.title, 'Unknown');
+  });
+
+  test('tickEventWithStack', function() {
+    var lines = [
+      'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+      'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var t = p.findAllThreadsNamed('V8')[0];
+    assert.equal(t.samples.length, 1);
+    assert.deepEqual(
+        ['v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[0].getUserFriendlyStackTrace());
+  });
+
+  test('twoTickEventsWithStack', function() {
+    var lines = [
+      'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+      'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304',
+      'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var t = p.findAllThreadsNamed('V8')[0];
+    assert.equal(t.samples.length, 2);
+    assert.equal(t.samples[0].start, 528674 / 1000);
+    assert.equal(t.samples[1].start, 536213 / 1000);
+    assert.deepEqual(
+        ['v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[0].getUserFriendlyStackTrace());
+    assert.deepEqual(
+        ['v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[1].getUserFriendlyStackTrace());
+    assert.equal(t.samples[0].leafStackFrame,
+                 t.samples[1].leafStackFrame);
+  });
+
+  test('twoTickEventsWithTwoStackFrames', function() {
+    var lines = [
+      'code-creation,LazyCompile,0,0x2904d560,876,"Instantiate native apinatives.js:9:21",0x56b190c8,~', // @suppress longLineCheck
+      'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+      'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304,0x2904d6e8',
+      'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304,0x2904d6e8'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var t = p.findAllThreadsNamed('V8')[0];
+    assert.equal(t.samples.length, 2);
+
+    assert.equal(t.samples[0].start, 528674 / 1000);
+    assert.equal(t.samples[1].start, 536213 / 1000);
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21',
+         'v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[0].getUserFriendlyStackTrace());
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21',
+         'v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[1].getUserFriendlyStackTrace());
+
+    var childStackFrame = t.samples[0].leafStackFrame;
+    assert.equal(childStackFrame, t.samples[1].leafStackFrame);
+    assert.equal(childStackFrame.children.length, 0);
+
+    var parentStackFrame = childStackFrame.parentFrame;
+    assert.equal(parentStackFrame.children.length, 1);
+    assert.equal(childStackFrame, parentStackFrame.children[0]);
+
+  });
+
+  test('threeTickEventsWithTwoStackFrames', function() {
+    var lines = [
+      'code-creation,LazyCompile,0,0x2904d560,876,"Instantiate native apinatives.js:9:21",0x56b190c8,~', // @suppress longLineCheck
+      'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+      'tick,0x7fd7f75c,518328,0,0x81d86da8,2,0x2904d6e8',
+      'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304,0x2904d6e8',
+      'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304,0x2904d6e8'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var threads = p.findAllThreadsNamed('V8');
+
+    var t = threads[0];
+    assert.equal(t.samples.length, 3);
+    assert.equal(t.samples[0].start, 518328 / 1000);
+    assert.equal(t.samples[1].start, 528674 / 1000);
+    assert.equal(t.samples[2].start, 536213 / 1000);
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21'],
+        t.samples[0].getUserFriendlyStackTrace());
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21',
+         'v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[1].getUserFriendlyStackTrace());
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21',
+         'v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[2].getUserFriendlyStackTrace());
+
+    var topLevelStackFrame = t.samples[0].leafStackFrame;
+    var childStackFrame = t.samples[1].leafStackFrame;
+    assert.equal(t.samples[2].leafStackFrame, childStackFrame);
+    assert.equal(topLevelStackFrame, childStackFrame.parentFrame);
+    assert.equal(topLevelStackFrame.children.length, 1);
+    assert.equal(childStackFrame.children.length, 0);
+    assert.equal(childStackFrame, topLevelStackFrame.children[0]);
+  });
+
+  test('twoSubStacks', function() {
+    var lines = [
+      'code-creation,LazyCompile,0,0x2904d560,876,"Instantiate native apinatives.js:9:21",0x56b190c8,~', // @suppress longLineCheck
+      'code-creation,LazyCompile,0,0x2905d0c0,1800,"InstantiateFunction native apinatives.js:26:29",0x56b19124,', // @suppress longLineCheck
+      'tick,0x7fd7f75c,518328,0,0x81d86da8,2,0x2904d6e8',
+      'tick,0x7fc6fe34,528674,0,0x3,0,0x2905d304,0x2904d6e8',
+      'tick,0x7fd2a534,536213,0,0x81d8d080,0,0x2905d304,0x2904d6e8',
+      'code-creation,Script,0,0x2906a7c0,792,"http://www.google.com/",0x5b12fe50,~', // @suppress longLineCheck
+      'tick,0xb6f51d30,794049,0,0xb6f7b368,2,0x2906a914',
+      'tick,0xb6f51d30,799146,0,0xb6f7b368,0,0x2906a914'
+    ];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var threads = p.findAllThreadsNamed('V8');
+    var t = threads[0];
+    assert.equal(t.samples.length, 5);
+
+    assert.equal(t.samples[0].start, 518328 / 1000);
+    assert.equal(t.samples[1].start, 528674 / 1000);
+    assert.equal(t.samples[2].start, 536213 / 1000);
+    assert.equal(t.samples[3].start, 794049 / 1000);
+    assert.equal(t.samples[4].start, 799146 / 1000);
+
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21'],
+        t.samples[0].getUserFriendlyStackTrace());
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21',
+         'v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[1].getUserFriendlyStackTrace());
+    assert.deepEqual(
+        ['v8: Instantiate native apinatives.js:9:21',
+         'v8: InstantiateFunction native apinatives.js:26:29'],
+        t.samples[2].getUserFriendlyStackTrace());
+    assert.deepEqual(['v8: http://www.google.com/'],
+                      t.samples[3].getUserFriendlyStackTrace());
+    assert.deepEqual(['v8: http://www.google.com/'],
+                      t.samples[4].getUserFriendlyStackTrace());
+
+    var firsStackTopLevelStackFrame = t.samples[0].leafStackFrame;
+    var firsStackChildStackFrame = t.samples[1].leafStackFrame;
+    assert.equal(firsStackChildStackFrame, t.samples[2].leafStackFrame);
+    assert.equal(firsStackTopLevelStackFrame,
+                 firsStackChildStackFrame.parentFrame);
+    assert.equal(firsStackTopLevelStackFrame.children.length, 1);
+    assert.equal(firsStackChildStackFrame.children.length, 0);
+    assert.equal(firsStackChildStackFrame,
+                 firsStackTopLevelStackFrame.children[0]);
+
+    var secondStackStackFrame = t.samples[3].leafStackFrame;
+    assert.equal(secondStackStackFrame, t.samples[4].leafStackFrame);
+    assert.equal(secondStackStackFrame.children.length, 0);
+    assert.isUndefined(secondStackStackFrame.parentFrame);
+  });
+
+  test('timerEventSliceCreation', function() {
+    var lines = ['timer-event,"V8.External",38189483,3'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    var p = m.processes[-32];
+    var threads = p.findAllThreadsNamed('V8 Timers');
+    assert.isDefined(threads);
+    assert.equal(threads.length, 1);
+    var t = threads[0];
+    assert.equal(t.sliceGroup.length, 1);
+  });
+
+  test('processThreadCreation', function() {
+    var lines = ['timer-event,"V8.External",38189483,3'];
+    var m = new tv.c.TraceModel(lines.join('\n'), false);
+    assert.isDefined(m);
+    var p = m.processes[-32];
+    assert.isDefined(p);
+    var threads = p.findAllThreadsNamed('V8 Timers');
+    assert.isDefined(threads);
+    assert.equal(1, threads.length);
+    var t = threads[0];
+    assert.equal('V8 Timers', t.name);
+  });
+
+  test('canImport', function() {
+    assert.isTrue(V8LogImporter.canImport(
+        'timer-event,"V8.External",38189483,3'));
+    assert.isFalse(V8LogImporter.canImport(''));
+    assert.isFalse(V8LogImporter.canImport([]));
+  });
+});
+</script>
+
diff --git a/trace-viewer/trace_viewer/extras/importer/zip_importer.html b/trace-viewer/trace_viewer/extras/importer/zip_importer.html
new file mode 100644
index 0000000..867b983
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/importer/zip_importer.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="/extras/importer/jszip.html">
+<link rel="import" href="/extras/importer/gzip_importer.html">
+<link rel="import" href="/core/importer/importer.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+
+<script>
+'use strict';
+
+/**
+ * @fileoverview ZipImporter inflates zip compressed data and passes it along
+ * to an actual importer.
+ */
+tv.exportTo('tv.e.importer', function() {
+  var Importer = tv.c.importer.Importer;
+
+  function ZipImporter(model, eventData) {
+    if (eventData instanceof ArrayBuffer)
+      eventData = new Uint8Array(eventData);
+    this.model_ = model;
+    this.eventData_ = eventData;
+  }
+
+  /**
+   * @param {eventData} string Possibly zip compressed data.
+   * @return {boolean} Whether eventData looks like zip compressed data.
+   */
+  ZipImporter.canImport = function(eventData) {
+    var header;
+    if (eventData instanceof ArrayBuffer)
+      header = new Uint8Array(eventData.slice(0, 2));
+    else if (typeof(eventData) === 'string' || eventData instanceof String)
+      header = [eventData.charCodeAt(0), eventData.charCodeAt(1)];
+    else
+      return false;
+    return header[0] === 'P'.charCodeAt(0) && header[1] === 'K'.charCodeAt(0);
+  };
+
+  ZipImporter.prototype = {
+    __proto__: Importer.prototype,
+
+    isTraceDataContainer: function() {
+      return true;
+    },
+
+    extractSubtraces: function() {
+      var zip = new JSZip(this.eventData_);
+      var subtraces = [];
+      for (var idx in zip.files)
+        subtraces.push(zip.files[idx].asBinary());
+      return subtraces;
+    }
+  };
+
+  tv.c.importer.Importer.register(ZipImporter);
+
+  return {
+    ZipImporter: ZipImporter
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/lean_config.html b/trace-viewer/trace_viewer/extras/lean_config.html
new file mode 100644
index 0000000..d451319
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/lean_config.html
@@ -0,0 +1,13 @@
+<!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.
+-->
+
+<!--
+The lean config is just enough to import uncompressed, trace-event-formatted
+json blobs.
+-->
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<link rel="import" href="/extras/highlighter/vsync_highlighter.html">
diff --git a/trace-viewer/trace_viewer/extras/net/net.html b/trace-viewer/trace_viewer/extras/net/net.html
new file mode 100644
index 0000000..d92bf0b
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/net/net.html
@@ -0,0 +1,8 @@
+<!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/net/net_async_slice.html">
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/extras/net/net_async_slice.html b/trace-viewer/trace_viewer/extras/net/net_async_slice.html
new file mode 100644
index 0000000..0cbe7b8
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/net/net_async_slice.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="/core/trace_model/async_slice.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.net', function() {
+  var AsyncSlice = tv.c.trace_model.AsyncSlice;
+
+  function NetAsyncSlice() {
+    AsyncSlice.apply(this, arguments);
+    // A boolean variable indicates whether we have computed the title.
+    this.isTitleComputed_ = false;
+  }
+
+  NetAsyncSlice.prototype = {
+    __proto__: AsyncSlice.prototype,
+
+    get viewSubGroupTitle() {
+      return 'NetLog';
+    },
+
+    get title() {
+      if (this.isTitleComputed_ || !this.isTopLevel) {
+        return this.title_;
+      }
+
+      // A recursive helper function that gets the url param of a slice or its
+      // nested subslices if there is one.
+      var getUrl = function(slice) {
+        if (slice.args !== undefined && slice.args.params !== undefined &&
+            slice.args.params.url !== undefined) {
+          return slice.args.params.url;
+        }
+        if (slice.subSlices === undefined || slice.subSlices.length === 0)
+          return undefined;
+        for (var i = 0; i < slice.subSlices.length; i++) {
+          var result = getUrl(slice.subSlices[i]);
+          if (result !== undefined)
+            return result;
+        }
+        return undefined;
+      };
+
+      var url = getUrl(this);
+      if (url !== undefined && url.length > 0) {
+        // Set the title so we do not have to recompute when it is redrawn.
+        this.title_ = url;
+      } else if (this.args !== undefined &&
+                 this.args.source_type !== undefined) {
+        // We do not have a URL, use the source type as the title.
+        this.title_ = this.args.source_type;
+      }
+      this.isTitleComputed_ = true;
+      return this.title_;
+    },
+
+    set title(title) {
+      this.title_ = title;
+    }
+  };
+
+  AsyncSlice.register(
+    NetAsyncSlice,
+    {
+      categoryParts: ['netlog', 'disabled-by-default-netlog']
+    });
+
+  return {
+    NetAsyncSlice: NetAsyncSlice
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/net/net_async_slice_test.html b/trace-viewer/trace_viewer/extras/net/net_async_slice_test.html
new file mode 100644
index 0000000..30e3e55
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/net/net_async_slice_test.html
@@ -0,0 +1,152 @@
+<!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/net/net.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<link rel="import" href="/extras/importer/trace_event_importer.html">
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var AsyncSlice = tv.c.trace_model.AsyncSlice;
+  var NetAsyncSlice = tv.e.net.NetAsyncSlice;
+
+  test('basic', function() {
+    var s = new NetAsyncSlice('netlog', 'HTTP_STREAM_JOB', 7, 0, {});
+    s.duration = 100;
+
+    assert.equal(AsyncSlice.getConstructor('netlog', 'HTTP_STREAM_JOB'),
+                 NetAsyncSlice);
+    assert.equal(s.viewSubGroupTitle, 'NetLog');
+  });
+
+  test('import', function() {
+    var events = [
+      {name: 'HTTP_STREAM_JOB', args: {}, pid: 1, ts: 100, cat: 'netlog', tid: 2, ph: 'b', id: 71}, // @suppress longLineCheck
+      {name: 'HTTP_STREAM_JOB', args: {}, pid: 1, ts: 200, cat: 'netlog', tid: 2, ph: 'e', id: 71} // @suppress longLineCheck
+    ];
+    var m = new tv.c.TraceModel(events);
+    var t2 = m.getOrCreateProcess(1).getOrCreateThread(2);
+    assert.equal(t2.asyncSliceGroup.length, 1);
+    assert.instanceOf(t2.asyncSliceGroup.slices[0], NetAsyncSlice);
+  });
+
+  test('ExposeURLBasic', function() {
+    var slice = new NetAsyncSlice('', 'a', 0, 1,
+                                  {params: {url: 'https://google.com'},
+                                   source_type: 'b'}, 0, true);
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    // URL is exposed as the title of the parent slice.
+    assert.equal(slice.title, 'https://google.com');
+  });
+
+  test('ExposeURLNested', function() {
+    var slice = new NetAsyncSlice(
+        '', 'a', 0, 1, {params: {}, source_type: 'HELLO'}, 1, true);
+    var childSlice = new NetAsyncSlice('', 'b', 0, 1,
+                                       {params: {url: 'http://test.url'}});
+    slice.subSlices = [childSlice];
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    assert.isFalse(childSlice.isTopLevel);
+    // URL is exposed as the title of the parent slice.
+    assert.equal(slice.title, 'http://test.url');
+    assert.equal(childSlice.title, 'b');
+  });
+
+  test('ExposeURLNestedNoURL', function() {
+    var slice = new NetAsyncSlice('', 'a', 0, 1, {params: {}}, 1, true);
+    var childSlice = new NetAsyncSlice('', 'b', 0, 1, {params: {}});
+    slice.subSlices = [childSlice];
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    assert.isFalse(childSlice.isTopLevel);
+    // URL is exposed as the title of the parent slice.
+    assert.equal(slice.title, 'a');
+    assert.equal(childSlice.title, 'b');
+  });
+
+  test('ExposeURLNestedBothChildrenHaveURL', function() {
+    var slice = new NetAsyncSlice('', 'a', 0, 1, {params: {}}, 1, true);
+    var childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+                                   {params: {url: 'http://test.url.net'}});
+    var childSlice2 = new NetAsyncSlice('', 'c', 0, 1,
+                                   {params: {url: 'http://test.url.com'}});
+    slice.subSlices = [childSlice1, childSlice2];
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    assert.isFalse(childSlice1.isTopLevel);
+    assert.isFalse(childSlice2.isTopLevel);
+    // Parent should take the first url param that it finds.
+    assert.equal(slice.title, 'http://test.url.net');
+    assert.equal(childSlice1.title, 'b');
+    assert.equal(childSlice2.title, 'c');
+  });
+
+  test('ExposeURLNestedBothParentAndChildHaveURL', function() {
+    var slice = new NetAsyncSlice('', 'a', 0, 1,
+                                  {params: {url: 'parent123.url.com'}}, 1,
+                                  true);
+    var childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+                                        {params: {url: 'http://test.url.net'}});
+    var childSlice2 = new NetAsyncSlice('', 'c', 0, 1);
+
+    slice.subSlices = [childSlice1, childSlice2];
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    assert.isFalse(childSlice1.isTopLevel);
+    assert.isFalse(childSlice2.isTopLevel);
+    // Parent should take its own url param if there is one.
+    assert.equal(slice.title, 'parent123.url.com');
+    assert.equal(childSlice1.title, 'b');
+    assert.equal(childSlice2.title, 'c');
+  });
+
+  test('ExposeURLDoNotComputeUrlTwice', function() {
+    var slice = new NetAsyncSlice('', 'a', 0, 1, {params: {}}, 1, true);
+    var childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+                                        {params: {url: 'http://test.url.net'}});
+    var childSlice2 = new NetAsyncSlice('', 'c', 0, 1);
+
+    slice.subSlices = [childSlice1, childSlice2];
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    assert.isFalse(childSlice1.isTopLevel);
+    assert.isFalse(childSlice2.isTopLevel);
+    // Parent should take its child's url param.
+    assert.equal(slice.title, 'http://test.url.net');
+    assert.equal(childSlice1.title, 'b');
+    assert.equal(childSlice2.title, 'c');
+    // Now if we change the url param of the child, the parent's title should
+    // remain the same. We do not recompute.
+    childSlice1.args.params.url = 'example.com';
+    assert.equal(slice.title, 'http://test.url.net');
+    assert.equal(childSlice1.title, 'b');
+    assert.equal(childSlice2.title, 'c');
+  });
+
+  test('ExposeSourceTypeAsTitle', function() {
+    var slice = new NetAsyncSlice('', 'a', 0, 1,
+                                  {params: {}, source_type: 'UDP_SOCKET'}, 1,
+                                  true);
+    var childSlice1 = new NetAsyncSlice('', 'b', 0, 1,
+                                        {params: {}, source_type: 'SOCKET'});
+    var childSlice2 = new NetAsyncSlice('', 'c', 0, 1);
+
+    slice.subSlices = [childSlice1, childSlice2];
+    // Make sure isTopLevel is populated in the constructor.
+    assert.isTrue(slice.isTopLevel);
+    assert.isFalse(childSlice1.isTopLevel);
+    assert.isFalse(childSlice2.isTopLevel);
+    // Parent should take its own source_type.
+    assert.equal(slice.title, 'UDP_SOCKET');
+    assert.equal(childSlice1.title, 'b');
+    assert.equal(childSlice2.title, 'c');
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/side_panel/alerts_side_panel.html b/trace-viewer/trace_viewer/extras/side_panel/alerts_side_panel.html
new file mode 100644
index 0000000..535999b
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/side_panel/alerts_side_panel.html
@@ -0,0 +1,156 @@
+<!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="/extras/audits/chrome_model_helper.html">
+<link rel="import" href="/core/analysis/table_builder.html">
+<link rel="import" href="/core/side_panel/side_panel.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/base/statistics.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/line_chart.html">
+
+<polymer-element name='tv-e-analysis-side-panel-alerts'
+    extends='tv-c-side-panel'>
+  <template>
+    <style>
+    :host {
+      display: block;
+      width: 250px;
+    }
+    #content {
+      flex-direction: column;
+      display: flex;
+    }
+    </style>
+
+    <div id='content'>
+      <toolbar id='toolbar'></toolbar>
+      <result-area id='result_area'></result-area>
+    </div>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.rangeOfInterest_ = new tv.b.Range();
+      this.selection_ = undefined;
+    },
+
+    get model() {
+      return this.model_;
+    },
+
+    set model(model) {
+      this.model_ = model;
+      this.updateContents_();
+    },
+
+    set selection(selection) {
+    },
+
+    set rangeOfInterest(rangeOfInterest) {
+    },
+
+    /**
+     * Fires a selection event selecting all alerts of the specified
+     * type.
+     */
+    selectAlertsOfType: function(alertTypeString) {
+      var alertsOfType = this.model_.alerts.filter(function(alert) {
+        return alert.title === alertTypeString;
+      });
+
+      var event = new tv.c.RequestSelectionChangeEvent();
+      event.selection = new tv.c.Selection(alertsOfType);
+      this.dispatchEvent(event);
+    },
+
+    /**
+     * Returns a map for the specified alerts where each key is the
+     * alert type string and each value is a list of alerts with that
+     * type.
+     */
+    alertsByType_: function(alerts) {
+      var alertsByType = {};
+      alerts.forEach(function(alert) {
+        var title = alert.type.title;
+        if (!alertsByType[title])
+          alertsByType[title] = [];
+
+        alertsByType[title].push(alert);
+      });
+      return alertsByType;
+    },
+
+    alertsTableRows_: function(alertsByType) {
+      return Object.keys(alertsByType).map(function(key) {
+        return {
+          alertType: key,
+          count: alertsByType[key].length
+        };
+      });
+    },
+
+    alertsTableColumns_: function() {
+      return [
+        {
+          title: 'Alert type',
+          value: function(row) { return row.alertType; },
+          width: '180px'
+        },
+        {
+          title: 'Count',
+          width: '100%',
+          value: function(row) { return row.count; }
+        }
+      ];
+    },
+
+    createAlertsTable_: function(alerts) {
+      var alertsByType = this.alertsByType_(alerts);
+
+      var table = document.createElement('tracing-analysis-nested-table');
+      table.tableColumns = this.alertsTableColumns_();
+      table.tableRows = this.alertsTableRows_(alertsByType);
+
+      table.rowClickCallback = function(e) {
+        var tr = e.target.parentElement;
+        var alertTypeString = tr.firstChild.innerText;
+        this.selectAlertsOfType(alertTypeString);
+      }.bind(this);
+
+      return table;
+    },
+
+    updateContents_: function() {
+      this.$.result_area.textContent = '';
+      if (this.model_ === undefined)
+        return;
+
+      var panel = this.createAlertsTable_(this.model_.alerts);
+      this.$.result_area.appendChild(panel);
+    },
+
+    supportsModel: function(m) {
+      if (m == undefined) {
+        return {
+          supported: false,
+          reason: 'Unknown tracing model'
+        };
+      }
+
+      return {
+        supported: true
+      };
+    },
+
+    textLabel: 'Alerts'
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/side_panel/alerts_side_panel_test.html b/trace-viewer/trace_viewer/extras/side_panel/alerts_side_panel_test.html
new file mode 100644
index 0000000..fad3967
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/side_panel/alerts_side_panel_test.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="/extras/side_panel/alerts_side_panel.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 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);
+
+  test('instantiate', function() {
+    var panel = document.createElement('tv-e-analysis-side-panel-alerts');
+    panel.model = createModelWithAlerts([
+      new tv.c.trace_model.Alert(ALERT_TYPE_1, 5),
+      new tv.c.trace_model.Alert(ALERT_TYPE_2, 35)
+    ]);
+    panel.style.height = '100px';
+
+    this.addHTMLOutput(panel);
+  });
+
+  test('selectAlertsOfType', function() {
+    var panel = document.createElement('tv-e-analysis-side-panel-alerts');
+    var alerts = [
+      new tv.c.trace_model.Alert(ALERT_TYPE_1, 1),
+      new tv.c.trace_model.Alert(ALERT_TYPE_1, 2),
+      new tv.c.trace_model.Alert(ALERT_TYPE_2, 3)
+    ];
+    panel.model = createModelWithAlerts(alerts);
+    panel.style.height = '100px';
+    this.addHTMLOutput(panel);
+
+    var selectionChanged = false;
+    panel.addEventListener('requestSelectionChange', function(e) {
+      selectionChanged = true;
+      assert.lengthOf(e.selection, 2);
+      assert.equal(alerts[0], e.selection[0]);
+      assert.equal(alerts[1], e.selection[1]);
+    });
+    panel.selectAlertsOfType(ALERT_TYPE_1.title);
+
+    assert.isTrue(selectionChanged);
+  });
+
+  function createModelWithAlerts(alerts) {
+    var m = new tv.c.TraceModel();
+    m.alerts = alerts;
+    return m;
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/side_panel/input_latency.html b/trace-viewer/trace_viewer/extras/side_panel/input_latency.html
new file mode 100644
index 0000000..a391d8d
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/side_panel/input_latency.html
@@ -0,0 +1,306 @@
+<!DOCTYPE html>
+<!--
+Copyright 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="/extras/audits/chrome_model_helper.html">
+<link rel="import" href="/core/side_panel/side_panel.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/base/statistics.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/line_chart.html">
+
+<polymer-element name='tv-e-side-panel-input-latency' extends='tv-c-side-panel'>
+  <template>
+    <style>
+    :host {
+      flex-direction: column;
+      display: flex;
+    }
+    toolbar {
+      flex: 0 0 auto;
+      border-bottom: 1px solid black;
+      display: flex;
+    }
+    result-area {
+      flex: 1 1 auto;
+      display: block;
+      min-height: 0;
+      overflow-y: auto;
+    }
+    </style>
+
+    <toolbar id='toolbar'></toolbar>
+    <result-area id='result_area'></result-area>
+  </template>
+
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.rangeOfInterest_ = new tv.b.Range();
+      this.frametimeType_ = tv.e.audits.IMPL_FRAMETIME_TYPE;
+      this.latencyChart_ = undefined;
+      this.frametimeChart_ = undefined;
+      this.selectedProcessId_ = undefined;
+      this.mouseDownIndex_ = undefined;
+      this.curMouseIndex_ = undefined;
+    },
+
+    get model() {
+      return this.model_;
+    },
+
+    set model(model) {
+      this.model_ = model;
+      if (this.model_)
+        this.modelHelper_ = new tv.e.audits.ChromeModelHelper(model);
+      else
+        this.modelHelper_ = undefined;
+
+      this.updateToolbar_();
+      this.updateContents_();
+    },
+
+    get frametimeType() {
+      return this.frametimeType_;
+    },
+
+    set frametimeType(type) {
+      if (this.frametimeType_ === type)
+        return;
+      this.frametimeType_ = type;
+      this.updateContents_();
+    },
+
+    get selectedProcessId() {
+      return this.selectedProcessId_;
+    },
+
+    set selectedProcessId(process) {
+      if (this.selectedProcessId_ === process)
+        return;
+      this.selectedProcessId_ = process;
+      this.updateContents_();
+    },
+
+    set selection(selection) {
+      if (this.latencyChart_ === undefined)
+        return;
+      this.latencyChart_.brushedRange = selection.bounds;
+    },
+
+    // This function is for testing purpose.
+    setBrushedIndices: function(mouseDownIndex, curIndex) {
+      this.mouseDownIndex_ = mouseDownIndex;
+      this.curMouseIndex_ = curIndex;
+      this.updateBrushedRange_();
+    },
+
+    updateBrushedRange_: function() {
+      if (this.latencyChart_ === undefined)
+        return;
+
+      var r = new tv.b.Range();
+      if (this.mouseDownIndex_ === undefined) {
+        this.latencyChart_.brushedRange = r;
+        return;
+      }
+      r = this.latencyChart_.computeBrushRangeFromIndices(
+          this.mouseDownIndex_, this.curMouseIndex_);
+      this.latencyChart_.brushedRange = r;
+
+      // Based on the brushed range, update the selection of LatencyInfo in
+      // the timeline view by sending a selectionChange event.
+      var latencySlices = [];
+      this.model_.getAllThreads().forEach(function(thread) {
+        thread.iterateAllEvents(function(event) {
+          if (event.title.indexOf('InputLatency:') === 0)
+            latencySlices.push(event);
+        });
+      });
+      latencySlices = tv.e.audits.getSlicesIntersectingRange(r, latencySlices);
+
+      var event = new tv.c.RequestSelectionChangeEvent();
+      event.selection = new tv.c.Selection(latencySlices);
+      this.latencyChart_.dispatchEvent(event);
+    },
+
+    registerMouseEventForLatencyChart_: function() {
+      this.latencyChart_.addEventListener('item-mousedown', function(e) {
+        this.mouseDownIndex_ = e.index;
+        this.curMouseIndex_ = e.index;
+        this.updateBrushedRange_();
+      }.bind(this));
+
+      this.latencyChart_.addEventListener('item-mousemove', function(e) {
+        if (e.button == undefined)
+          return;
+        this.curMouseIndex_ = e.index;
+        this.updateBrushedRange_();
+      }.bind(this));
+
+      this.latencyChart_.addEventListener('item-mouseup', function(e) {
+        this.curMouseIndex = e.index;
+        this.updateBrushedRange_();
+      }.bind(this));
+    },
+
+    updateToolbar_: function() {
+      var rendererProcesses = this.modelHelper_.rendererProcesses;
+      var browserProcess = this.modelHelper_.browserProcess;
+      var labels = [];
+
+      if (browserProcess !== undefined) {
+        var label_str = 'Browser: ' + browserProcess.pid;
+        labels.push({label: label_str, value: browserProcess.pid});
+      }
+
+      rendererProcesses.forEach(function(rendererProcess) {
+        var label_str = 'Renderer: ' + rendererProcess.pid;
+        labels.push({label: label_str, value: rendererProcess.pid});
+      });
+
+      if (labels.length === 0)
+        return;
+
+      this.selectedProcessId_ = labels[0].value;
+      var toolbarEl = this.$.toolbar;
+      toolbarEl.appendChild(tv.b.ui.createSelector(
+          this, 'frametimeType',
+          'inputLatencySidePanel.frametimeType', this.frametimeType_,
+          [{label: 'Main Thread Frame Times',
+            value: tv.e.audits.MAIN_FRAMETIME_TYPE},
+           {label: 'Impl Thread Frame Times',
+            value: tv.e.audits.IMPL_FRAMETIME_TYPE}
+          ]));
+      toolbarEl.appendChild(tv.b.ui.createSelector(
+          this, 'selectedProcessId',
+          'inputLatencySidePanel.selectedProcessId',
+          this.selectedProcessId_,
+          labels));
+    },
+
+    get currentRangeOfInterest() {
+      if (this.rangeOfInterest_.isEmpty)
+        return this.model_.bounds;
+      else
+        return this.rangeOfInterest_;
+    },
+
+    createLatencyLineChart: function(data, title) {
+      var chart = new tv.b.ui.LineChart();
+      var width = 600;
+      if (document.body.clientWidth != undefined)
+        width = document.body.clientWidth * 0.5;
+      chart.setSize({width: width, height: chart.height});
+      chart.chartTitle = title;
+      chart.data = data;
+      return chart;
+    },
+
+    updateContents_: function() {
+      var resultArea = this.$.result_area;
+      this.latencyChart_ = undefined;
+      this.frametimeChart_ = undefined;
+      resultArea.textContent = '';
+
+      if (this.modelHelper_ === undefined)
+        return;
+
+      var rangeOfInterest = this.currentRangeOfInterest;
+
+      var chromeProcess;
+      if (this.modelHelper_.renderers[this.selectedProcessId_])
+        chromeProcess = this.modelHelper_.renderers[this.selectedProcessId_];
+      else
+        chromeProcess = this.modelHelper_.browser;
+
+      var frameEvents = chromeProcess.getFrameEventsInRange(
+          this.frametimeType, rangeOfInterest);
+
+      var frametimeData = tv.e.audits.getFrametimeDataFromEvents(frameEvents);
+      var averageFrametime = tv.b.Statistics.mean(frametimeData, function(d) {
+        return d.frametime;
+      });
+
+      var latencyData = this.modelHelper_.browser.getLatencyDataInRange(
+          rangeOfInterest);
+
+      var averageLatency = tv.b.Statistics.mean(latencyData, function(d) {
+        return d.latency;
+      });
+
+      // Create summary.
+      var latencySummaryText = document.createElement('div');
+      latencySummaryText.appendChild(tv.b.ui.createSpan({
+        textContent: 'Average Latency ' + averageLatency + ' ms',
+        bold: true}));
+      resultArea.appendChild(latencySummaryText);
+
+      var frametimeSummaryText = document.createElement('div');
+      frametimeSummaryText.appendChild(tv.b.ui.createSpan({
+        textContent: 'Average Frame Time ' + averageFrametime + ' ms',
+        bold: true}));
+      resultArea.appendChild(frametimeSummaryText);
+
+      if (latencyData.length !== 0) {
+        this.latencyChart_ = this.createLatencyLineChart(
+            latencyData, 'Latency Over Time');
+        this.registerMouseEventForLatencyChart_();
+        resultArea.appendChild(this.latencyChart_);
+      }
+
+      if (frametimeData.length != 0) {
+        this.frametimeChart_ = this.createLatencyLineChart(
+            frametimeData, 'Frame Times');
+        this.frametimeChart_.style.display = 'block';
+        resultArea.appendChild(this.frametimeChart_);
+      }
+    },
+
+    get rangeOfInterest() {
+      return this.rangeOfInterest_;
+    },
+
+    set rangeOfInterest(rangeOfInterest) {
+      this.rangeOfInterest_ = rangeOfInterest;
+      this.updateContents_();
+    },
+
+    supportsModel: function(m) {
+      if (m == undefined) {
+        return {
+          supported: false,
+          reason: 'Unknown tracing model'
+        };
+      }
+
+      if (!tv.e.audits.ChromeModelHelper.supportsModel(m)) {
+        return {
+          supported: false,
+          reason: 'No Chrome browser or renderer process found'
+        };
+      }
+
+      var modelHelper = new tv.e.audits.ChromeModelHelper(m);
+      if (modelHelper.browser && modelHelper.browser.hasLatencyEvents) {
+        return {
+          supported: true
+        };
+      }
+
+      return {
+        supported: false,
+        reason: 'No InputLatency events trace. Consider enabling ' +
+            'benchmark" and "input" category when recording the trace'
+      };
+    },
+
+    textLabel: 'Input Latency'
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/side_panel/input_latency_test.html b/trace-viewer/trace_viewer/extras/side_panel/input_latency_test.html
new file mode 100644
index 0000000..4c98725
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/side_panel/input_latency_test.html
@@ -0,0 +1,140 @@
+<!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="/extras/side_panel/input_latency.html">
+<link rel="import" href="/extras/importer/trace_event_importer.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() {
+  test('basic', function() {
+    var latencyData = [
+      {
+        x: 1000,
+        latency: 16
+      },
+      {
+        x: 2000,
+        latency: 17
+      },
+      {
+        x: 3000,
+        latency: 14
+      },
+      {
+        x: 4000,
+        latency: 23
+      }
+    ];
+    var lc = document.createElement('tv-e-side-panel-input-latency');
+    var latencyChart = lc.createLatencyLineChart(latencyData, 'latency');
+    this.addHTMLOutput(latencyChart);
+
+    var frametimeData = [
+      {
+        x: 1000,
+        frametime: 16
+      },
+      {
+        x: 2000,
+        frametime: 17
+      },
+      {
+        x: 3000,
+        frametime: 14
+      },
+      {
+        x: 4000,
+        frametime: 23
+      }
+    ];
+    var lc = document.createElement('tv-e-side-panel-input-latency');
+    var frametimeChart = lc.createLatencyLineChart(frametimeData, 'frametime');
+    this.addHTMLOutput(frametimeChart);
+  });
+
+  test('brushedRangeChange', function() {
+    var events = [];
+    for (var i = 0; i < 10; i++) {
+      var start_ts = i * 10000;
+      var end_ts = start_ts + 1000 * (i % 2);
+      events.push(
+        {
+          'cat': 'benchmark',
+          'pid': 3507,
+          'tid': 3507,
+          'ts': start_ts,
+          'ph': 'S',
+          'name': 'InputLatency',
+          'id': i
+        });
+      events.push(
+        {
+          'cat': 'benchmark',
+          'pid': 3507,
+          'tid': 3507,
+          'ts': end_ts,
+          'ph': 'T',
+          'name': 'InputLatency',
+          'args': {'step': 'GestureScrollUpdate'},
+          'id': i
+        });
+      events.push(
+        {
+          'cat': 'benchmark',
+          'pid': 3507,
+          'tid': 3507,
+          'ts': end_ts,
+          'ph': 'F',
+          'name': 'InputLatency',
+          'args': {
+            'data': {
+              'INPUT_EVENT_LATENCY_ORIGINAL_COMPONENT': {
+                'time': start_ts
+              },
+              'INPUT_EVENT_LATENCY_TERMINATED_FRAME_SWAP_COMPONENT': {
+                'time': end_ts
+              }
+            }
+          },
+          'id': i
+        });
+    }
+    events.push({'cat': '__metadata',
+      'pid': 3507,
+      'tid': 3507,
+      'ts': 0,
+      'ph': 'M',
+      'name': 'thread_name',
+      'args': {'name': 'CrBrowserMain'}});
+
+    var panel = document.createElement('tv-e-side-panel-input-latency');
+    this.addHTMLOutput(panel);
+
+    var selectionChanged = false;
+    panel.model = new tv.c.TraceModel(events);
+    function listener(e) {
+      selectionChanged = true;
+      assert.equal(e.selection.length, 3);
+      assert.equal(e.selection[0].start, 20);
+      assert.equal(e.selection[1].start, 31);
+      assert.equal(e.selection[2].start, 40);
+    }
+    panel.ownerDocument.addEventListener('requestSelectionChange', listener);
+    try {
+      panel.setBrushedIndices(2, 4);
+    } finally {
+      panel.ownerDocument.removeEventListener(
+          'requestSelectionChange', listener);
+    }
+    assert.isTrue(selectionChanged);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/side_panel/time_summary.html b/trace-viewer/trace_viewer/extras/side_panel/time_summary.html
new file mode 100644
index 0000000..ef3b014
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/side_panel/time_summary.html
@@ -0,0 +1,430 @@
+<!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="/core/side_panel/side_panel.html">
+<link rel="import" href="/base/iteration_helpers.html">
+<link rel="import" href="/base/statistics.html">
+<link rel="import" href="/base/ui/dom_helpers.html">
+<link rel="import" href="/base/ui/pie_chart.html">
+
+<polymer-element name="tv-e-side-panel-time-summary" extends="tv-c-side-panel">
+  <template>
+    <style>
+    :host {
+      flex-direction: column;
+      display: flex;
+    }
+    toolbar {
+      flex: 0 0 auto;
+      border-bottom: 1px solid black;
+      display: flex;
+    }
+    result-area {
+      flex: 1 1 auto;
+      display: block;
+      min-height: 0;
+      overflow-y: auto;
+    }
+    </style>
+
+    <toolbar id='toolbar'></toolbar>
+    <result-area id='result_area'></result-area>
+  </template>
+
+  <script>
+  'use strict';
+  (function() {
+    var GROUP_BY_PROCESS_NAME = 'process';
+    var GROUP_BY_THREAD_NAME = 'thread';
+
+    var WALL_TIME_GROUPING_UNIT = 'Wall time';
+    var CPU_TIME_GROUPING_UNIT = 'CPU time';
+
+    /**
+     * @constructor
+     */
+    function ResultsForGroup(model, name) {
+      this.model = model;
+      this.name = name;
+      this.topLevelSlices = [];
+      this.allSlices = [];
+    }
+
+    ResultsForGroup.prototype = {
+      get wallTime() {
+        var wallSum = tv.b.Statistics.sum(
+            this.topLevelSlices, function(x) { return x.duration; });
+        return wallSum;
+      },
+
+      get cpuTime() {
+        var cpuDuration = 0;
+        for (var i = 0; i < this.topLevelSlices.length; i++) {
+          var x = this.topLevelSlices[i];
+          // Only report thread-duration if we have it for all events.
+          //
+          // A thread_duration of 0 is valid, so this only returns 0 if it is
+          // None.
+          if (x.cpuDuration === undefined) {
+            if (x.duration === undefined)
+              continue;
+            return 0;
+          }
+          cpuDuration += x.cpuDuration;
+        }
+        return cpuDuration;
+      },
+
+      appendGroupContents: function(group) {
+        if (group.model != this.model)
+          throw new Error('Models must be the same');
+
+        group.allSlices.forEach(function(slice) {
+          this.allSlices.push(slice);
+        }, this);
+        group.topLevelSlices.forEach(function(slice) {
+          this.topLevelSlices.push(slice);
+        }, this);
+      },
+
+      appendThreadSlices: function(rangeOfInterest, thread) {
+        var tmp = this.getSlicesIntersectingRange(
+            rangeOfInterest, thread.sliceGroup.slices);
+        tmp.forEach(function(slice) {
+          this.allSlices.push(slice);
+        }, this);
+        tmp = this.getSlicesIntersectingRange(
+            rangeOfInterest, thread.sliceGroup.topLevelSlices);
+        tmp.forEach(function(slice) {
+          this.topLevelSlices.push(slice);
+        }, this);
+      },
+
+      getSlicesIntersectingRange: function(rangeOfInterest, slices) {
+        var slicesInFilterRange = [];
+        for (var i = 0; i < slices.length; i++) {
+          var slice = slices[i];
+          if (rangeOfInterest.intersectsExplicitRange(slice.start, slice.end))
+            slicesInFilterRange.push(slice);
+        }
+        return slicesInFilterRange;
+      }
+    };
+
+    Polymer({
+      ready: function() {
+        this.rangeOfInterest_ = new tv.b.Range();
+        this.selection_ = undefined;
+        this.groupBy_ = GROUP_BY_PROCESS_NAME;
+        this.groupingUnit_ = CPU_TIME_GROUPING_UNIT;
+        this.showCpuIdleTime_ = true;
+        this.chart_ = undefined;
+
+        var toolbarEl = this.$.toolbar;
+        this.groupBySelector_ = tv.b.ui.createSelector(
+            this, 'groupBy',
+            'timeSummarySidePanel.groupBy', this.groupBy_,
+            [{label: 'Group by process', value: GROUP_BY_PROCESS_NAME},
+             {label: 'Group by thread', value: GROUP_BY_THREAD_NAME}
+            ]);
+        toolbarEl.appendChild(this.groupBySelector_);
+
+        this.groupingUnitSelector_ = tv.b.ui.createSelector(
+            this, 'groupingUnit',
+            'timeSummarySidePanel.groupingUnit', this.groupingUnit_,
+            [{label: 'Wall time', value: WALL_TIME_GROUPING_UNIT},
+             {label: 'CPU time', value: CPU_TIME_GROUPING_UNIT}
+            ]);
+        toolbarEl.appendChild(this.groupingUnitSelector_);
+
+        this.showCpuIdleTimeCheckbox_ = tv.b.ui.createCheckBox(
+            this, 'showCpuIdleTime',
+            'timeSummarySidePanel.showCpuIdleTime', this.showCpuIdleTime_,
+            'Show CPU idle time');
+        toolbarEl.appendChild(this.showCpuIdleTimeCheckbox_);
+        this.updateShowCpuIdleTimeCheckboxVisibility_();
+      },
+
+      /**
+       * This function takes an array of groups and merges smaller groups into
+       * the provided 'Other' group item such that the remaining items are ready
+       * for pie-chart consumption. Otherwise, the pie chart gets overwhelmed
+       * with tons of little slices.
+       */
+      trimPieChartData: function(groups, otherGroup, getValue, opt_extraValue) {
+        // Copy the array so it can be mutated.
+        groups = groups.filter(function(d) {
+          return getValue(d) != 0;
+        });
+
+        // Figure out total array range.
+        var sum = tv.b.Statistics.sum(groups, getValue);
+        if (opt_extraValue !== undefined)
+          sum += opt_extraValue;
+
+        // Sort by value.
+        function compareByValue(a, b) {
+          return getValue(a) - getValue(b);
+        }
+        groups.sort(compareByValue);
+
+        // Now start fusing elements until none are less than threshold in size.
+        var thresshold = 0.1 * sum;
+        while (groups.length > 1) {
+          var group = groups[0];
+          if (getValue(group) >= thresshold)
+            break;
+
+          var v = getValue(group);
+          if (v + getValue(otherGroup) > thresshold)
+            break;
+
+          // Remove the group from the list and add it to the 'Other' group.
+          groups.splice(0, 1);
+          otherGroup.appendGroupContents(group);
+        }
+
+        // Final return.
+        if (getValue(otherGroup) > 0)
+          groups.push(otherGroup);
+
+        groups.sort(compareByValue);
+
+        return groups;
+      },
+
+      generateResultsForGroup: function(model, name) {
+        return new ResultsForGroup(model, name);
+      },
+
+      createPieChartFromResultGroups: function(
+          groups, title, getValue, opt_extraData) {
+        var chart = new tv.b.ui.PieChart();
+
+        function pushDataForGroup(data, resultsForGroup, value) {
+          data.push({
+            label: resultsForGroup.name,
+            value: value,
+            valueText: tv.c.analysis.tsString(value),
+            resultsForGroup: resultsForGroup
+          });
+        }
+        chart.addEventListener('item-click', function(clickEvent) {
+          var resultsForGroup = clickEvent.data.resultsForGroup;
+          if (resultsForGroup === undefined)
+            return;
+
+          var event = new tv.c.RequestSelectionChangeEvent();
+          event.selection = new tv.c.Selection(resultsForGroup.allSlices);
+          event.selection.timeSummaryGroupName = resultsForGroup.name;
+          chart.dispatchEvent(event);
+        });
+
+
+        // Build chart data.
+        var data = [];
+        groups.forEach(function(resultsForGroup) {
+          var value = getValue(resultsForGroup);
+          if (value === 0)
+            return;
+          pushDataForGroup(data, resultsForGroup, value);
+        });
+        if (opt_extraData)
+          data.push.apply(data, opt_extraData);
+
+        chart.chartTitle = title;
+        chart.data = data;
+        return chart;
+      },
+
+      get model() {
+        return this.model_;
+      },
+
+      set model(model) {
+        this.model_ = model;
+        this.updateContents_();
+      },
+
+      get groupBy() {
+        return groupBy_;
+      },
+
+      set groupBy(groupBy) {
+        this.groupBy_ = groupBy;
+        if (this.groupBySelector_)
+          this.groupBySelector_.selectedValue = groupBy;
+        this.updateContents_();
+      },
+
+      get groupingUnit() {
+        return groupingUnit_;
+      },
+
+      set groupingUnit(groupingUnit) {
+        this.groupingUnit_ = groupingUnit;
+        if (this.groupingUnitSelector_)
+          this.groupingUnitSelector_.selectedValue = groupingUnit;
+        this.updateShowCpuIdleTimeCheckboxVisibility_();
+        this.updateContents_();
+      },
+
+      get showCpuIdleTime() {
+        return this.showCpuIdleTime_;
+      },
+
+      set showCpuIdleTime(showCpuIdleTime) {
+        this.showCpuIdleTime_ = showCpuIdleTime;
+        if (this.showCpuIdleTimeCheckbox_)
+          this.showCpuIdleTimeCheckbox_.checked = showCpuIdleTime;
+        this.updateContents_();
+      },
+
+      updateShowCpuIdleTimeCheckboxVisibility_: function() {
+        if (!this.showCpuIdleTimeCheckbox_)
+          return;
+        var visible = this.groupingUnit_ == CPU_TIME_GROUPING_UNIT;
+        if (visible)
+          this.showCpuIdleTimeCheckbox_.style.display = '';
+        else
+          this.showCpuIdleTimeCheckbox_.style.display = 'none';
+      },
+
+      getGroupNameForThread_: function(thread) {
+        if (this.groupBy_ == GROUP_BY_THREAD_NAME)
+          return thread.name ? thread.name : thread.userFriendlyName;
+
+        if (this.groupBy_ == GROUP_BY_PROCESS_NAME)
+          return thread.parent.userFriendlyName;
+      },
+
+      updateContents_: function() {
+        var resultArea = this.$.result_area;
+        this.chart_ = undefined;
+        resultArea.textContent = '';
+
+        if (this.model_ === undefined)
+          return;
+
+        var rangeOfInterest;
+        if (this.rangeOfInterest_.isEmpty)
+          rangeOfInterest = this.model_.bounds;
+        else
+          rangeOfInterest = this.rangeOfInterest_;
+
+        var allGroup = this.generateResultsForGroup(this.model_, 'all');
+        var resultsByGroupName = {};
+        this.model_.getAllThreads().forEach(function(thread) {
+          var groupName = this.getGroupNameForThread_(thread);
+          if (resultsByGroupName[groupName] === undefined) {
+            resultsByGroupName[groupName] = this.generateResultsForGroup(
+                this.model_, groupName);
+          }
+          resultsByGroupName[groupName].appendThreadSlices(
+              rangeOfInterest, thread);
+
+          allGroup.appendThreadSlices(rangeOfInterest, thread);
+        }, this);
+
+        // Helper function for working with the produced group.
+        var getValueFromGroup = function(group) {
+          if (this.groupingUnit_ == WALL_TIME_GROUPING_UNIT)
+            return group.wallTime;
+          return group.cpuTime;
+        }.bind(this);
+
+        // Create summary.
+        var summaryText = document.createElement('div');
+        summaryText.appendChild(tv.b.ui.createSpan({
+          textContent: 'Total ' + this.groupingUnit_ + ': ',
+          bold: true}));
+        summaryText.appendChild(tv.b.ui.createSpan({
+          textContent: tv.c.analysis.tsString(getValueFromGroup(allGroup))}));
+        resultArea.appendChild(summaryText);
+
+        // If needed, add in the idle time.
+        var extraValue = 0;
+        var extraData = [];
+        if (this.showCpuIdleTime_ &&
+            this.groupingUnit_ === CPU_TIME_GROUPING_UNIT &&
+            this.model.kernel.bestGuessAtCpuCount !== undefined) {
+          var maxCpuTime = rangeOfInterest.range *
+              this.model.kernel.bestGuessAtCpuCount;
+          var idleTime = Math.max(0, maxCpuTime - allGroup.cpuTime);
+          extraData.push({
+            label: 'CPU Idle',
+            value: idleTime,
+            valueText: tv.c.analysis.tsString(idleTime)
+          });
+          extraValue += idleTime;
+        }
+
+        // Create the actual chart.
+        var otherGroup = this.generateResultsForGroup(this.model_, 'Other');
+        var groups = this.trimPieChartData(
+            tv.b.dictionaryValues(resultsByGroupName),
+            otherGroup,
+            getValueFromGroup,
+            extraValue);
+
+        if (groups.length == 0) {
+          resultArea.appendChild(tv.b.ui.createSpan({textContent: 'No data'}));
+          return undefined;
+        }
+
+        this.chart_ = this.createPieChartFromResultGroups(
+            groups,
+            this.groupingUnit_ + ' breakdown by ' + this.groupBy_,
+            getValueFromGroup, extraData);
+        resultArea.appendChild(this.chart_);
+
+        this.chart_.addEventListener('click', function() {
+          var event = new tv.c.RequestSelectionChangeEvent();
+          event.selection = new tv.c.Selection([]);
+          this.dispatchEvent(event);
+        });
+        this.chart_.setSize(this.chart_.getMinSize());
+      },
+
+      get selection() {
+        return selection_;
+      },
+
+      set selection(selection) {
+        this.selection_ = selection;
+
+        if (this.chart_ === undefined)
+          return;
+
+        if (selection.timeSummaryGroupName)
+          this.chart_.highlightedLegendKey = selection.timeSummaryGroupName;
+        else
+          this.chart_.highlightedLegendKey = undefined;
+      },
+
+      get rangeOfInterest() {
+        return this.rangeOfInterest_;
+      },
+
+      set rangeOfInterest(rangeOfInterest) {
+        this.rangeOfInterest_ = rangeOfInterest;
+        this.updateContents_();
+      },
+
+      supportsModel: function(model) {
+        return {
+          supported: true
+        };
+      },
+
+      textLabel: 'Time Summary'
+    });
+  }());
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/side_panel/time_summary_test.html b/trace-viewer/trace_viewer/extras/side_panel/time_summary_test.html
new file mode 100644
index 0000000..505305e
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/side_panel/time_summary_test.html
@@ -0,0 +1,200 @@
+<!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="/extras/side_panel/time_summary.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 newSliceNamed = tv.c.test_utils.newSliceNamed;
+
+  function createModel(opt_options) {
+    var options = opt_options || {};
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+      if (options.provideSoftwareMeasuredCpuCount)
+        m.kernel.softwareMeasuredCpuCount = 2;
+
+      var browserProcess = m.getOrCreateProcess(1);
+      var browserMain = browserProcess.getOrCreateThread(2);
+      browserMain.name = 'CrBrowserMain';
+      browserMain.sliceGroup.beginSlice('cat', 'Task', 0, undefined, 0);
+      browserMain.sliceGroup.endSlice(10, 9);
+      browserMain.sliceGroup.beginSlice('cat', 'Task', 20, undefined, 10);
+      browserMain.sliceGroup.endSlice(30, 20);
+
+      var rendererProcess = m.getOrCreateProcess(4);
+      var rendererMain = rendererProcess.getOrCreateThread(5);
+      rendererMain.name = 'CrRendererMain';
+      rendererMain.sliceGroup.beginSlice('cat', 'Task', 0, undefined, 0);
+      rendererMain.sliceGroup.endSlice(30, 25);
+      rendererMain.sliceGroup.beginSlice('cat', 'Task', 40, undefined, 40);
+      rendererMain.sliceGroup.endSlice(60, 50);
+    });
+    return m;
+  }
+
+  test('group', function() {
+    var ts = document.createElement('tv-e-side-panel-time-summary');
+    var m = createModel();
+    var group = ts.generateResultsForGroup(m, 'foo');
+    group.appendThreadSlices(m.bounds, m.processes[1].threads[2]);
+    assert.equal(group.wallTime, 20);
+    assert.equal(group.cpuTime, 19);
+  });
+
+  test('trim', function() {
+    var groupData = [
+      {
+        value: 2.854999999999997,
+        label: '156959'
+      },
+      {
+        value: 9.948999999999998,
+        label: '16131'
+      },
+      {
+        value: 42.314000000000725,
+        label: '51511'
+      },
+      {
+        value: 31.06900000000028,
+        label: 'AudioOutputDevice'
+      },
+      {
+        value: 1.418,
+        label: 'BrowserBlockingWorker2/50951'
+      },
+      {
+        value: 0.044,
+        label: 'BrowserBlockingWorker3/50695'
+      },
+      {
+        value: 18.52599999999993,
+        label: 'Chrome_ChildIOThread'
+      },
+      {
+        value: 2.888,
+        label: 'Chrome_FileThread'
+      },
+      {
+        value: 0.067,
+        label: 'Chrome_HistoryThread'
+      },
+      {
+        value: 25.421000000000046,
+        label: 'Chrome_IOThread'
+      },
+      {
+        value: 0.019,
+        label: 'Chrome_ProcessLauncherThread'
+      },
+      {
+        value: 643.087999999995,
+        label: 'Compositor'
+      },
+      {
+        value: 4.049999999999973,
+        label: 'CompositorRasterWorker1/22031'
+      },
+      {
+        value: 50.040000000000106,
+        label: 'CrBrowserMain'
+      },
+      {
+        value: 1256.5130000000042,
+        label: 'CrGpuMain'
+      },
+      {
+        value: 5502.19499999999,
+        label: 'CrRendererMain'
+      },
+      {
+        value: 15.552999999999862,
+        label: 'FFmpegDemuxer'
+      },
+      {
+        value: 63.706000000001524,
+        label: 'Media'
+      },
+      {
+        value: 2.7419999999999987,
+        label: 'PowerSaveBlocker'
+      },
+      {
+        value: 0.11500000000000005,
+        label: 'Watchdog'
+      }
+    ];
+
+    var ts = document.createElement('tv-e-side-panel-time-summary');
+
+    var groups = [];
+    var m = new tv.c.TraceModel();
+    m.importTraces([], false, false, function() {
+      var start = 0;
+      groupData.forEach(function(groupData) {
+        var group = ts.generateResultsForGroup(m, groupData.label);
+
+        var slice = newSliceNamed(groupData.label, start, groupData.value);
+        start += groupData.value;
+        group.allSlices.push(slice);
+        group.topLevelSlices.push(slice);
+
+        groups.push(group);
+      });
+    });
+
+    function getValueFromGroup(d) { return d.wallTime; }
+
+    var otherGroup = ts.generateResultsForGroup(m, 'Other');
+    var newGroups = ts.trimPieChartData(groups, otherGroup, getValueFromGroup);
+
+    // Visualize the data once its trimmed.
+    var ce = document.createElement('tv-e-side-panel-time-summary');
+    var chart = ce.createPieChartFromResultGroups(
+        newGroups, 'Trimmed', getValueFromGroup);
+    this.addHTMLOutput(chart);
+    chart.setSize(chart.getMinSize());
+  });
+
+  test('basicInWallTimeMode', function() {
+    var m = createModel();
+
+    var panel = document.createElement('tv-e-side-panel-time-summary');
+    this.addHTMLOutput(panel);
+    panel.model = m;
+    panel.groupingUnit = 'Wall time';
+    panel.style.border = '1px solid black';
+  });
+
+  test('basicInCpuTimeModeButNoCpuData', function() {
+    var m = createModel();
+
+    var panel = document.createElement('tv-e-side-panel-time-summary');
+    this.addHTMLOutput(panel);
+    panel.model = m;
+    panel.groupingUnit = 'CPU time';
+    panel.style.border = '1px solid black';
+  });
+
+  test('basicInCpuTimeModeAndCpuData', function() {
+    var m = createModel({
+      provideSoftwareMeasuredCpuCount: true
+    });
+
+    var panel = document.createElement('tv-e-side-panel-time-summary');
+    this.addHTMLOutput(panel);
+    panel.model = m;
+    panel.groupingUnit = 'CPU time';
+    panel.style.border = '1px solid black';
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats.html b/trace-viewer/trace_viewer/extras/system_stats/system_stats.html
new file mode 100644
index 0000000..d28bfe6
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats.html
@@ -0,0 +1,10 @@
+<!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/system_stats/system_stats_snapshot.html">
+<link rel="import" href="/extras/system_stats/system_stats_snapshot_view.html">
+<link rel="import" href="/extras/system_stats/system_stats_instance_track.html">
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track.css b/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track.css
new file mode 100644
index 0000000..db530ec
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track.css
@@ -0,0 +1,15 @@
+/* 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.
+ */
+
+.system-stats-instance-track {
+  height: 500px;
+}
+
+.system-stats-instance-track ul {
+  list-style: none;
+  list-style-position: outside;
+  margin: 0;
+  overflow: hidden;
+}
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track.html b/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track.html
new file mode 100644
index 0000000..a1a1423
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_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="/extras/system_stats/system_stats_instance_track.css">
+<link rel="import" href="/base/sorted_array_utils.html">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/core/tracks/stacked_bars_track.html">
+<link rel="import" href="/core/tracks/object_instance_track.html">
+<link rel="import" href="/core/event_presenter.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.system_stats', function() {
+  var EventPresenter = tv.c.EventPresenter;
+
+  var palette = tv.b.ui.getColorPalette();
+  var highlightIdBoost = tv.b.ui.getColorPaletteHighlightIdBoost();
+
+  var statCount;
+
+  var excludedStats = {'meminfo': {
+                        'pswpin': 0,
+                        'pswpout': 0,
+                        'pgmajfault': 0},
+                      'diskinfo': {
+                        'io': 0,
+                        'io_time': 0,
+                        'read_time': 0,
+                        'reads': 0,
+                        'reads_merged': 0,
+                        'sectors_read': 0,
+                        'sectors_written': 0,
+                        'weighted_io_time': 0,
+                        'write_time': 0,
+                        'writes': 0,
+                        'writes_merged': 0},
+                      'swapinfo': {}
+                      };
+
+  /**
+   * Tracks that display system stats data.
+   *
+   * @constructor
+   * @extends {StackedBarsTrack}
+   */
+
+  var SystemStatsInstanceTrack = tv.b.ui.define(
+      'system-stats-instance-track', tv.c.tracks.StackedBarsTrack);
+
+  SystemStatsInstanceTrack.prototype = {
+
+    __proto__: tv.c.tracks.StackedBarsTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.StackedBarsTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('system-stats-instance-track');
+      this.objectInstance_ = null;
+    },
+
+    set objectInstances(objectInstances) {
+      if (!objectInstances) {
+        this.objectInstance_ = [];
+        return;
+      }
+      if (objectInstances.length != 1)
+        throw new Error('Bad object instance count.');
+      this.objectInstance_ = objectInstances[0];
+      if (this.objectInstance_ !== null) {
+        this.computeRates_(this.objectInstance_.snapshots);
+        this.maxStats_ = this.computeMaxStats_(
+            this.objectInstance_.snapshots);
+      }
+    },
+
+    computeRates_: function(snapshots) {
+      for (var i = 0; i < snapshots.length; i++) {
+        var snapshot = snapshots[i];
+        var stats = snapshot.getStats();
+        var prevSnapshot;
+        var prevStats;
+
+        if (i == 0) {
+          // Deltas will be zero.
+          prevSnapshot = snapshots[0];
+        } else {
+          prevSnapshot = snapshots[i - 1];
+        }
+        prevStats = prevSnapshot.getStats();
+        var timeIntervalSeconds = (snapshot.ts - prevSnapshot.ts) / 1000;
+        // Prevent divide by zero.
+        if (timeIntervalSeconds == 0)
+          timeIntervalSeconds = 1;
+
+        this.computeRatesRecursive_(prevStats, stats,
+                                    timeIntervalSeconds);
+      }
+    },
+
+    computeRatesRecursive_: function(prevStats, stats,
+                                     timeIntervalSeconds) {
+      for (var statName in stats) {
+        if (stats[statName] instanceof Object) {
+          this.computeRatesRecursive_(prevStats[statName],
+                                      stats[statName],
+                                      timeIntervalSeconds);
+        } else {
+          if (statName == 'sectors_read') {
+            stats['bytes_read_per_sec'] = (stats['sectors_read'] -
+                                           prevStats['sectors_read']) *
+                                          512 / timeIntervalSeconds;
+          }
+          if (statName == 'sectors_written') {
+            stats['bytes_written_per_sec'] =
+                (stats['sectors_written'] -
+                 prevStats['sectors_written']) *
+                512 / timeIntervalSeconds;
+          }
+          if (statName == 'pgmajfault') {
+            stats['pgmajfault_per_sec'] = (stats['pgmajfault'] -
+                                           prevStats['pgmajfault']) /
+                                          timeIntervalSeconds;
+          }
+          if (statName == 'pswpin') {
+            stats['bytes_swpin_per_sec'] = (stats['pswpin'] -
+                                            prevStats['pswpin']) *
+                                           1000 / timeIntervalSeconds;
+          }
+          if (statName == 'pswpout') {
+            stats['bytes_swpout_per_sec'] = (stats['pswpout'] -
+                                             prevStats['pswpout']) *
+                                            1000 / timeIntervalSeconds;
+          }
+        }
+      }
+    },
+
+    computeMaxStats_: function(snapshots) {
+      var maxStats = new Object();
+      statCount = 0;
+
+      for (var i = 0; i < snapshots.length; i++) {
+        var snapshot = snapshots[i];
+        var stats = snapshot.getStats();
+
+        this.computeMaxStatsRecursive_(stats, maxStats,
+                                       excludedStats);
+      }
+
+      return maxStats;
+    },
+
+    computeMaxStatsRecursive_: function(stats, maxStats, excludedStats) {
+      for (var statName in stats) {
+        if (stats[statName] instanceof Object) {
+          if (!(statName in maxStats))
+            maxStats[statName] = new Object();
+
+          var excludedNested;
+          if (excludedStats && statName in excludedStats)
+            excludedNested = excludedStats[statName];
+          else
+            excludedNested = null;
+
+          this.computeMaxStatsRecursive_(stats[statName],
+                                         maxStats[statName],
+                                         excludedNested);
+        } else {
+          if (excludedStats && statName in excludedStats)
+            continue;
+          if (!(statName in maxStats)) {
+            maxStats[statName] = 0;
+            statCount++;
+          }
+          if (stats[statName] > maxStats[statName])
+            maxStats[statName] = stats[statName];
+        }
+      }
+    },
+
+    get height() {
+      return window.getComputedStyle(this).height;
+    },
+
+    set height(height) {
+      this.style.height = height;
+    },
+
+    draw: function(type, viewLWorld, viewRWorld) {
+      switch (type) {
+        case tv.c.tracks.DrawType.SLICE:
+          this.drawStatBars_(viewLWorld, viewRWorld);
+          break;
+      }
+    },
+
+    drawStatBars_: function(viewLWorld, viewRWorld) {
+      var ctx = this.context();
+      var pixelRatio = window.devicePixelRatio || 1;
+
+      var bounds = this.getBoundingClientRect();
+      var width = bounds.width * pixelRatio;
+      var height = (bounds.height * pixelRatio) / statCount;
+
+      // Culling parameters.
+      var vp = this.viewport.currentDisplayTransform;
+
+      // Scale by the size of the largest snapshot.
+      var maxStats = this.maxStats_;
+
+      var objectSnapshots = this.objectInstance_.snapshots;
+      var lowIndex = tv.b.findLowIndexInSortedArray(
+          objectSnapshots,
+          function(snapshot) {
+            return snapshot.ts;
+          },
+          viewLWorld);
+
+      // Assure that the stack with the left edge off screen still gets drawn
+      if (lowIndex > 0)
+        lowIndex -= 1;
+
+      for (var i = lowIndex; i < objectSnapshots.length; ++i) {
+        var snapshot = objectSnapshots[i];
+        var trace = snapshot.getStats();
+        var currentY = height;
+
+        var left = snapshot.ts;
+        if (left > viewRWorld)
+          break;
+        var leftView = vp.xWorldToView(left);
+        if (leftView < 0)
+          leftView = 0;
+
+        // Compute the edges for the column graph bar.
+        var right;
+        if (i != objectSnapshots.length - 1) {
+          right = objectSnapshots[i + 1].ts;
+        } else {
+          // If this is the last snaphot of multiple snapshots, use the width of
+          // the previous snapshot for the width.
+          if (objectSnapshots.length > 1)
+            right = objectSnapshots[i].ts + (objectSnapshots[i].ts -
+                    objectSnapshots[i - 1].ts);
+          else
+            // If there's only one snapshot, use max bounds as the width.
+            right = this.objectInstance_.parent.model.bounds.max;
+        }
+
+        var rightView = vp.xWorldToView(right);
+        if (rightView > width)
+          rightView = width;
+
+        // Floor the bounds to avoid a small gap between stacks.
+        leftView = Math.floor(leftView);
+        rightView = Math.floor(rightView);
+
+        // Descend into nested stats.
+        this.drawStatBarsRecursive_(snapshot,
+                                    leftView,
+                                    rightView,
+                                    height,
+                                    trace,
+                                    maxStats,
+                                    currentY);
+
+        if (i == lowIndex)
+          this.drawStatNames_(leftView, height, currentY, '', maxStats);
+      }
+      ctx.lineWidth = 1;
+    },
+
+    drawStatBarsRecursive_: function(snapshot,
+                                     leftView,
+                                     rightView,
+                                     height,
+                                     stats,
+                                     maxStats,
+                                     currentY) {
+      var ctx = this.context();
+
+      for (var statName in maxStats) {
+        if (stats[statName] instanceof Object) {
+          // Use the y-position returned from the recursive call.
+          currentY = this.drawStatBarsRecursive_(snapshot,
+                                                 leftView,
+                                                 rightView,
+                                                 height,
+                                                 stats[statName],
+                                                 maxStats[statName],
+                                                 currentY);
+        } else {
+          var maxStat = maxStats[statName];
+
+          // Draw a bar for the stat. The height of the bar is scaled
+          // against the largest value of the stat across all snapshots.
+          ctx.fillStyle = EventPresenter.getBarSnapshotColor(
+              snapshot, Math.round(currentY / height));
+
+          var barHeight;
+
+          if (maxStat > 0) {
+            barHeight = height * Math.max(stats[statName], 0) / maxStat;
+          } else {
+            barHeight = 0;
+          }
+
+          ctx.fillRect(leftView, currentY - barHeight,
+                       Math.max(rightView - leftView, 1), barHeight);
+
+          currentY += height;
+        }
+      }
+
+      // Return the updated y-position.
+      return currentY;
+    },
+
+    drawStatNames_: function(leftView, height, currentY, prefix, maxStats) {
+      var ctx = this.context();
+
+      ctx.textAlign = 'end';
+      ctx.font = '12px Arial';
+      ctx.fillStyle = '#000000';
+      for (var statName in maxStats) {
+        if (maxStats[statName] instanceof Object) {
+          currentY = this.drawStatNames_(leftView, height, currentY,
+                                         statName, maxStats[statName]);
+        } else {
+          var fullname = statName;
+
+          if (prefix != '')
+            fullname = prefix + ' :: ' + statName;
+
+          ctx.fillText(fullname, leftView - 10, currentY - height / 4);
+          currentY += height;
+        }
+      }
+
+      return currentY;
+    }
+  };
+
+  tv.c.tracks.ObjectInstanceTrack.register(
+      SystemStatsInstanceTrack,
+      {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+  return {
+    SystemStatsInstanceTrack: SystemStatsInstanceTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track_test.html b/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track_test.html
new file mode 100644
index 0000000..c15a4ae
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats_instance_track_test.html
@@ -0,0 +1,69 @@
+<!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/system_stats/system_stats.html">
+<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/timeline_viewport.html">
+<link rel="import" href="/core/tracks/drawing_container.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() { // @suppress longLineCheck
+  var SystemStatsInstanceTrack = tv.e.system_stats.SystemStatsInstanceTrack;
+  var Viewport = tv.c.TimelineViewport;
+
+  var createObjects = function() {
+    var objectInstance = new tv.c.trace_model.ObjectInstance({});
+    var snapshots = [];
+
+    var stats1 = new Object();
+    var stats2 = new Object();
+
+    stats1['committed_memory'] = 2000000;
+    stats2['committed_memory'] = 3000000;
+
+    stats1['meminfo'] = new Object();
+    stats1.meminfo['free'] = 10000;
+    stats2['meminfo'] = new Object();
+    stats2.meminfo['free'] = 20000;
+
+    snapshots.push(new tv.e.system_stats.SystemStatsSnapshot(objectInstance,
+                                                             10, stats1));
+    snapshots.push(new tv.e.system_stats.SystemStatsSnapshot(objectInstance,
+                                                             20, stats2));
+
+    objectInstance.snapshots = snapshots;
+
+    return objectInstance;
+  };
+
+  test('instantiate', function() {
+    var objectInstances = [];
+    objectInstances.push(createObjects());
+
+    var div = document.createElement('div');
+    var viewport = new Viewport(div);
+    var drawingContainer = new tv.c.tracks.DrawingContainer(viewport);
+    div.appendChild(drawingContainer);
+
+    var track = new SystemStatsInstanceTrack(viewport);
+    track.objectInstances = objectInstances;
+    drawingContainer.appendChild(track);
+
+    this.addHTMLOutput(div);
+    drawingContainer.invalidate();
+
+    track.heading = 'testBasic';
+    var dt = new tv.c.TimelineDisplayTransform();
+    dt.xSetWorldBounds(0, 50, track.clientWidth);
+    track.viewport.setDisplayTransformImmediately(dt);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot.html b/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot.html
new file mode 100644
index 0000000..aaab262
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot.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="/extras/cc/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.system_stats', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function SystemStatsSnapshot(objectInstance, ts, args) {
+    ObjectSnapshot.apply(this, arguments);
+    this.objectInstance = objectInstance;
+    this.ts = ts;
+    this.args = args;
+    this.stats = args;
+  }
+
+  SystemStatsSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    initialize: function() {
+      if (this.args.length == 0)
+        throw new Error('No system stats snapshot data.');
+      this.stats_ = this.args;
+    },
+
+    getStats: function() {
+      return this.stats_;
+    },
+
+    setStats: function(stats) {
+      this.stats_ = stats;
+    }
+  };
+
+  ObjectSnapshot.register(
+    SystemStatsSnapshot,
+    {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+  return {
+    SystemStatsSnapshot: SystemStatsSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot_view.css b/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot_view.css
new file mode 100644
index 0000000..130463e
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot_view.css
@@ -0,0 +1,28 @@
+/* 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.
+ */
+
+.subhead {
+  font-size: small;
+  padding-bottom: 10px;
+}
+
+.system-stats-snapshot-view ul {
+  background-position: 0 5px;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  font-family: monospace;
+  list-style: none;
+  margin: 0;
+  padding-left: 15px;
+}
+
+.system-stats-snapshot-view li {
+  background-position: 0 5px;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  list-style: none;
+  margin: 0;
+  padding-left: 15px;
+}
diff --git a/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot_view.html b/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot_view.html
new file mode 100644
index 0000000..ce42b73
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/system_stats/system_stats_snapshot_view.html
@@ -0,0 +1,83 @@
+<!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="/extras/system_stats/system_stats_snapshot_view.css">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.system_stats', function() {
+  /*
+   * Displays a system stats snapshot in a human readable form. @constructor
+   */
+  var SystemStatsSnapshotView = tv.b.ui.define('system-stats-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  SystemStatsSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('system-stats-snapshot-view');
+    },
+
+    updateContents: function() {
+      var snapshot = this.objectSnapshot_;
+      if (!snapshot || !snapshot.getStats()) {
+        this.textContent = 'No system stats snapshot found.';
+        return;
+      }
+      // Clear old snapshot view.
+      this.textContent = '';
+
+      var stats = snapshot.getStats();
+      this.appendChild(this.buildList_(stats));
+    },
+
+    isFloat: function(n) {
+      return typeof n === 'number' && n % 1 !== 0;
+    },
+
+    /**
+     * Creates nested lists.
+     *
+     * @param {Object} stats The current trace system stats entry.
+     * @return {Element} A ul list element.
+     */
+    buildList_: function(stats) {
+      var statList = document.createElement('ul');
+
+      for (var statName in stats) {
+        var statText = document.createElement('li');
+        statText.textContent = '' + statName + ': ';
+        statList.appendChild(statText);
+
+        if (stats[statName] instanceof Object) {
+          statList.appendChild(this.buildList_(stats[statName]));
+        } else {
+          if (this.isFloat(stats[statName]))
+            statText.textContent += stats[statName].toFixed(2);
+          else
+            statText.textContent += stats[statName];
+        }
+      }
+
+      return statList;
+    }
+  };
+
+  tv.c.analysis.ObjectSnapshotView.register(
+      SystemStatsSnapshotView,
+      {typeName: 'base::TraceEventSystemStatsMonitor::SystemStats'});
+
+  return {
+    SystemStatsSnapshotView: SystemStatsSnapshotView
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/systrace_config.html b/trace-viewer/trace_viewer/extras/systrace_config.html
new file mode 100644
index 0000000..8b19dc6
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/systrace_config.html
@@ -0,0 +1,12 @@
+<!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.
+-->
+
+<!-- This config is used by Android systrace. -->
+<link rel="import" href="/extras/importer/ddms_importer.html">
+<link rel="import" href="/extras/importer/linux_perf/linux_perf_importer.html">
+<link rel="import" href="/extras/side_panel/alerts_side_panel.html">
+<link rel="import" href="/extras/audits/android_auditor.html">
\ No newline at end of file
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/heap.html b/trace-viewer/trace_viewer/extras/tcmalloc/heap.html
new file mode 100644
index 0000000..d3778a2
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/heap.html
@@ -0,0 +1,98 @@
+<!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="/extras/cc/util.html">
+<script>
+'use strict';
+
+tv.exportTo('tv.e.tcmalloc', function() {
+  var ObjectSnapshot = tv.c.trace_model.ObjectSnapshot;
+
+  /**
+   * @constructor
+   */
+  function HeapSnapshot() {
+    ObjectSnapshot.apply(this, arguments);
+  }
+
+  HeapSnapshot.prototype = {
+    __proto__: ObjectSnapshot.prototype,
+
+    preInitialize: function() {
+      tv.e.cc.preInitializeObject(this);
+
+      // TODO(jamescook): Any generic field setup can go here.
+    },
+
+    // TODO(jamescook): This seems to be called before the green dot is clicked.
+    // Consider doing it in heap_view.js.
+    initialize: function() {
+      if (this.args.length == 0)
+        throw new Error('No heap snapshot data.');
+
+      // The first entry is total allocations across all stack traces.
+      this.total_ = this.args[0];
+      // The rest is a list of allocations.
+      var allocs = this.args.slice(1);
+
+      // Build a nested dictionary of trace event names.
+      this.heap_ = {
+        children: {},
+        currentBytes: 0,
+        currentAllocs: 0,
+        totalBytes: 0,
+        totalAllocs: 0
+      };
+      for (var i = 0; i < allocs.length; i++) {
+        var alloc = allocs[i];
+        var traceNames = alloc.trace.split(' ');
+        // We don't want to record allocations caused by the heap profiling
+        // system itself, so skip allocations with this special name.
+        if (traceNames.indexOf('trace-memory-ignore') != -1)
+          continue;
+        var heapEntry = this.heap_;
+        // Walk down into the heap of stack traces.
+        for (var j = 0; j < traceNames.length; j++) {
+          // Look for existing children with this trace.
+          var traceName = traceNames[j];
+          // The empty trace name means "(here)", so don't roll those up into
+          // parent traces because they have already been counted.
+          if (traceName.length != 0) {
+            // Add up the total memory for intermediate entries, so the top of
+            // each subtree is the total memory for that tree.
+            heapEntry.currentBytes += alloc.currentBytes;
+            heapEntry.currentAllocs += alloc.currentAllocs;
+            heapEntry.totalBytes += alloc.totalBytes;
+            heapEntry.totalAllocs += alloc.totalAllocs;
+          }
+          if (!heapEntry.children[traceName]) {
+            // New trace entry at this depth, so create a child for it.
+            heapEntry.children[traceName] = {
+              children: {},
+              currentBytes: alloc.currentBytes,
+              currentAllocs: alloc.currentAllocs,
+              totalBytes: alloc.totalBytes,
+              totalAllocs: alloc.totalAllocs
+            };
+          }
+          // Descend into the children.
+          heapEntry = heapEntry.children[traceName];
+        }
+      }
+    }
+
+  };
+
+  ObjectSnapshot.register(
+    HeapSnapshot,
+    {typeName: 'memory::Heap'});
+
+  return {
+    HeapSnapshot: HeapSnapshot
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/heap_instance_track.css b/trace-viewer/trace_viewer/extras/tcmalloc/heap_instance_track.css
new file mode 100644
index 0000000..145a046
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/heap_instance_track.css
@@ -0,0 +1,15 @@
+/* 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.
+ */
+
+.heap-instance-track {
+  height: 150px;
+}
+
+.heap-instance-track ul {
+  list-style: none;
+  list-style-position: outside;
+  margin: 0;
+  overflow: hidden;
+}
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/heap_instance_track.html b/trace-viewer/trace_viewer/extras/tcmalloc/heap_instance_track.html
new file mode 100644
index 0000000..629dd0f
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/heap_instance_track.html
@@ -0,0 +1,176 @@
+<!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="/extras/tcmalloc/heap_instance_track.css">
+<link rel="import" href="/core/tracks/stacked_bars_track.html">
+<link rel="import" href="/core/tracks/object_instance_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.e.tcmalloc', function() {
+  var EventPresenter = tv.c.EventPresenter;
+
+  /**
+   * A track that displays heap memory data.
+   * @constructor
+   * @extends {StackedBarsTrack}
+   */
+
+  var HeapInstanceTrack = tv.b.ui.define(
+      'heap-instance-track', tv.c.tracks.StackedBarsTrack);
+
+  HeapInstanceTrack.prototype = {
+
+    __proto__: tv.c.tracks.StackedBarsTrack.prototype,
+
+    decorate: function(viewport) {
+      tv.c.tracks.StackedBarsTrack.prototype.decorate.call(this, viewport);
+      this.classList.add('heap-instance-track');
+      this.objectInstance_ = null;
+    },
+
+    set objectInstances(objectInstances) {
+      if (!objectInstances) {
+        this.objectInstance_ = [];
+        return;
+      }
+      if (objectInstances.length != 1)
+        throw new Error('Bad object instance count.');
+      this.objectInstance_ = objectInstances[0];
+      this.maxBytes_ = this.computeMaxBytes_(
+          this.objectInstance_.snapshots);
+    },
+
+    computeMaxBytes_: function(snapshots) {
+      var maxBytes = 0;
+      for (var i = 0; i < snapshots.length; i++) {
+        var snapshot = snapshots[i];
+        // Sum all the current allocations in this snapshot.
+        var traceNames = Object.keys(snapshot.heap_.children);
+        var sumBytes = 0;
+        for (var j = 0; j < traceNames.length; j++) {
+          sumBytes += snapshot.heap_.children[traceNames[j]].currentBytes;
+        }
+        // Keep track of the maximum across all snapshots.
+        if (sumBytes > maxBytes)
+          maxBytes = sumBytes;
+      }
+      return maxBytes;
+    },
+
+    get height() {
+      return window.getComputedStyle(this).height;
+    },
+
+    set height(height) {
+      this.style.height = height;
+    },
+
+    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 width = bounds.width * pixelRatio;
+      var height = bounds.height * pixelRatio;
+
+      // Culling parameters.
+      var dt = this.viewport.currentDisplayTransform;
+
+      // Scale by the size of the largest snapshot.
+      var maxBytes = this.maxBytes_;
+
+      var objectSnapshots = this.objectInstance_.snapshots;
+      var lowIndex = tv.b.findLowIndexInSortedArray(
+          objectSnapshots,
+          function(snapshot) {
+            return snapshot.ts;
+          },
+          viewLWorld);
+      // Assure that the stack with the left edge off screen still gets drawn
+      if (lowIndex > 0)
+        lowIndex -= 1;
+
+      for (var i = lowIndex; i < objectSnapshots.length; ++i) {
+        var snapshot = objectSnapshots[i];
+
+        var left = snapshot.ts;
+        if (left > viewRWorld)
+          break;
+        var leftView = dt.xWorldToView(left);
+        if (leftView < 0)
+          leftView = 0;
+
+        // Compute the edges for the column graph bar.
+        var right;
+        if (i != objectSnapshots.length - 1) {
+          right = objectSnapshots[i + 1].ts;
+        } else {
+          // If this is the last snaphot of multiple snapshots, use the width of
+          // the previous snapshot for the width.
+          if (objectSnapshots.length > 1)
+            right = objectSnapshots[i].ts + (objectSnapshots[i].ts -
+                    objectSnapshots[i - 1].ts);
+          else
+            // If there's only one snapshot, use max bounds as the width.
+            right = this.objectInstance_.parent.model.bounds.max;
+        }
+
+        var rightView = dt.xWorldToView(right);
+        if (rightView > width)
+          rightView = width;
+
+        // Floor the bounds to avoid a small gap between stacks.
+        leftView = Math.floor(leftView);
+        rightView = Math.floor(rightView);
+
+        // Draw a stacked bar graph. Largest item is stored first in the
+        // heap data structure, so iterate backwards. Likewise draw from
+        // the bottom of the bar upwards.
+        var currentY = height;
+        var keys = Object.keys(snapshot.heap_.children);
+        for (var k = keys.length - 1; k >= 0; k--) {
+          var trace = snapshot.heap_.children[keys[k]];
+          if (this.objectInstance_.selectedTraces &&
+              this.objectInstance_.selectedTraces.length > 0 &&
+              this.objectInstance_.selectedTraces[0] == keys[k]) {
+            // A trace selected in the analysis view is bright yellow.
+            ctx.fillStyle = 'rgb(239, 248, 206)';
+          } else
+            ctx.fillStyle = EventPresenter.getBarSnapshotColor(snapshot, k);
+
+          var barHeight = height * trace.currentBytes / maxBytes;
+          ctx.fillRect(leftView, currentY - barHeight,
+                       Math.max(rightView - leftView, 1), barHeight);
+          currentY -= barHeight;
+        }
+      }
+      ctx.lineWidth = 1;
+    }
+  };
+
+  tv.c.tracks.ObjectInstanceTrack.register(
+      HeapInstanceTrack,
+      {typeName: 'memory::Heap'});
+
+  return {
+    HeapInstanceTrack: HeapInstanceTrack
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/heap_test.html b/trace-viewer/trace_viewer/extras/tcmalloc/heap_test.html
new file mode 100644
index 0000000..6b13bf1
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/heap_test.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="/extras/tcmalloc/heap.html">
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  var HeapSnapshot = tv.e.tcmalloc.HeapSnapshot;
+
+  // Tests total allocation count.
+  test('totals', function() {
+    var snapshot = new HeapSnapshot({}, 1, [
+      {
+        'current_allocs': 10,
+        'total_allocs': 100,
+        'current_bytes': 10000,
+        'trace': '',
+        'total_bytes': 100000
+      },
+      {
+        'current_allocs': 2,
+        'total_allocs': 22,
+        'current_bytes': 200,
+        'trace': 'TestObject::TestMethod ',
+        'total_bytes': 2200
+      }
+    ]);
+    snapshot.preInitialize();
+    snapshot.initialize();
+
+    // Base class got the timestamp.
+    assert.equal(snapshot.ts, 1);
+
+    // The first entry in the list is for totals.
+    assert.equal(snapshot.total_.currentAllocs, 10);
+    assert.equal(snapshot.total_.currentBytes, 10000);
+  });
+
+  // Tests multi-level trace stacks.
+  test('multiLevel', function() {
+    var snapshot = new HeapSnapshot({}, 1, [
+      {
+        'current_allocs': 10,
+        'total_allocs': 100,
+        'current_bytes': 10000,
+        'trace': '',
+        'total_bytes': 100000
+      },
+      {
+        'current_allocs': 2,
+        'total_allocs': 22,
+        'current_bytes': 200,
+        'trace': 'TestObject::TestMethod ',
+        'total_bytes': 2200
+      },
+      {
+        'current_allocs': 3,
+        'total_allocs': 33,
+        'current_bytes': 300,
+        'trace': 'TestObject2::TestMethod2  ',
+        'total_bytes': 3300
+      },
+      {
+        'current_allocs': 5,
+        'total_allocs': 55,
+        'current_bytes': 500,
+        'trace': 'TestObject2::TestMethod2 TestObject3::TestMethod3 ',
+        'total_bytes': 5500
+      }
+    ]);
+    snapshot.preInitialize();
+    snapshot.initialize();
+
+    // Our heap has two top-level stacks.
+    var heap = snapshot.heap_;
+    var childKeys = Object.keys(heap.children);
+    assert.equal(childKeys.length, 2);
+    // Both methods exist as children.
+    assert.notEqual(childKeys.indexOf('TestObject::TestMethod'), -1);
+    assert.notEqual(childKeys.indexOf('TestObject2::TestMethod2'), -1);
+
+    // Verify the first trace entry stack.
+    var trace = heap.children['TestObject::TestMethod'];
+    assert.equal(trace.currentAllocs, 2);
+    assert.equal(trace.currentBytes, 200);
+    // One child for "(here)".
+    assert.equal(Object.keys(trace.children).length, 1);
+    assert.isNotNull(trace.children['(here)']);
+
+    // Verify the second trace entry stack.
+    trace = heap.children['TestObject2::TestMethod2'];
+    // Memory should have summed up.
+    assert.equal(trace.currentAllocs, 8);
+    assert.equal(trace.currentBytes, 800);
+    // Two children, "(here)" and another stack.
+    assert.equal(Object.keys(trace.children).length, 2);
+    assert.isNotNull(trace.children['TestObject3::TestMethod3']);
+    assert.isNotNull(trace.children['(here)']);
+
+    trace = trace.children['TestObject3::TestMethod3'];
+    assert.equal(trace.currentAllocs, 5);
+    assert.equal(trace.currentBytes, 500);
+    assert.equal(Object.keys(trace.children).length, 1);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/images/collapse.png b/trace-viewer/trace_viewer/extras/tcmalloc/images/collapse.png
new file mode 100644
index 0000000..c5fb718
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/images/collapse.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/images/expand.png b/trace-viewer/trace_viewer/extras/tcmalloc/images/expand.png
new file mode 100644
index 0000000..8f2d0ef
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/images/expand.png
Binary files differ
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc.html b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc.html
new file mode 100644
index 0000000..5a22df6
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc.html
@@ -0,0 +1,11 @@
+<!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/tcmalloc/heap.html">
+<link rel="import" href="/extras/tcmalloc/tcmalloc_instance_view.html">
+<link rel="import" href="/extras/tcmalloc/tcmalloc_snapshot_view.html">
+<link rel="import" href="/extras/tcmalloc/heap_instance_track.html">
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_instance_view.css b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_instance_view.css
new file mode 100644
index 0000000..c22b8e5
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_instance_view.css
@@ -0,0 +1,37 @@
+/* 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.
+ */
+
+.subhead {
+  font-size: small;
+  padding-bottom: 10px;
+}
+
+.tcmalloc-instance-view #args {
+  white-space: pre;
+}
+
+.tcmalloc-instance-view #snapshots > * {
+  display: block;
+}
+
+.tcmalloc-instance-view {
+  overflow: auto;
+}
+
+.tcmalloc-instance-view * {
+  -webkit-user-select: text;
+}
+
+.tcmalloc-instance-view .title {
+  border-bottom: 1px solid rgb(128, 128, 128);
+  font-size: 110%;
+  font-weight: bold;
+}
+
+.tcmalloc-instance-view td,
+.tcmalloc-instance-view th {
+  font-size: small;
+  text-align: right;
+}
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_instance_view.html b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_instance_view.html
new file mode 100644
index 0000000..fb4b464
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_instance_view.html
@@ -0,0 +1,111 @@
+<!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="/extras/tcmalloc/tcmalloc_instance_view.css">
+<link rel="import" href="/core/analysis/object_instance_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.tcmalloc', function() {
+  /**
+   * Displays tcmalloc heap memory information over time. A tcmalloc instance
+   * has multiple snapshots.
+   * @constructor
+   */
+  var TcmallocInstanceView = tv.b.ui.define(
+      'tcmalloc-instance-view', tv.c.analysis.ObjectInstanceView);
+
+  TcmallocInstanceView.prototype = {
+    __proto__: tv.c.analysis.ObjectInstanceView.prototype,
+
+    decorate: function() {
+      tv.c.analysis.ObjectInstanceView.prototype.decorate.apply(this);
+      this.classList.add('tcmalloc-instance-view');
+    },
+
+    updateContents: function() {
+      var instance = this.objectInstance_;
+      if (!instance || !instance.snapshots || instance.snapshots.length == 0) {
+        this.textContent = 'No data found.';
+        return;
+      }
+      // Clear old view.
+      this.textContent = '';
+
+      // First, grab the largest N traces from the first snapshot.
+      var snapshot = instance.snapshots[0];
+      var heapEntry = snapshot.heap_;
+      var traceNames = Object.keys(heapEntry.children);
+      traceNames.sort(function(a, b) {
+        // Sort from large to small.
+        return heapEntry.children[b].currentBytes -
+            heapEntry.children[a].currentBytes;
+      });
+      // Only use the largest 5 traces to start
+      traceNames = traceNames.slice(0, 5);
+
+      var table = document.createElement('table');
+      var titles = ['Total'];
+      titles = titles.concat(traceNames);
+      table.appendChild(this.buildRow_(null, titles));
+
+      // One array per trace name.
+      var chartArrays = [[], [], [], [], []];
+      for (var i = 0; i < instance.snapshots.length; i++) {
+        var snapshot = instance.snapshots[i];
+        var rowData = [snapshot.total_.currentBytes];
+        for (var j = 0; j < 5; j++) {
+          var bytes = snapshot.heap_.children[traceNames[j]].currentBytes;
+          rowData.push(bytes);
+          // Associate a megabyte count with a time in seconds.
+          chartArrays[j].push(
+              [Math.round(snapshot.ts / 1000), bytes / 1024 / 1024]);
+        }
+        var row = this.buildRow_(snapshot, rowData);
+        table.appendChild(row);
+      }
+      this.appendChild(table);
+    },
+
+    buildRow_: function(snapshot, items) {
+      var row = document.createElement('tr');
+      var td = document.createElement('td');
+      if (snapshot) {
+        var snapshotLink = document.createElement('tv-c-analysis-link');
+        snapshotLink.selection = new tv.c.Selection(snapshot);
+        td.appendChild(snapshotLink);
+      }
+      row.appendChild(td);
+      for (var i = 0; i < items.length; i++) {
+        var data = document.createElement('td');
+        data.textContent = items[i];
+        row.appendChild(data);
+      }
+      return row;
+    },
+
+    /*
+     * Returns a human readable string for a size in bytes.
+     */
+    getByteString_: function(bytes) {
+      var mb = bytes / 1024 / 1024;
+      return mb.toFixed(1) + ' MB';
+    }
+  };
+
+  tv.c.analysis.ObjectInstanceView.register(
+      TcmallocInstanceView,
+      {typeName: 'memory::Heap'});
+
+  return {
+    TcmallocInstanceView: TcmallocInstanceView
+  };
+
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_snapshot_view.css b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_snapshot_view.css
new file mode 100644
index 0000000..808d541
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_snapshot_view.css
@@ -0,0 +1,59 @@
+/* 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.
+ */
+
+.subhead {
+  font-size: small;
+  padding-bottom: 10px;
+}
+
+.tcmalloc-snapshot-view ul {
+  background-position: 0 5px;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  font-family: monospace;
+  list-style: none;
+  margin: 0;
+  padding-left: 15px;
+}
+
+.tcmalloc-snapshot-view li {
+  background-position: 0 5px;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  list-style: none;
+  margin: 0;
+  padding-left: 15px;
+}
+
+/* Collapsed state for list element */
+.tcmalloc-snapshot-view .collapsed {
+  background-image: url(./images/expand.png);
+}
+
+/* Expanded state for list element. Must be located under the collapsed one. */
+.tcmalloc-snapshot-view .expanded {
+  background-image: url(./images/collapse.png);
+}
+
+/* Allocation size in MB, right-aligned for easier comparison of columns. */
+.trace-bytes {
+  display: inline-block;
+  padding-right: 10px;
+  text-align: right;
+  width: 80px;
+}
+
+/* Trace allocation count. */
+.trace-allocs {
+  display: inline-block;
+  padding-right: 10px;
+  text-align: right;
+  width: 120px;
+}
+
+/* Trace name, inline so it appears to the right of the byte count. */
+.trace-name {
+  display: inline-block;
+}
diff --git a/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_snapshot_view.html b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_snapshot_view.html
new file mode 100644
index 0000000..fc16da3
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tcmalloc/tcmalloc_snapshot_view.html
@@ -0,0 +1,176 @@
+<!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="/extras/tcmalloc/tcmalloc_snapshot_view.css">
+<link rel="import" href="/core/analysis/object_snapshot_view.html">
+<link rel="import" href="/core/analysis/util.html">
+
+<script>
+'use strict';
+
+tv.exportTo('tv.e.tcmalloc', function() {
+  /*
+   * Displays a heap memory snapshot in a human readable form.
+   * @constructor
+   */
+  var TcmallocSnapshotView = tv.b.ui.define(
+      'heap-snapshot-view',
+      tv.c.analysis.ObjectSnapshotView);
+
+  TcmallocSnapshotView.prototype = {
+    __proto__: tv.c.analysis.ObjectSnapshotView.prototype,
+
+    decorate: function() {
+      this.classList.add('tcmalloc-snapshot-view');
+    },
+
+    updateContents: function() {
+      var snapshot = this.objectSnapshot_;
+      if (!snapshot || !snapshot.heap_) {
+        this.textContent = 'No heap found.';
+        return;
+      }
+      // Clear old snapshot view.
+      this.textContent = '';
+
+      // Note: "total" may actually be less than the largest allocation bin.
+      // This might happen if one stack is doing a lot of allocation, then
+      // passing off to another stack for deallocation.  That stack will
+      // have a high "current bytes" count and the other one might be
+      // negative or zero. So "total" may be smaller than the largest trace.
+      var subhead = document.createElement('div');
+      subhead.textContent = 'Retaining ' +
+          this.getByteString_(snapshot.total_.currentBytes) + ' in ' +
+          snapshot.total_.currentAllocs +
+          ' allocations. Showing > 0.1 MB.';
+      subhead.className = 'subhead';
+      this.appendChild(subhead);
+
+      // Build a nested tree-view of allocations
+      var myList = this.buildAllocList_(snapshot.heap_, false);
+      this.appendChild(myList);
+    },
+
+    /**
+     * Creates a nested list with clickable entries.
+     * @param {Object} heapEntry The current trace heap entry.
+     * @param {boolean} hide Whether this list is hidden by default.
+     * @return {Element} A <ul> list element.
+     */
+    buildAllocList_: function(heapEntry, hide) {
+      var myList = document.createElement('ul');
+      myList.hidden = hide;
+      var keys = Object.keys(heapEntry.children);
+      keys.sort(function(a, b) {
+        // Sort from large to small.
+        return heapEntry.children[b].currentBytes -
+            heapEntry.children[a].currentBytes;
+      });
+      for (var i = 0; i < keys.length; i++) {
+        var traceName = keys[i];
+        var trace = heapEntry.children[traceName];
+        // Don't show small nodes - they make things harder to see.
+        if (trace.currentBytes < 100 * 1024)
+          continue;
+        var childCount = Object.keys(trace.children).length;
+        var isLeaf = childCount == 0;
+        var myItem = this.buildItem_(
+            traceName, isLeaf, trace.currentBytes, trace.currentAllocs);
+        myList.appendChild(myItem);
+        // Build a nested <ul> list of my children.
+        if (childCount > 0)
+          myItem.appendChild(this.buildAllocList_(trace, true));
+      }
+      return myList;
+    },
+
+    /*
+     * Returns a <li> for an allocation traceName of size bytes.
+     */
+    buildItem_: function(traceName, isLeaf, bytes, allocs) {
+      var myItem = document.createElement('li');
+      myItem.className = 'trace-item';
+      myItem.id = traceName;
+
+      var byteDiv = document.createElement('div');
+      byteDiv.textContent = this.getByteString_(bytes);
+      byteDiv.className = 'trace-bytes';
+      myItem.appendChild(byteDiv);
+
+      if (traceName.length == 0) {
+        // The empty trace name indicates that the allocations occurred at
+        // this trace level, not in a sub-trace. This looks weird as the
+        // empty string, so replace it with something non-empty and don't
+        // give that line an expander.
+        traceName = '(here)';
+      } else if (traceName.indexOf('..') == 0) {
+        // Tasks in RunTask have special handling. They show the path of the
+        // filename. Convert '../../foo.cc' into 'RunTask from foo.cc'.
+        var lastSlash = traceName.lastIndexOf('/');
+        if (lastSlash != -1)
+          traceName = 'Task from ' + traceName.substr(lastSlash + 1);
+      }
+      var traceDiv = document.createElement('div');
+      traceDiv.textContent = traceName;
+      traceDiv.className = 'trace-name';
+      myItem.appendChild(traceDiv);
+
+      // Don't allow leaf nodes to be expanded.
+      if (isLeaf)
+        return myItem;
+
+      // Expand the element when it is clicked.
+      var self = this;
+      myItem.addEventListener('click', function(event) {
+        // Allow click on the +/- image (li) or child divs.
+        if (this == event.target || this == event.target.parentElement) {
+          this.classList.toggle('expanded');
+          var child = this.querySelector('ul');
+          child.hidden = !child.hidden;
+          // Highlight this stack trace in the timeline view.
+          self.onItemClicked_(this);
+        }
+      });
+      myItem.classList.add('collapsed');
+      return myItem;
+    },
+
+    onItemClicked_: function(traceItem) {
+      // Compute the full stack trace the user just clicked.
+      var traces = [];
+      while (traceItem.classList.contains('trace-item')) {
+        var traceNameDiv = traceItem.firstElementChild.nextElementSibling;
+        traces.unshift(traceNameDiv.textContent);
+        var traceNameUl = traceItem.parentElement;
+        traceItem = traceNameUl.parentElement;
+      }
+      // Tell the instance that this stack trace is selected.
+      var instance = this.objectSnapshot_.objectInstance;
+      instance.selectedTraces = traces;
+      // Invalid the viewport to cause a redraw.
+      var trackView = document.querySelector('.timeline-track-view');
+      trackView.viewport_.dispatchChangeEvent();
+    },
+
+    /*
+     * Returns a human readable string for a size in bytes.
+     */
+    getByteString_: function(bytes) {
+      var mb = bytes / 1024 / 1024;
+      return mb.toFixed(1) + ' MB';
+    }
+  };
+
+  tv.c.analysis.ObjectSnapshotView.register(
+      TcmallocSnapshotView,
+      {typeName: 'memory::Heap'});
+
+  return {
+    TcmallocSnapshotView: TcmallocSnapshotView
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/extras/tquery/context.html b/trace-viewer/trace_viewer/extras/tquery/context.html
new file mode 100644
index 0000000..8d31b9c
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tquery/context.html
@@ -0,0 +1,35 @@
+<!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-e-tquery-context'>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.event = undefined;
+      this.ancestors = [];
+    },
+
+    push: function(event) {
+      var ctx = document.createElement(this.element.name);
+      ctx.ancestors = this.ancestors.slice();
+      ctx.ancestors.push(event);
+      return ctx;
+    },
+
+    pop: function(event) {
+      var ctx = document.createElement(this.element.name);
+      ctx.event = this.ancestors[this.ancestors.length - 1];
+      ctx.ancestors = this.ancestors.slice(0, this.ancestors.length - 1);
+      return ctx;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/tquery/filter.html b/trace-viewer/trace_viewer/extras/tquery/filter.html
new file mode 100644
index 0000000..982a700
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tquery/filter.html
@@ -0,0 +1,44 @@
+<!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_object.html">
+
+<polymer-element name='tv-e-tquery-filter' extends='tv-c-scripting-object'>
+  <script>
+  'use strict';
+
+  Polymer({
+    get scriptName() {
+    },
+
+    evaluate: function(context) {
+      throw new Error('Not implemented');
+    },
+
+    matchValue_: function(value, expected) {
+      if (expected instanceof RegExp)
+        return expected.test(value);
+      else if (expected instanceof Function)
+        return expected(value);
+      return value === expected;
+    },
+
+    normalizeFilterExpression: function(filterExpression) {
+      // Shortcut: naked strings and regexps can be used to match against slice
+      // titles.
+      if (filterExpression instanceof String ||
+          typeof(filterExpression) == 'string' ||
+          filterExpression instanceof RegExp) {
+        var filter = document.createElement('tv-e-tquery-filter-has-title');
+        filter.expected = filterExpression;
+        return filter;
+      }
+      return filterExpression;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/tquery/filter_has_ancestor.html b/trace-viewer/trace_viewer/extras/tquery/filter_has_ancestor.html
new file mode 100644
index 0000000..3abc7fb
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tquery/filter_has_ancestor.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="/extras/tquery/filter.html">
+
+<polymer-element name='tv-e-tquery-filter-has-ancestor'
+    extends='tv-e-tquery-filter'>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.subExpression_ = undefined;
+    },
+
+    get scriptName() {
+      return 'hasAncestor';
+    },
+
+    get scriptValue() {
+      return function(subExpression) {
+        var filter = document.createElement(this.element.name);
+        filter.subExpression = subExpression;
+        return filter;
+      }.bind(this);
+    },
+
+    set subExpression(expr) {
+      this.subExpression_ = this.normalizeFilterExpression(expr);
+    },
+
+    get subExpression() {
+      return this.subExpression_;
+    },
+
+    evaluate: function(context) {
+      if (!this.subExpression)
+        return context.ancestors.length > 0;
+      while (context.ancestors.length) {
+        context = context.pop();
+        if (this.subExpression.evaluate(context))
+          return true;
+      }
+      return false;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/tquery/filter_has_title.html b/trace-viewer/trace_viewer/extras/tquery/filter_has_title.html
new file mode 100644
index 0000000..57a1264
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tquery/filter_has_title.html
@@ -0,0 +1,37 @@
+<!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="/extras/tquery/filter.html">
+
+<polymer-element name='tv-e-tquery-filter-has-title'
+    extends='tv-e-tquery-filter'>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.expected = undefined;
+    },
+
+    get scriptName() {
+      return 'hasTitle';
+    },
+
+    get scriptValue() {
+      return function(expected) {
+        var filter = document.createElement(this.element.name);
+        filter.expected = expected;
+        return filter;
+      }.bind(this);
+    },
+
+    evaluate: function(context) {
+      return this.matchValue_(context.event.title, this.expected);
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/tquery/tquery.html b/trace-viewer/trace_viewer/extras/tquery/tquery.html
new file mode 100644
index 0000000..6a678be
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tquery/tquery.html
@@ -0,0 +1,166 @@
+<!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/task.html">
+<link rel="import" href="/core/filter.html">
+<link rel="import" href="/core/selection.html">
+<link rel="import" href="/core/scripting_object.html">
+<link rel="import" href="/extras/tquery/context.html">
+<link rel="import" href="/extras/tquery/filter_has_ancestor.html">
+<link rel="import" href="/extras/tquery/filter_has_title.html">
+
+<polymer-element name='tv-e-tquery' extends='tv-c-scripting-object'>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.timeline_ = undefined;
+      this.parent_ = undefined;
+      this.filterExpression_ = undefined;
+      // Memoized filtering result.
+      this.selection_ = undefined;
+    },
+
+    get scriptName() {
+      return '$t';
+    },
+
+    onTimelineChanged: function(t) {
+      this.timeline_ = t;
+      this.selection_ = undefined;
+    },
+
+    get timeline() {
+      return this.timeline_;
+    },
+
+    // Append a new filter expression to this query and return a query node
+    // that represents the result.
+    filter: function(filterExpression) {
+      var result = document.createElement('tv-e-tquery');
+      result.timeline_ = this.timeline;
+      result.parent_ = this;
+      // TODO(skyostil): Can we use a static helper function for this?
+      result.filterExpression_ =
+          document.createElement('tv-e-tquery-filter').
+              normalizeFilterExpression(filterExpression);
+      return result;
+    },
+
+    // Creates a graph of {Task} objects which will compute the selections for
+    // this filter object and all of its parents. The return value is an object
+    // with the following fields:
+    //  - rootTask: {Task} which should be executed to kick off processing for
+    //              the entire task graph.
+    //  - lastTask: The final {Task} of the graph. Can be used by the caller to
+    //              enqueue additional processing at the end.
+    //  - lastNode: The last filter object in the task. It's selection property
+    //              will contain the filtering result once |finalTask|
+    //              completes.
+    createFilterTaskGraph_: function() {
+      // List of nodes in order from the current one to the root.
+      var nodes = [];
+      var node = this;
+      while (node !== undefined) {
+        nodes.push(node);
+        node = node.parent_;
+      }
+
+      var rootTask = new tv.b.Task();
+      var lastTask = rootTask;
+      for (var i = nodes.length - 1; i >= 0; i--) {
+        var node = nodes[i];
+        // Reuse any memoized result.
+        if (node.selection_ !== undefined)
+          continue;
+        node.selection_ = new tv.c.Selection();
+        if (node.parent_ === undefined) {
+          // If this is the root, start by collecting all objects from the
+          // timeline.
+          lastTask = lastTask.after(
+              this.selectEverythingAsTask_(node.selection_));
+        } else {
+          // Otherwise execute the filter expression for this node and fill
+          // in its selection.
+          var prevNode = nodes[i + 1];
+          lastTask = this.createFilterTaskForNode_(lastTask, node, prevNode);
+        }
+      }
+      return {rootTask: rootTask, lastTask: lastTask, lastNode: node};
+    },
+
+    createFilterTaskForNode_: function(lastTask, node, prevNode) {
+      return lastTask.after(function() {
+        // TODO(skyostil): Break into subtasks.
+        node.evaluateFilterExpression_(
+            prevNode.selection_, node.selection_);
+      }, this);
+    },
+
+    // Applies the result of a filter expression for a given event and all
+    // of its subslices and adds the matching events to an output selection.
+    evaluateFilterExpression_: function(inputSelection, outputSelection) {
+      var seenEvents = {};
+      inputSelection.forEach(function(event) {
+        var context = document.createElement('tv-e-tquery-context');
+        context.event = event;
+        this.evaluateFilterExpressionForEvent_(
+            context, inputSelection, outputSelection, seenEvents);
+      }.bind(this));
+    },
+
+    evaluateFilterExpressionForEvent_: function(
+        context, inputSelection, outputSelection, seenEvents) {
+      var event = context.event;
+      if (inputSelection.contains(event) && !seenEvents[event.guid]) {
+        seenEvents[event.guid] = true;
+        if (!this.filterExpression_ ||
+            this.filterExpression_.evaluate(context))
+          outputSelection.push(event);
+      }
+      if (!event.subSlices)
+        return;
+      context = context.push(event);
+      for (var i = 0; i < event.subSlices.length; i++) {
+        context.event = event.subSlices[i];
+        this.evaluateFilterExpressionForEvent_(
+            context, inputSelection, outputSelection, seenEvents);
+      }
+    },
+
+    // Show the result of this query as a highlight on the timeline. Returns a
+    // {Task} which runs the query and sets the highlight.
+    show: function() {
+      var graph = this.createFilterTaskGraph_();
+
+      graph.lastTask = graph.lastTask.after(function() {
+        this.timeline.setHighlightAndClearSelection(graph.lastNode.selection_);
+      }, this);
+      return graph.rootTask;
+    },
+
+    // Returns a task that fills the given selection with everything in the
+    // timeline.
+    selectEverythingAsTask_: function(selection) {
+      var passThroughFilter = new tv.c.Filter();
+      var filterTask =
+        this.timeline.addAllObjectsMatchingFilterToSelectionAsTask(
+            passThroughFilter, selection);
+      return filterTask;
+    },
+
+    get selection() {
+      if (this.selection_ === undefined) {
+        var graph = this.createFilterTaskGraph_();
+        tv.b.Task.RunSynchronously(graph.rootTask);
+      }
+      return this.selection_;
+    }
+  });
+  </script>
+</polymer-element>
diff --git a/trace-viewer/trace_viewer/extras/tquery/tquery_test.html b/trace-viewer/trace_viewer/extras/tquery/tquery_test.html
new file mode 100644
index 0000000..76a36c3
--- /dev/null
+++ b/trace-viewer/trace_viewer/extras/tquery/tquery_test.html
@@ -0,0 +1,143 @@
+<!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/task.html">
+<link rel="import" href="/extras/tquery/tquery.html">
+
+<polymer-element name='tv-e-tquery-fake-timeline'>
+  <script>
+  'use strict';
+
+  Polymer({
+    ready: function() {
+      this.allObjects = [];
+    },
+
+    addAllObjectsMatchingFilterToSelectionAsTask: function(filter, selection) {
+      return new tv.b.Task(function() {
+        var n = this.allObjects.length;
+        for (var i = 0; i < n; i++) {
+          this.addSubtreeToSelection_(selection, this.allObjects[i]);
+        }
+      }, this);
+    },
+
+    addSubtreeToSelection_: function(selection, event) {
+      selection.push(event);
+      if (event.subSlices) {
+        for (var i = 0; i < event.subSlices.length; i++) {
+          this.addSubtreeToSelection_(selection, event.subSlices[i]);
+        }
+      }
+    },
+
+    setHighlightAndClearSelection: function(highlight) {
+      this.highlight = highlight;
+    }
+  });
+  </script>
+</polymer-element>
+
+<script>
+'use strict';
+
+tv.b.unittest.testSuite(function() {
+  function createFakeTimeline_(sliceCount) {
+    var timeline = document.createElement('tv-e-tquery-fake-timeline');
+    timeline.allObjects = [];
+    for (var i = 0; i < sliceCount; i++) {
+      timeline.allObjects.push({guid: i});
+    }
+    return timeline;
+  }
+
+  function createFakeTimelineWithEvents_(events) {
+    var timeline = document.createElement('tv-e-tquery-fake-timeline');
+    timeline.allObjects = events;
+    return timeline;
+  }
+
+  test('tqueryAsyncSelection', function() {
+    var tquery = document.createElement('tv-e-tquery');
+    var timeline = createFakeTimeline_(3);
+    tquery.onTimelineChanged(timeline);
+
+    var result = tquery.show();
+    tv.b.Task.RunSynchronously(result);
+    assert.equal(timeline.highlight.length, 3);
+  });
+
+  test('tquerySyncSelection', function() {
+    var tquery = document.createElement('tv-e-tquery');
+    var timeline = createFakeTimeline_(3);
+    tquery.onTimelineChanged(timeline);
+
+    assert.equal(tquery.selection.length, 3);
+
+    // Selection should get reset when the timeline changes.
+    tquery.onTimelineChanged(createFakeTimeline_(5));
+    assert.equal(tquery.selection.length, 5);
+  });
+
+  test('tqueryPassThroughFiltering', function() {
+    var tquery = document.createElement('tv-e-tquery');
+    var timeline = createFakeTimeline_(3);
+    tquery.onTimelineChanged(timeline);
+
+    var result = tquery.filter().filter().show();
+    tv.b.Task.RunSynchronously(result);
+    assert.equal(timeline.highlight.length, 3);
+  });
+
+  test('tqueryFilterHasTitle', function() {
+    var tquery = document.createElement('tv-e-tquery');
+    var hasTitle = document.createElement(
+        'tv-e-tquery-filter-has-title').scriptValue;
+    var timeline = createFakeTimelineWithEvents_([
+        {guid: 1, title: 'a'},
+        {guid: 2, title: 'b'},
+        {guid: 3, title: 'c'}
+    ]);
+    tquery.onTimelineChanged(timeline);
+
+    var result = tquery.filter(hasTitle('a')).selection;
+    assert.equal(result.length, 1);
+    assert.equal(result[0].guid, 1);
+
+    var result = tquery.filter('b').selection;
+    assert.equal(result.length, 1);
+    assert.equal(result[0].guid, 2);
+
+    var result = tquery.filter(/^c$/).selection;
+    assert.equal(result.length, 1);
+    assert.equal(result[0].guid, 3);
+  });
+
+  test('tqueryFilterHasAncestor', function() {
+    var tquery = document.createElement('tv-e-tquery');
+    var hasAncestor = document.createElement(
+        'tv-e-tquery-filter-has-ancestor').scriptValue;
+    var timeline = createFakeTimelineWithEvents_([
+        {guid: 1, title: 'a'},
+        {guid: 2, title: 'b', subSlices: [{guid: 4}]},
+        {guid: 3, title: 'c'}
+    ]);
+    tquery.onTimelineChanged(timeline);
+
+    var result = tquery.filter(hasAncestor('b')).selection;
+    assert.equal(result.length, 1);
+    assert.equal(result[0].guid, 4);
+
+    var result = tquery.filter(hasAncestor()).selection;
+    assert.equal(result.length, 1);
+    assert.equal(result[0].guid, 4);
+
+    var result = tquery.filter(hasAncestor('a')).selection;
+    assert.equal(result.length, 0);
+  });
+});
+</script>
diff --git a/trace-viewer/trace_viewer/trace_viewer.html b/trace-viewer/trace_viewer/trace_viewer.html
new file mode 100644
index 0000000..b04de00
--- /dev/null
+++ b/trace-viewer/trace_viewer/trace_viewer.html
@@ -0,0 +1,88 @@
+<!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="stylesheet" href="/base/ui/common.css">
+<link rel="import" href="/base/ui.html">
+<link rel="import" href="/core/timeline_view.html">
+<link rel="import" href="/core/trace_model/trace_model.html">
+<script>
+'use strict';
+tv.exportTo('tv', function() {
+  /**
+   * Creates a trace viewer and, if given, loads the provided URL.
+   *
+   * For more advanced usage, consider using TimelineView and TraceModel
+   * directly.
+   *
+   * Returns an HTMLElement that is the trace-viwer.
+   */
+  var TraceViewer = tv.b.ui.define('trace-viewer', tv.c.TimelineView);
+
+  TraceViewer.prototype = {
+    __proto__: tv.c.TimelineView.prototype,
+
+    decorate: function(opt_url) {
+      tv.c.TimelineView.prototype.decorate.call(this);
+
+      if (opt_url === undefined)
+        return;
+      var url = opt_url;
+      var that = this;
+
+      var req = new XMLHttpRequest();
+        var is_binary = /[.]gz$/.test(url) || /[.]zip$/.test(url);
+        req.overrideMimeType('text/plain; charset=x-user-defined');
+        req.open('GET', url, true);
+        if (is_binary)
+          req.responseType = 'arraybuffer';
+        req.onreadystatechange = function(aEvt) {
+          if (req.readyState == 4) {
+            window.setTimeout(function() {
+              if (req.status == 200) {
+                onResult(is_binary ? req.response : req.responseText);
+              } else {
+                onResultFail(req.status);
+              }
+            }, 0);
+          }
+        };
+      req.send(null);
+
+      function onResultFail(err) {
+        var overlay = new tv.b.ui.Overlay();
+        overlay.textContent = err + ': ' + url + ' could not be loaded';
+        overlay.title = 'Failed to fetch data';
+        overlay.visible = true;
+      }
+
+      var model;
+      function onResult(result) {
+        model = new tv.c.TraceModel();
+        var p = model.importTracesWithProgressDialog([result], true);
+        p.then(onModelLoaded, onImportFail);
+      }
+
+      function onModelLoaded() {
+        that.model = model;
+        that.viewTitle = url;
+        if (that.timeline)
+          that.timeline.focusElement = that;
+      }
+
+      function onImportFail() {
+        var overlay = new tv.b.ui.Overlay();
+        overlay.textContent = tv.b.normalizeException(err).message;
+        overlay.title = 'Import error';
+        overlay.visible = true;
+      }
+    }
+  };
+
+  return {
+    TraceViewer: TraceViewer
+  };
+});
+</script>
diff --git a/trace-viewer/trace_viewer/trace_viewer_project.py b/trace-viewer/trace_viewer/trace_viewer_project.py
new file mode 100644
index 0000000..99fb8fd
--- /dev/null
+++ b/trace-viewer/trace_viewer/trace_viewer_project.py
@@ -0,0 +1,134 @@
+# 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.
+import sys
+import os
+import re
+
+
+from tvcm import project as project_module
+
+
+def _FindAllFilesRecursive(source_paths):
+  all_filenames = set()
+  for source_path in source_paths:
+    for dirpath, dirnames, filenames in os.walk(source_path):
+      for f in filenames:
+        if f.startswith('.'):
+          continue
+        x = os.path.abspath(os.path.join(dirpath, f))
+        all_filenames.add(x)
+  return all_filenames
+
+def _IsFilenameATest(loader, x):
+  if x.endswith('_test.js'):
+    return True
+
+  if x.endswith('_test.html'):
+    return True
+
+  if x.endswith('_unittest.js'):
+    return True
+
+  if x.endswith('_unittest.html'):
+    return True
+
+  # TODO(nduca): Add content test?
+  return False
+
+class TraceViewerProject(project_module.Project):
+  trace_viewer_path = os.path.abspath(os.path.join(
+      os.path.dirname(__file__), '..'))
+
+  src_path = os.path.abspath(os.path.join(
+      trace_viewer_path, 'trace_viewer'))
+
+  extras_path = os.path.join(src_path, 'extras')
+
+  trace_viewer_third_party_path = os.path.abspath(os.path.join(
+      trace_viewer_path, 'third_party'))
+
+  jszip_path = os.path.abspath(os.path.join(
+      trace_viewer_third_party_path, 'jszip'))
+
+  glmatrix_path = os.path.abspath(os.path.join(
+      trace_viewer_third_party_path, 'gl-matrix', 'dist'))
+
+  d3_path = os.path.abspath(os.path.join(
+      trace_viewer_third_party_path, 'd3'))
+
+  chai_path = os.path.abspath(os.path.join(
+      trace_viewer_third_party_path, 'chai'))
+
+  mocha_path = os.path.abspath(os.path.join(
+      trace_viewer_third_party_path, 'mocha'))
+
+  test_data_path = os.path.join(trace_viewer_path, 'test_data')
+  skp_data_path = os.path.join(trace_viewer_path, 'skp_data')
+
+  def __init__(self, *args, **kwargs):
+    super(TraceViewerProject, self).__init__(*args, **kwargs)
+
+    self.source_paths.append(self.src_path)
+    self.source_paths.append(self.trace_viewer_third_party_path)
+    self.source_paths.append(self.jszip_path)
+    self.source_paths.append(self.glmatrix_path)
+    self.source_paths.append(self.d3_path)
+    self.source_paths.append(self.chai_path)
+    self.source_paths.append(self.mocha_path)
+
+    self.non_module_html_files.extendRel(self.trace_viewer_path, [
+      'bin/index.html',
+      'test_data/android_systrace.html',
+    ])
+    for config_name in self.GetConfigNames():
+      self.non_module_html_files.appendRel(self.trace_viewer_path,
+        'bin/trace_viewer_%s.html' % config_name)
+
+    # Igore the old viewer if it still exists.
+    self.non_module_html_files.appendRel(self.trace_viewer_path,
+      'bin/trace_viewer.html')
+
+    self.non_module_html_files.extendRel(self.trace_viewer_third_party_path, [
+      'gl-matrix/jsdoc-template/static/header.html',
+      'gl-matrix/jsdoc-template/static/index.html',
+    ])
+
+  def FindAllTestModuleResources(self):
+    all_filenames = _FindAllFilesRecursive([self.src_path])
+    test_module_filenames = [x for x in all_filenames if
+                             _IsFilenameATest(self.loader, x)]
+    test_module_filenames.sort()
+
+    # Find the equivalent resources.
+    return [self.loader.FindResourceGivenAbsolutePath(x)
+            for x in test_module_filenames]
+
+  def GetConfigNames(self):
+    config_files = [
+        os.path.join(self.extras_path, x) for x in os.listdir(self.extras_path)
+        if x.endswith('_config.html')
+    ]
+
+    config_files = [x for x in config_files if os.path.isfile(x)]
+
+    config_basenames = [os.path.basename(x) for x in config_files]
+    config_names = [re.match('(.+)_config.html$', x).group(1)
+                    for x in config_basenames]
+    return config_names
+
+  def GetDefaultConfigName(self):
+    assert 'full' in self.GetConfigNames()
+    return 'full'
+
+  def AddConfigNameOptionToParser(self, parser):
+    choices = self.GetConfigNames()
+    parser.add_option(
+        '--config', dest='config_name',
+        type='choice', choices=choices,
+        default=self.GetDefaultConfigName(),
+        help='Picks a browser config. Valid choices: %s' % ', '.join(choices))
+    return choices
+
+  def GetModuleNameForConfigName(self, config_name):
+    return 'extras.%s_config' % config_name