systrace: embed js & css when generating html
This change makes the systrace.py script embed the CSS and Javascript in the
HTML file that is generated.
Change-Id: I1ac7456f9a9d35a8c55a564432b01ea6966f8092
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..25e8ab1
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,91 @@
+# Names should be added to this file like so:
+# Name or Organization <email address>
+
+Google Inc.
+Seo Sanghyeon <sanxiyn@gmail.com>
+Alex Scheele <alexscheele@gmail.com>
+Andrew Brampton <me@bramp.net>
+Paweł Hajdan jr <phajdan.jr@gmail.com>
+Jesse Miller <jesse@jmiller.biz>
+Szymon Piechowicz <szymonpiechowicz@o2.pl>
+James Vega <vega.james@gmail.com>
+Marco Rodrigues <gothicx@gmail.com>
+Matthias Reitinger <reimarvin@gmail.com>
+Peter Bright <drpizza@quiscalusmexicanus.org>
+Arthur Lussos <developer0420@gmail.com>
+Masahiro Yado <yado.masa@gmail.com>
+Yarin Kaul <yarin.kaul@gmail.com>
+Gaetano Mendola <mendola@gmail.com>
+Comodo CA Limited
+Torchmobile Inc.
+Craig Schlenter <craig.schlenter@gmail.com>
+Ibrar Ahmed <ibrar.ahmad@gmail.com>
+Naoki Takano <takano.naoki@gmail.com>
+Fabien Tassin <fta@sofaraway.org>
+Kunal Thakar <kunalt@gmail.com>
+Mohamed Mansour <m0.interactive@gmail.com>
+Joshua Roesslein <jroesslein@gmail.com>
+Yong Shin <sy3620@gmail.com>
+Laszlo Radanyi <bekkra@gmail.com>
+Raman Tenneti <raman.tenneti@gmail.com>
+Kyle Nahrgang <kpn24@drexel.edu>
+Kim Christensen <kimworking@gmail.com>
+Paul Robinson <paulrobinson85@googlemail.com>
+Josué Ratelle <jorat1346@gmail.com>
+Edward Crossman <tedoc2000@gmail.com>
+Nikita Ofitserov <himikof@gmail.com>
+Sean Bryant <sean@cyberwang.net>
+Robert Sesek <rsesek@bluestatic.org>
+Janwar Dinata <j.dinata@gmail.com>
+Will Hirsch <chromium@willhirsch.co.uk>
+Yoav Zilberberg <yoav.zilberberg@gmail.com>
+Joel Stanley <joel@jms.id.au>
+Jacob Mandelson <jacob@mandelson.org>
+Yuri Gorobets <yuri.gorobets@gmail.com>
+Paul Wicks <pwicks86@gmail.com>
+Thiago Farina <thiago.farina@gmail.com>
+Viet-Trung Luu <viettrungluu@gmail.com>
+Pierre-Antoine LaFayette <pierre.lafayette@gmail.com>
+Song YeWen <ffmpeg@gmail.com>
+Philippe Beauchamp <philippe.beauchamp@gmail.com>
+Vedran Šajatović <vedran.sajatovic@gmail.com>
+Randy Posynick <randy.posynick@gmail.com>
+Bruno Calvignac <brunocalvignac@gmail.com>
+Jaime Soriano Pastor <jsorianopastor@gmail.com>
+Bryan Donlan <bdonlan@gmail.com>
+Ramkumar Ramachandra <artagnon@gmail.com>
+Dominic Jodoin <dominic.jodoin@gmail.com>
+Kaspar Brand <googlecontrib@velox.ch>
+Clemens Fruhwirth <clemens@endorphin.org>
+Kevin Lee Helpingstine <sig11@reprehensible.net>
+Bernard Cafarelli <voyageur@gentoo.org>
+Vernon Tang <vt@foilhead.net>
+Alexander Sulfrian <alexander@sulfrian.net>
+Philippe Beaudoin <philippe.beaudoin@gmail.com>
+Mark Hahnenberg <mhahnenb@gmail.com>
+Alex Gartrell <alexgartrell@gmail.com>
+Leith Bade <leith@leithalweapon.geek.nz>
+James Choi <jchoi42@pha.jhu.edu>
+Paul Kehrer <paul.l.kehrer@gmail.com>
+Chamal De Silva <chamal.desilva@gmail.com>
+Jay Soffian <jaysoffian@gmail.com>
+Brian G. Merrell <bgmerrell@gmail.com>
+Matthew Willis <appamatto@gmail.com>
+Novell Inc.
+Ryan Sleevi <ryan.sleevi@gmail.com>
+Satoshi Matsuzaki <satoshi.matsuzaki@gmail.com>
+Benjamin Jemlich <pcgod99@gmail.com>
+Ningxin Hu <ningxin.hu@intel.com>
+Jared Wein <weinjared@gmail.com>
+Mingmin Xie <melvinxie@gmail.com>
+Michael Gilbert <floppymaster@gmail.com>
+Giuseppe Iuculano <giuseppe@iuculano.it>
+litl LLC
+James Willcox <jwillcox@litl.com>
+Shreyas VA <v.a.shreyas@gmail.com>
+Steven Pennington <spenn@engr.uvic.ca>
+Jorge Villatoro <jorge@tomatocannon.com>
+Paul Nettleship <pnettleship@gmail.com>
+David Benjamin <davidben@mit.edu>
+Sevan Janiyan <venture37@geeklan.co.uk>
+Peter Beverloo <peter@lvp-media.com>
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8dc3504
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MODULE_LICENSE_BSD_LIKE b/MODULE_LICENSE_BSD_LIKE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_BSD_LIKE
diff --git a/README b/README
index e1e3c56..cedfff2 100644
--- a/README
+++ b/README
@@ -1,11 +1,10 @@
-The systrace.py tool and the Javascript / HTML that goes along with it are
-being checked in under the vendor/google repository for now. This is because
-the trace HTML files generated by systrace.py contain references to files that
-can only be accessed on the corp network. Ultimately these files will be
-served publicly at which point this tool should move to the development
-repository. However, to enable use of the tool sooner I'm putting it here for
-now.
+The things in the www subdirectory were forked from the Chrome tracing tool.
+This should end up getting merged upstream at some point into either the
+Chromium code base or else a separate shared project.
-Also, the things in the www subdirectory were forked from the Chrome tracing
-tool. This may end up getting merged upstream into either the Chromium code
-base or else a separate shared project.
+The update.sh script should be used to package the CSS and Javascript files in
+the www directory into the style.css and script.js files. These files are
+being checked in, and get embedded in each trace HTML file the systrace.py
+generates. Each time a file in the www directory gets updated, this script
+will need to be rerun, and the generated files must be committed along with the
+changes to the www directory.
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..993791c
--- /dev/null
+++ b/script.js
@@ -0,0 +1,4762 @@
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const cr = (function() {
+
+ /**
+ * Whether we are using a Mac or not.
+ * @type {boolean}
+ */
+ const isMac = /Mac/.test(navigator.platform);
+
+ /**
+ * Whether this is on the Windows platform or not.
+ * @type {boolean}
+ */
+ const isWindows = /Win/.test(navigator.platform);
+
+ /**
+ * Whether this is on chromeOS or not.
+ * @type {boolean}
+ */
+ const isChromeOS = /CrOS/.test(navigator.userAgent);
+
+ /**
+ * Whether this is on vanilla Linux (not chromeOS).
+ * @type {boolean}
+ */
+ const isLinux = /Linux/.test(navigator.userAgent);
+
+ /**
+ * Whether this uses GTK or not.
+ * @type {boolean}
+ */
+ const isGTK = /GTK/.test(chrome.toolkit);
+
+ /**
+ * Whether this uses the views toolkit or not.
+ * @type {boolean}
+ */
+ const isViews = /views/.test(chrome.toolkit);
+
+ /**
+ * Whether this window is optimized for touch-based input.
+ * @type {boolean}
+ */
+ const isTouchOptimized = !!chrome.touchOptimized;
+
+ /**
+ * Sets the os and toolkit attributes in the <html> element so that platform
+ * specific css rules can be applied.
+ */
+ function enablePlatformSpecificCSSRules() {
+ if (isMac)
+ doc.documentElement.setAttribute('os', 'mac');
+ if (isWindows)
+ doc.documentElement.setAttribute('os', 'windows');
+ if (isChromeOS)
+ doc.documentElement.setAttribute('os', 'chromeos');
+ if (isLinux)
+ doc.documentElement.setAttribute('os', 'linux');
+ if (isGTK)
+ doc.documentElement.setAttribute('toolkit', 'gtk');
+ if (isViews)
+ doc.documentElement.setAttribute('toolkit', 'views');
+ if (isTouchOptimized)
+ doc.documentElement.setAttribute('touch-optimized', '');
+ }
+
+ /**
+ * 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 window}.
+ * @private
+ */
+ function exportPath(name, opt_object, opt_objectToExportTo) {
+ var parts = name.split('.');
+ var cur = opt_objectToExportTo || window /* 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;
+ };
+
+ // cr.Event is called CrEvent in here to prevent naming conflicts. We also
+ // store the original Event in case someone does a global alias of cr.Event.
+ const DomEvent = Event;
+
+ /**
+ * Creates a new event to be used with cr.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 {DomEvent}
+ */
+ function CrEvent(type, opt_bubbles, opt_preventable) {
+ var e = cr.doc.createEvent('Event');
+ e.initEvent(type, !!opt_bubbles, !!opt_preventable);
+ e.__proto__ = CrEvent.prototype;
+ return e;
+ }
+
+ CrEvent.prototype = {
+ __proto__: DomEvent.prototype
+ };
+
+ /**
+ * 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) {
+ var e = new CrEvent(propertyName + 'Change');
+ e.propertyName = propertyName;
+ e.newValue = newValue;
+ e.oldValue = oldValue;
+ target.dispatchEvent(e);
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * The kind of property to define in {@code defineProperty}.
+ * @enum {number}
+ */
+ const 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
+ * @param {cr.PropertyKind} kind
+ * @return {function():*} The getter for the property.
+ */
+ function getGetter(name, kind) {
+ switch (kind) {
+ case PropertyKind.JS:
+ var privateName = 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 {cr.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.
+ * @return {function(*):void} The function to use as a setter.
+ */
+ function getSetter(name, kind, opt_setHook) {
+ switch (kind) {
+ case PropertyKind.JS:
+ var privateName = 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);
+ }
+ };
+
+ case PropertyKind.ATTR:
+ var attributeName = getAttributeName(name);
+ return function(value) {
+ var oldValue = this[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);
+ }
+ };
+
+ case PropertyKind.BOOL_ATTR:
+ var attributeName = getAttributeName(name);
+ return function(value) {
+ var oldValue = this[attributeName];
+ 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);
+ }
+ };
+ }
+ }
+
+ /**
+ * 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 {cr.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.
+ */
+ function defineProperty(obj, name, opt_kind, opt_setHook) {
+ 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));
+ }
+ }
+
+ /**
+ * Counter for use with createUid
+ */
+ var uidCounter = 1;
+
+ /**
+ * @return {number} A new unique ID.
+ */
+ function createUid() {
+ return uidCounter++;
+ }
+
+ /**
+ * Returns a unique ID for the item. This mutates the item so it needs to be
+ * an object
+ * @param {!Object} item The item to get the unique ID for.
+ * @return {number} The unique ID for the item.
+ */
+ function getUid(item) {
+ if (item.hasOwnProperty('uid'))
+ return item.uid;
+ return item.uid = createUid();
+ }
+
+ /**
+ * 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 cr.Event(type, opt_bubbles, opt_cancelable);
+ return target.dispatchEvent(e);
+ }
+
+ /**
+ * @param {string} name
+ * @param {!Function} fun
+ */
+ function define(name, fun) {
+ var obj = exportPath(name);
+ var exports = fun();
+ 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);
+ }
+ }
+
+ /**
+ * Document used for various document related operations.
+ * @type {!Document}
+ */
+ var doc = document;
+
+
+ /**
+ * Allows you to run func in the context of a different document.
+ * @param {!Document} document The document to use.
+ * @param {function():*} func The function to call.
+ */
+ function withDoc(document, func) {
+ var oldDoc = doc;
+ doc = document;
+ try {
+ func();
+ } finally {
+ doc = oldDoc;
+ }
+ }
+
+ /**
+ * 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());
+ };
+ }
+
+ return {
+ addSingletonGetter: addSingletonGetter,
+ isChromeOS: isChromeOS,
+ isMac: isMac,
+ isWindows: isWindows,
+ isLinux: isLinux,
+ isViews: isViews,
+ isTouchOptimized: isTouchOptimized,
+ enablePlatformSpecificCSSRules: enablePlatformSpecificCSSRules,
+ define: define,
+ defineProperty: defineProperty,
+ PropertyKind: PropertyKind,
+ createUid: createUid,
+ getUid: getUid,
+ dispatchSimpleEvent: dispatchSimpleEvent,
+ dispatchPropertyChange: dispatchPropertyChange,
+
+ /**
+ * The document that we are currently using.
+ * @type {!Document}
+ */
+ get doc() {
+ return doc;
+ },
+ withDoc: withDoc,
+ Event: CrEvent
+ };
+})();
+// Copyright (c) 2010 The Chromium 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 This contains an implementation of the EventTarget interface
+ * as defined by DOM Level 2 Events.
+ */
+
+cr.define('cr', 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.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;
+ });
+ event.preventDefault = function() {
+ this.returnValue = 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.returnValue;
+ }
+ };
+
+ // Export
+ return {
+ EventTarget: EventTarget
+ };
+});
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('cr.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 = cr.doc.querySelectorAll(source);
+ else
+ elements = [source];
+
+ for (var i = 0, el; el = elements[i]; i++) {
+ if (!(el instanceof constr))
+ constr.decorate(el);
+ }
+ }
+
+ /**
+ * Helper function for creating new element for define.
+ */
+ function createElementHelper(tagName, opt_bag) {
+ // Allow passing in ownerDocument to create in a different document.
+ var doc;
+ if (opt_bag && opt_bag.ownerDocument)
+ doc = opt_bag.ownerDocument;
+ else
+ doc = cr.doc;
+ return doc.createElement(tagName);
+ }
+
+ /**
+ * Creates the constructor for a UI element class.
+ *
+ * Usage:
+ * <pre>
+ * var List = cr.ui.define('list');
+ * List.prototype = {
+ * __proto__: HTMLUListElement.prototype,
+ * decorate: function() {
+ * ...
+ * },
+ * ...
+ * };
+ * </pre>
+ *
+ * @param {string|Function} tagNameOrFunction The tagName or
+ * function to use for newly created elements. If this is a function it
+ * needs to return a new element when called.
+ * @return {function(Object=):Element} The constructor function which takes
+ * an optional property bag. The function also has a static
+ * {@code decorate} method added to it.
+ */
+ function define(tagNameOrFunction) {
+ var createFunction, tagName;
+ if (typeof tagNameOrFunction == 'function') {
+ createFunction = tagNameOrFunction;
+ tagName = '';
+ } else {
+ createFunction = createElementHelper;
+ tagName = tagNameOrFunction;
+ }
+
+ /**
+ * Creates a new UI element constructor.
+ * @param {Object=} opt_propertyBag Optional bag of properties to set on the
+ * object after created. The property {@code ownerDocument} is special
+ * cased and it allows you to create the element in a different
+ * document than the default.
+ * @constructor
+ */
+ function f(opt_propertyBag) {
+ var el = createFunction(tagName, opt_propertyBag);
+ f.decorate(el);
+ for (var propertyName in opt_propertyBag) {
+ el[propertyName] = opt_propertyBag[propertyName];
+ }
+ 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();
+ };
+
+ return f;
+ }
+
+ /**
+ * Input elements do not grow and shrink with their content. This is a simple
+ * (and not very efficient) way of handling shrinking to content with support
+ * for min width and limited by the width of the parent element.
+ * @param {HTMLElement} el The element to limit the width for.
+ * @param {number} parentEl The parent element that should limit the size.
+ * @param {number} min The minimum width.
+ */
+ function limitInputWidth(el, parentEl, min) {
+ // Needs a size larger than borders
+ el.style.width = '10px';
+ var doc = el.ownerDocument;
+ var win = doc.defaultView;
+ var computedStyle = win.getComputedStyle(el);
+ var parentComputedStyle = win.getComputedStyle(parentEl);
+ var rtl = computedStyle.direction == 'rtl';
+
+ // To get the max width we get the width of the treeItem minus the position
+ // of the input.
+ var inputRect = el.getBoundingClientRect(); // box-sizing
+ var parentRect = parentEl.getBoundingClientRect();
+ var startPos = rtl ? parentRect.right - inputRect.right :
+ inputRect.left - parentRect.left;
+
+ // Add up border and padding of the input.
+ var inner = parseInt(computedStyle.borderLeftWidth, 10) +
+ parseInt(computedStyle.paddingLeft, 10) +
+ parseInt(computedStyle.paddingRight, 10) +
+ parseInt(computedStyle.borderRightWidth, 10);
+
+ // We also need to subtract the padding of parent to prevent it to overflow.
+ var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) :
+ parseInt(parentComputedStyle.paddingRight, 10);
+
+ var max = parentEl.clientWidth - startPos - inner - parentPadding;
+
+ function limit() {
+ if (el.scrollWidth > max) {
+ el.style.width = max + 'px';
+ } else {
+ el.style.width = 0;
+ var sw = el.scrollWidth;
+ if (sw < min) {
+ el.style.width = min + 'px';
+ } else {
+ el.style.width = sw + 'px';
+ }
+ }
+ }
+
+ el.addEventListener('input', limit);
+ limit();
+ }
+
+ return {
+ decorate: decorate,
+ define: define,
+ limitInputWidth: limitInputWidth
+ };
+});
+// Copyright (c) 2011 The Chromium 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 global object.
+ * @type {!Object}
+ */
+const global = this;
+
+/**
+ * Alias for document.getElementById.
+ * @param {string} id The ID of the element to find.
+ * @return {HTMLElement} The found element or null if not found.
+ */
+function $(id) {
+ return document.getElementById(id);
+}
+
+/**
+ * Calls chrome.send with a callback and restores the original afterwards.
+ * @param {string} name The name of the message to send.
+ * @param {!Array} params The parameters to send.
+ * @param {string} callbackName The name of the function that the backend calls.
+ * @param {!Function} The function to call.
+ */
+function chromeSend(name, params, callbackName, callback) {
+ var old = global[callbackName];
+ global[callbackName] = function() {
+ // restore
+ global[callbackName] = old;
+
+ var args = Array.prototype.slice.call(arguments);
+ return callback.apply(global, args);
+ };
+ chrome.send(name, params);
+}
+
+/**
+ * Generates a CSS url string.
+ * @param {string} s The URL to generate the CSS url for.
+ * @return {string} The CSS url string.
+ */
+function url(s) {
+ // http://www.w3.org/TR/css3-values/#uris
+ // Parentheses, commas, whitespace characters, single quotes (') and double
+ // quotes (") appearing in a URI must be escaped with a backslash
+ var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1');
+ // WebKit has a bug when it comes to URLs that end with \
+ // https://bugs.webkit.org/show_bug.cgi?id=28885
+ if (/\\\\$/.test(s2)) {
+ // Add a space to work around the WebKit bug.
+ s2 += ' ';
+ }
+ return 'url("' + s2 + '")';
+}
+
+/**
+ * Parses query parameters from Location.
+ * @param {string} s The URL to generate the CSS url for.
+ * @return {object} Dictionary containing name value pairs for URL
+ */
+function parseQueryParams(location) {
+ var params = {};
+ var query = unescape(location.search.substring(1));
+ var vars = query.split("&");
+ for (var i=0; i < vars.length; i++) {
+ var pair = vars[i].split("=");
+ params[pair[0]] = pair[1];
+ }
+ return params;
+}
+
+function findAncestorByClass(el, className) {
+ return findAncestor(el, function(el) {
+ if (el.classList)
+ return el.classList.contains(className);
+ return null;
+ });
+}
+
+/**
+ * Return the first ancestor for which the {@code predicate} returns true.
+ * @param {Node} node The node to check.
+ * @param {function(Node) : boolean} predicate The function that tests the
+ * nodes.
+ * @return {Node} The found ancestor or null if not found.
+ */
+function findAncestor(node, predicate) {
+ var last = false;
+ while (node != null && !(last = predicate(node))) {
+ node = node.parentNode;
+ }
+ return last ? node : null;
+}
+
+function swapDomNodes(a, b) {
+ var afterA = a.nextSibling;
+ if (afterA == b) {
+ swapDomNodes(b, a);
+ return;
+ }
+ var aParent = a.parentNode;
+ b.parentNode.replaceChild(a, b);
+ aParent.insertBefore(b, afterA);
+}
+
+/**
+ * Disables text selection and dragging.
+ */
+function disableTextSelectAndDrag() {
+ // Disable text selection.
+ document.onselectstart = function(e) {
+ e.preventDefault();
+ }
+
+ // Disable dragging.
+ document.ondragstart = function(e) {
+ e.preventDefault();
+ }
+}
+
+/**
+ * Check the directionality of the page.
+ * @return {boolean} True if Chrome is running an RTL UI.
+ */
+function isRTL() {
+ return document.documentElement.dir == 'rtl';
+}
+
+/**
+ * Simple common assertion API
+ * @param {*} condition The condition to test. Note that this may be used to
+ * test whether a value is defined or not, and we don't want to force a
+ * cast to Boolean.
+ * @param {string=} opt_message A message to use in any error.
+ */
+function assert(condition, opt_message) {
+ 'use strict';
+ if (!condition) {
+ var msg = 'Assertion failed';
+ if (opt_message)
+ msg = msg + ': ' + opt_message;
+ throw new Error(msg);
+ }
+}
+
+/**
+ * Get an element that's known to exist by its ID. We use this instead of just
+ * calling getElementById and not checking the result because this lets us
+ * satisfy the JSCompiler type system.
+ * @param {string} id The identifier name.
+ * @return {!Element} the Element.
+ */
+function getRequiredElement(id) {
+ var element = $(id);
+ assert(element, 'Missing required element: ' + id);
+ return element;
+}
+
+// Handle click on a link. If the link points to a chrome: or file: url, then
+// call into the browser to do the navigation.
+document.addEventListener('click', function(e) {
+ // Allow preventDefault to work.
+ if (!e.returnValue)
+ return;
+
+ var el = e.target;
+ if (el.nodeType == Node.ELEMENT_NODE &&
+ el.webkitMatchesSelector('A, A *')) {
+ while (el.tagName != 'A') {
+ el = el.parentElement;
+ }
+
+ if ((el.protocol == 'file:' || el.protocol == 'about:') &&
+ (e.button == 0 || e.button == 1)) {
+ chrome.send('navigateToUrl', [
+ el.href,
+ el.target,
+ e.button,
+ e.altKey,
+ e.ctrlKey,
+ e.metaKey,
+ e.shiftKey
+ ]);
+ e.preventDefault();
+ }
+ }
+});
+
+/**
+ * Creates a new URL which is the old URL with a GET param of key=value.
+ * @param {string} url The base URL. There is not sanity checking on the URL so
+ * it must be passed in a proper format.
+ * @param {string} key The key of the param.
+ * @param {string} value The value of the param.
+ * @return {string}
+ */
+function appendParam(url, key, value) {
+ var param = encodeURIComponent(key) + '=' + encodeURIComponent(value);
+
+ if (url.indexOf('?') == -1)
+ return url + '?' + param;
+ return url + '&' + param;
+}
+// Copyright (c) 2011 The Chromium 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 TimelineModel 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.
+ *
+ */
+cr.define('tracing', function() {
+ /**
+ * A TimelineSlice represents an interval of time on a given resource plus
+ * parameters associated with that interval.
+ *
+ * A slice is 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_END() at time=0.3ms
+ * This results in a single timeline slice from 0.1 with duration 0.2 on a
+ * specific thread.
+ *
+ * A slice can also be an interval of time on a Cpu on a TimelineCpu.
+ *
+ * All time units are stored in milliseconds.
+ * @constructor
+ */
+ function TimelineSlice(title, colorId, start, args, opt_duration) {
+ this.title = title;
+ this.start = start;
+ this.colorId = colorId;
+ this.args = args;
+ this.didNotFinish = false;
+ this.subSlices = [];
+ if (opt_duration !== undefined)
+ this.duration = opt_duration;
+ }
+
+ TimelineSlice.prototype = {
+ selected: false,
+
+ duration: undefined,
+
+ get end() {
+ return this.start + this.duration;
+ }
+ };
+
+ /**
+ * A TimelineThread stores all the trace events collected for a particular
+ * thread. We organize the slices on a thread by "subrows," where subrow 0
+ * has all the root slices, subrow 1 those nested 1 deep, and so on. There
+ * is also a set of non-nested subrows.
+ *
+ * @constructor
+ */
+ function TimelineThread(parent, tid) {
+ this.parent = parent;
+ this.tid = tid;
+ this.subRows = [[]];
+ this.nonNestedSubRows = [];
+ }
+
+ TimelineThread.prototype = {
+ /**
+ * Name of the thread, if present.
+ */
+ name: undefined,
+
+ getSubrow: function(i) {
+ while (i >= this.subRows.length)
+ this.subRows.push([]);
+ return this.subRows[i];
+ },
+
+ addNonNestedSlice: function(slice) {
+ for (var i = 0; i < this.nonNestedSubRows.length; i++) {
+ var currSubRow = this.nonNestedSubRows[i];
+ var lastSlice = currSubRow[currSubRow.length - 1];
+ if (slice.start >= lastSlice.start + lastSlice.duration) {
+ currSubRow.push(slice);
+ return;
+ }
+ }
+ this.nonNestedSubRows.push([slice]);
+ },
+
+ /**
+ * Updates the minTimestamp and maxTimestamp fields based on the
+ * current slices and nonNestedSubRows attached to the thread.
+ */
+ updateBounds: function() {
+ var values = [];
+ var slices;
+ if (this.subRows[0].length != 0) {
+ slices = this.subRows[0];
+ values.push(slices[0].start);
+ values.push(slices[slices.length - 1].end);
+ }
+ for (var i = 0; i < this.nonNestedSubRows.length; ++i) {
+ slices = this.nonNestedSubRows[i];
+ values.push(slices[0].start);
+ values.push(slices[slices.length - 1].end);
+ }
+ if (values.length) {
+ this.minTimestamp = Math.min.apply(Math, values);
+ this.maxTimestamp = Math.max.apply(Math, values);
+ } else {
+ this.minTimestamp = undefined;
+ this.maxTimestamp = undefined;
+ }
+ },
+
+ /**
+ * @return {String} A user-friendly name for this thread.
+ */
+ get userFriendlyName() {
+ var tname = this.name || this.tid;
+ return this.parent.pid + ': ' + tname;
+ },
+
+ /**
+ * @return {String} User friendly details about this thread.
+ */
+ get userFriendlyDetials() {
+ return 'pid: ' + this.parent.pid +
+ ', tid: ' + this.tid +
+ (this.name ? ', name: ' + this.name : '');
+ }
+
+ };
+
+ /**
+ * Comparison between threads that orders first by pid,
+ * then by names, then by tid.
+ */
+ TimelineThread.compare = function(x, y) {
+ if (x.parent.pid != y.parent.pid) {
+ return TimelineProcess.compare(x.parent, y.parent.pid);
+ }
+
+ if (x.name && y.name) {
+ var tmp = x.name.localeCompare(y.name);
+ if (tmp == 0)
+ return x.tid - y.tid;
+ return tmp;
+ } else if (x.name) {
+ return -1;
+ } else if (y.name) {
+ return 1;
+ } else {
+ return x.tid - y.tid;
+ }
+ };
+
+ /**
+ * Stores all the samples for a given counter.
+ * @constructor
+ */
+ function TimelineCounter(parent, id, name) {
+ this.parent = parent;
+ this.id = id;
+ this.name = name;
+ this.seriesNames = [];
+ this.seriesColors = [];
+ this.timestamps = [];
+ this.samples = [];
+ }
+
+ TimelineCounter.prototype = {
+ __proto__: Object.prototype,
+
+ get numSeries() {
+ return this.seriesNames.length;
+ },
+
+ get numSamples() {
+ return this.timestamps.length;
+ },
+
+ /**
+ * Updates the bounds for this counter based on the samples it contains.
+ */
+ updateBounds: function() {
+ if (this.seriesNames.length != this.seriesColors.length)
+ throw 'seriesNames.length must match seriesColors.length';
+ if (this.numSeries * this.numSamples != this.samples.length)
+ throw 'samples.length must be a multiple of numSamples.';
+
+ this.totals = [];
+ if (this.samples.length == 0) {
+ this.minTimestamp = undefined;
+ this.maxTimestamp = undefined;
+ this.maxTotal = 0;
+ return;
+ }
+ this.minTimestamp = this.timestamps[0];
+ this.maxTimestamp = this.timestamps[this.timestamps.length - 1];
+
+ var numSeries = this.numSeries;
+ var maxTotal = -Infinity;
+ for (var i = 0; i < this.timestamps.length; i++) {
+ var total = 0;
+ for (var j = 0; j < numSeries; j++) {
+ total += this.samples[i * numSeries + j];
+ this.totals.push(total);
+ }
+ if (total > maxTotal)
+ maxTotal = total;
+ }
+
+ if (this.maxTotal === undefined) {
+ this.maxTotal = maxTotal;
+ }
+ }
+
+ };
+
+ /**
+ * Comparison between counters that orders by pid, then name.
+ */
+ TimelineCounter.compare = function(x, y) {
+ if (x.parent.pid != y.parent.pid) {
+ return TimelineProcess.compare(x.parent, y.parent.pid);
+ }
+ var tmp = x.name.localeCompare(y.name);
+ if (tmp == 0)
+ return x.tid - y.tid;
+ return tmp;
+ };
+
+ /**
+ * The TimelineProcess represents a single process in the
+ * trace. Right now, we keep this around purely for bookkeeping
+ * reasons.
+ * @constructor
+ */
+ function TimelineProcess(pid) {
+ this.pid = pid;
+ this.threads = {};
+ this.counters = {};
+ };
+
+ TimelineProcess.prototype = {
+ get numThreads() {
+ var n = 0;
+ for (var p in this.threads) {
+ n++;
+ }
+ return n;
+ },
+
+ /**
+ * @return {TimlineThread} 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 TimelineThread(this, tid);
+ return this.threads[tid];
+ },
+
+ /**
+ * @return {TimlineCounter} 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 TimelineCounter(this, id, name);
+ return this.counters[id];
+ }
+ };
+
+ /**
+ * Comparison between processes that orders by pid.
+ */
+ TimelineProcess.compare = function(x, y) {
+ return x.pid - y.pid;
+ };
+
+ /**
+ * The TimelineCpu represents a Cpu from the kernel's point of view.
+ * @constructor
+ */
+ function TimelineCpu(number) {
+ this.cpuNumber = number;
+ this.slices = [];
+ this.counters = {};
+ };
+
+ TimelineCpu.prototype = {
+ /**
+ * @return {TimlineCounter} 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 TimelineCounter(this, id, name);
+ return this.counters[id];
+ },
+
+ /**
+ * Updates the minTimestamp and maxTimestamp fields based on the
+ * current slices attached to the cpu.
+ */
+ updateBounds: function() {
+ var values = [];
+ if (this.slices.length) {
+ this.minTimestamp = this.slices[0].start;
+ this.maxTimestamp = this.slices[this.slices.length - 1].end;
+ } else {
+ this.minTimestamp = undefined;
+ this.maxTimestamp = undefined;
+ }
+ }
+ };
+
+ /**
+ * Comparison between processes that orders by cpuNumber.
+ */
+ TimelineCpu.compare = function(x, y) {
+ return x.cpuNumber - y.cpuNumber;
+ };
+
+ // The color pallette is split in half, with the upper
+ // half of the pallette being the "highlighted" verison
+ // of the base color. So, color 7's highlighted form is
+ // 7 + (pallette.length / 2).
+ //
+ // These bright versions of colors are automatically generated
+ // from the base colors.
+ //
+ // Within the color pallette, there are "regular" 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.
+ const palletteBase = [
+ {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},
+ // Reserved Entires
+ {r: 182, g: 125, b: 143},
+ {r: 126, g: 200, b: 148},
+ {r: 133, g: 160, b: 210},
+ {r: 240, g: 240, b: 240}];
+
+ // Make sure this number tracks the number of reserved entries in the
+ // pallette.
+ const numReservedColorIds = 4;
+
+ function brighten(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 colorToString(c) {
+ return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')';
+ }
+
+ /**
+ * The number of color IDs that getStringColorId can choose from.
+ */
+ const numRegularColorIds = palletteBase.length - numReservedColorIds;
+ const highlightIdBoost = palletteBase.length;
+
+ const pallette = palletteBase.concat(palletteBase.map(brighten)).
+ map(colorToString);
+ /**
+ * 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 pallette.
+ */
+ function getPallette() {
+ return pallette;
+ }
+
+ /**
+ * @return {Number} The value to add to a color ID to get its highlighted
+ * colro ID. E.g. 7 + getPalletteHighlightIdBoost() yields a brightened from
+ * of 7's base color.
+ */
+ function getPalletteHighlightIdBoost() {
+ return highlightIdBoost;
+ }
+
+ /**
+ * @param {String} name The color name.
+ * @return {Number} The color ID for the given color name.
+ */
+ function getColorIdByName(name) {
+ if (name == 'iowait')
+ return numRegularColorIds;
+ if (name == 'running')
+ return numRegularColorIds + 1;
+ if (name == 'runnable')
+ return numRegularColorIds + 2;
+ if (name == 'sleeping')
+ return numRegularColorIds + 3;
+ throw 'Unrecognized color ' + name;
+ }
+
+ // 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 regular
+ * ID space only, e.g. no reserved ID will be used.
+ */
+ function getStringColorId(string) {
+ if (stringColorIdCache[string] === undefined) {
+ var hash = getStringHash(string);
+ stringColorIdCache[string] = hash % numRegularColorIds;
+ }
+ return stringColorIdCache[string];
+ }
+
+ /**
+ * Builds a model from an array of TraceEvent objects.
+ * @param {Object=} opt_data The event data to import into the new model.
+ * See TimelineModel.importEvents for details and more advanced ways to
+ * import data.
+ * @param {bool=} opt_zeroAndBoost Whether to align to zero and boost the
+ * by 15%. Defaults to true.
+ * @constructor
+ */
+ function TimelineModel(opt_eventData, opt_zeroAndBoost) {
+ this.cpus = {};
+ this.processes = {};
+ this.importErrors = [];
+
+ if (opt_eventData)
+ this.importEvents(opt_eventData, opt_zeroAndBoost);
+ }
+
+ var importerConstructors = [];
+
+ /**
+ * Registers an importer. All registered importers are considered
+ * when processing an import request.
+ *
+ * @param {Function} importerConstructor The importer's constructor function.
+ */
+ TimelineModel.registerImporter = function(importerConstructor) {
+ importerConstructors.push(importerConstructor);
+ }
+
+ TimelineModel.prototype = {
+ __proto__: cr.EventTarget.prototype,
+
+ get numProcesses() {
+ var n = 0;
+ for (var p in this.processes)
+ n++;
+ return n;
+ },
+
+ /**
+ * @return {TimelineProcess} Gets a specific TimelineCpu or creates one if
+ * it does not exist.
+ */
+ getOrCreateCpu: function(cpuNumber) {
+ if (!this.cpus[cpuNumber])
+ this.cpus[cpuNumber] = new TimelineCpu(cpuNumber);
+ return this.cpus[cpuNumber];
+ },
+
+ /**
+ * @return {TimelineProcess} Gets a TimlineProcess for a specified pid or
+ * creates one if it does not exist.
+ */
+ getOrCreateProcess: function(pid) {
+ if (!this.processes[pid])
+ this.processes[pid] = new TimelineProcess(pid);
+ return this.processes[pid];
+ },
+
+ /**
+ * The import takes an array of json-ified TraceEvents and adds them into
+ * the TimelineModel as processes, threads, and slices.
+ */
+
+ /**
+ * Removes threads from the model that are fully empty.
+ */
+ pruneEmptyThreads: function() {
+ for (var pid in this.processes) {
+ var process = this.processes[pid];
+ var prunedThreads = {};
+ for (var tid in process.threads) {
+ var thread = process.threads[tid];
+
+ // Begin-events without matching end events leave a thread in a state
+ // where the toplevel subrows are empty but child subrows have
+ // entries. The autocloser will fix this up later. But, for the
+ // purposes of pruning, such threads need to be treated as having
+ // content.
+ var hasNonEmptySubrow = false;
+ for (var s = 0; s < thread.subRows.length; s++)
+ hasNonEmptySubrow |= thread.subRows[s].length > 0;
+
+ if (hasNonEmptySubrow || thread.nonNestedSubRows.legnth)
+ prunedThreads[tid] = thread;
+ }
+ process.threads = prunedThreads;
+ }
+ },
+
+ updateBounds: function() {
+ var wmin = Infinity;
+ var wmax = -wmin;
+ var hasData = false;
+
+ var threads = this.getAllThreads();
+ for (var tI = 0; tI < threads.length; tI++) {
+ var thread = threads[tI];
+ thread.updateBounds();
+ if (thread.minTimestamp != undefined &&
+ thread.maxTimestamp != undefined) {
+ wmin = Math.min(wmin, thread.minTimestamp);
+ wmax = Math.max(wmax, thread.maxTimestamp);
+ hasData = true;
+ }
+ }
+ var counters = this.getAllCounters();
+ for (var tI = 0; tI < counters.length; tI++) {
+ var counter = counters[tI];
+ counter.updateBounds();
+ if (counter.minTimestamp != undefined &&
+ counter.maxTimestamp != undefined) {
+ hasData = true;
+ wmin = Math.min(wmin, counter.minTimestamp);
+ wmax = Math.max(wmax, counter.maxTimestamp);
+ }
+ }
+
+ for (var cpuNumber in this.cpus) {
+ var cpu = this.cpus[cpuNumber];
+ cpu.updateBounds();
+ if (cpu.minTimestamp != undefined &&
+ cpu.maxTimestamp != undefined) {
+ hasData = true;
+ wmin = Math.min(wmin, cpu.minTimestamp);
+ wmax = Math.max(wmax, cpu.maxTimestamp);
+ }
+ }
+
+ if (hasData) {
+ this.minTimestamp = wmin;
+ this.maxTimestamp = wmax;
+ } else {
+ this.maxTimestamp = undefined;
+ this.minTimestamp = undefined;
+ }
+ },
+
+ shiftWorldToZero: function() {
+ if (this.minTimestamp === undefined)
+ return;
+ var timeBase = this.minTimestamp;
+ var threads = this.getAllThreads();
+ for (var tI = 0; tI < threads.length; tI++) {
+ var thread = threads[tI];
+ var shiftSubRow = function(subRow) {
+ for (var tS = 0; tS < subRow.length; tS++) {
+ var slice = subRow[tS];
+ slice.start = (slice.start - timeBase);
+ }
+ };
+
+ if (thread.cpuSlices)
+ shiftSubRow(thread.cpuSlices);
+
+ for (var tSR = 0; tSR < thread.subRows.length; tSR++) {
+ shiftSubRow(thread.subRows[tSR]);
+ }
+ for (var tSR = 0; tSR < thread.nonNestedSubRows.length; tSR++) {
+ shiftSubRow(thread.nonNestedSubRows[tSR]);
+ }
+ }
+ var counters = this.getAllCounters();
+ for (var tI = 0; tI < counters.length; tI++) {
+ var counter = counters[tI];
+ for (var sI = 0; sI < counter.timestamps.length; sI++)
+ counter.timestamps[sI] = (counter.timestamps[sI] - timeBase);
+ }
+ var cpus = this.getAllCpus();
+ for (var tI = 0; tI < cpus.length; tI++) {
+ var cpu = cpus[tI];
+ for (var sI = 0; sI < cpu.slices.length; sI++)
+ cpu.slices[sI].start = (cpu.slices[sI].start - timeBase);
+ }
+ this.updateBounds();
+ },
+
+ getAllThreads: function() {
+ var threads = [];
+ 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 cpus in the model.
+ */
+ getAllCpus: function() {
+ var cpus = [];
+ for (var cpu in this.cpus)
+ cpus.push(this.cpus[cpu]);
+ return cpus;
+ },
+
+ /**
+ * @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 = [];
+ for (var pid in this.processes) {
+ var process = this.processes[pid];
+ for (var tid in process.counters) {
+ counters.push(process.counters[tid]);
+ }
+ }
+ for (var cpuNumber in this.cpus) {
+ var cpu = this.cpus[cpuNumber];
+ for (var counterName in cpu.counters)
+ counters.push(cpu.counters[counterName]);
+ }
+ return counters;
+ },
+
+ /**
+ * Imports the provided events into the model. The eventData type
+ * is undefined and will be passed to all the timeline importers registered
+ * via TimelineModel.registerImporter. The first importer that returns true
+ * for canImport(events) will be used to import the events.
+ *
+ * @param {Object} events Events to import.
+ * @param {boolean} isChildImport True the eventData being imported is an
+ * additional trace after the primary eventData.
+ */
+ importOneTrace_: function(eventData, isAdditionalImport) {
+ var importerConstructor;
+ for (var i = 0; i < importerConstructors.length; ++i) {
+ if (importerConstructors[i].canImport(eventData)) {
+ importerConstructor = importerConstructors[i];
+ break;
+ }
+ }
+ if (!importerConstructor)
+ throw 'Could not find an importer for the provided eventData.';
+
+ var importer = new importerConstructor(
+ this, eventData, isAdditionalImport);
+ importer.importEvents();
+ this.pruneEmptyThreads();
+ },
+
+ /**
+ * Imports the provided traces into the model. The eventData type
+ * is undefined and will be passed to all the timeline importers registered
+ * via TimelineModel.registerImporter. 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 {Object} eventData Events to import.
+ * @param {bool=} opt_zeroAndBoost Whether to align to zero and boost the
+ * by 15%. Defaults to true.
+ * @param {Array=} opt_additionalEventData An array of eventData objects
+ * (e.g. array of arrays) to
+ * import after importing the primary events.
+ */
+ importEvents: function(eventData,
+ opt_zeroAndBoost, opt_additionalEventData) {
+ if (opt_zeroAndBoost === undefined)
+ opt_zeroAndBoost = true;
+
+ this.importOneTrace_(eventData, false);
+ if (opt_additionalEventData) {
+ for (var i = 0; i < opt_additionalEventData.length; ++i) {
+ this.importOneTrace_(opt_additionalEventData[i], true);
+ }
+ }
+
+ this.updateBounds();
+
+ if (opt_zeroAndBoost)
+ this.shiftWorldToZero();
+
+ if (opt_zeroAndBoost &&
+ this.minTimestamp !== undefined &&
+ this.maxTimestamp !== undefined) {
+ var boost = (this.maxTimestamp - this.minTimestamp) * 0.15;
+ this.minTimestamp = this.minTimestamp - boost;
+ this.maxTimestamp = this.maxTimestamp + boost;
+ }
+ }
+ };
+
+ return {
+ getPallette: getPallette,
+ getPalletteHighlightIdBoost: getPalletteHighlightIdBoost,
+ getColorIdByName: getColorIdByName,
+ getStringHash: getStringHash,
+ getStringColorId: getStringColorId,
+
+ TimelineSlice: TimelineSlice,
+ TimelineThread: TimelineThread,
+ TimelineCounter: TimelineCounter,
+ TimelineProcess: TimelineProcess,
+ TimelineCpu: TimelineCpu,
+ TimelineModel: TimelineModel
+ };
+});
+// Copyright (c) 2011 The Chromium 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 Imports text files in the Linux event trace format into the
+ * timeline model. 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.
+ */
+cr.define('tracing', function() {
+ /**
+ * Represents the scheduling state for a single thread.
+ * @constructor
+ */
+ function CpuState(cpu) {
+ this.cpu = cpu;
+ }
+
+ CpuState.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Switches the active pid on this Cpu. If necessary, add a TimelineSlice
+ * to the cpu representing the time spent on that Cpu since the last call to
+ * switchRunningLinuxPid.
+ */
+ switchRunningLinuxPid: function(importer, prevState, ts, pid, comm, prio) {
+ // Generate a slice if the last active pid was not the idle task
+ if (this.lastActivePid !== undefined && this.lastActivePid != 0) {
+ var duration = ts - this.lastActiveTs;
+ var thread = importer.threadsByLinuxPid[this.lastActivePid];
+ if (thread)
+ name = thread.userFriendlyName;
+ else
+ name = this.lastActiveComm;
+
+ var slice = new tracing.TimelineSlice(name,
+ tracing.getStringColorId(name),
+ this.lastActiveTs,
+ {comm: this.lastActiveComm,
+ tid: this.lastActivePid,
+ prio: this.lastActivePrio,
+ stateWhenDescheduled: prevState
+ },
+ duration);
+ this.cpu.slices.push(slice);
+ }
+
+ this.lastActiveTs = ts;
+ this.lastActivePid = pid;
+ this.lastActiveComm = comm;
+ this.lastActivePrio = prio;
+ }
+ };
+
+ function ThreadState(tid) {
+ this.openSlices = [];
+ }
+
+ /**
+ * Imports linux perf events into a specified model.
+ * @constructor
+ */
+ function LinuxPerfImporter(model, events, isAdditionalImport) {
+ this.isAdditionalImport_ = isAdditionalImport;
+ this.model_ = model;
+ this.events_ = events;
+ this.clockSyncRecords_ = [];
+ this.cpuStates_ = {};
+ this.kernelThreadStates_ = {};
+ this.buildMapFromLinuxPidsToTimelineThreads();
+
+ // To allow simple indexing of threads, we store all the threads by their
+ // kernel KPID. The KPID is a unique key for a thread in the trace.
+ this.threadStateByKPID_ = {};
+ }
+
+ TestExports = {};
+
+ // Matches the generic trace record:
+ // <idle>-0 [001] 1.23: sched_switch
+ var lineRE = /^\s*(.+?)\s+\[(\d+)\]\s*(\d+\.\d+):\s+(\S+):\s(.*)$/;
+ TestExports.lineRE = lineRE;
+
+ // Matches the sched_switch record
+ var schedSwitchRE = new RegExp(
+ 'prev_comm=(.+) prev_pid=(\\d+) prev_prio=(\\d+) prev_state=(\\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;
+
+ // 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;
+
+ // 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 (.+)/;
+
+ /**
+ * 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 (/^# tracer:/.exec(events))
+ return true;
+
+ var m = /^(.+)\n/.exec(events);
+ if (m)
+ events = m[1];
+ if (lineRE.exec(events))
+ return true;
+
+ return false;
+ };
+
+ LinuxPerfImporter.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Precomputes a lookup table from linux pids back to existing
+ * TimelineThreads. This is used during importing to add information to each
+ * timeline thread about whether it was running, descheduled, sleeping, et
+ * cetera.
+ */
+ buildMapFromLinuxPidsToTimelineThreads: function() {
+ this.threadsByLinuxPid = {};
+ this.model_.getAllThreads().forEach(
+ function(thread) {
+ this.threadsByLinuxPid[thread.tid] = thread;
+ }.bind(this));
+ },
+
+ /**
+ * @return {CpuState} A CpuState corresponding to the given cpuNumber.
+ */
+ getOrCreateCpuState: function(cpuNumber) {
+ if (!this.cpuStates_[cpuNumber]) {
+ var cpu = this.model_.getOrCreateCpu(cpuNumber);
+ this.cpuStates_[cpuNumber] = new CpuState(cpu);
+ }
+ return this.cpuStates_[cpuNumber];
+ },
+
+ /**
+ * @return {number} The pid extracted from the kernel thread name.
+ */
+ parsePid: function(kernelThreadName) {
+ var pid = /.+-(\d+)/.exec(kernelThreadName)[1];
+ pid = parseInt(pid);
+ return pid;
+ },
+
+ /**
+ * @return {number} The string portion of the thread extracted from the
+ * kernel thread name.
+ */
+ parseThreadName: function(kernelThreadName) {
+ return /(.+)-\d+/.exec(kernelThreadName)[1];
+ },
+
+ /**
+ * @return {TimelinThread} A thread corresponding to the kernelThreadName
+ */
+ getOrCreateKernelThread: function(kernelThreadName) {
+ if (!this.kernelThreadStates_[kernelThreadName]) {
+ pid = this.parsePid(kernelThreadName);
+
+ var thread = this.model_.getOrCreateProcess(pid).getOrCreateThread(pid);
+ thread.name = kernelThreadName;
+ this.kernelThreadStates_[kernelThreadName] = {
+ pid: pid,
+ thread: thread,
+ openSlice: undefined,
+ openSliceTS: undefined
+ };
+ this.threadsByLinuxPid[pid] = thread;
+ }
+ return this.kernelThreadStates_[kernelThreadName];
+ },
+
+ /**
+ * Imports the data in this.events_ into model_.
+ */
+ importEvents: function() {
+ this.importCpuData();
+ if (!this.alignClocks())
+ return;
+ this.buildPerThreadCpuSlicesFromCpuState();
+ },
+
+ /**
+ * Builds the cpuSlices array on each thread based on our knowledge of what
+ * each Cpu is doing. This is done only for TimelineThreads 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.cpuStates_) {
+ var cpuState = this.cpuStates_[cpuNumber];
+ var cpu = cpuState.cpu;
+
+ for (var i = 0; i < cpu.slices.length; i++) {
+ var slice = cpu.slices[i];
+
+ var thread = this.threadsByLinuxPid[slice.args.tid];
+ if (!thread)
+ continue;
+ if (!thread.tempCpuSlices)
+ thread.tempCpuSlices = [];
+
+ // Because Chrome's Array.sort is not a stable sort, we need to keep
+ // the slice index around to keep slices with identical start times in
+ // the proper order when sorting them.
+ slice.index = i;
+
+ thread.tempCpuSlices.push(slice);
+ }
+ }
+
+ // Create slices for when the thread is not running.
+ var runningId = tracing.getColorIdByName('running');
+ var runnableId = tracing.getColorIdByName('runnable');
+ var sleepingId = tracing.getColorIdByName('sleeping');
+ var ioWaitId = tracing.getColorIdByName('iowait');
+ this.model_.getAllThreads().forEach(function(thread) {
+ if (!thread.tempCpuSlices)
+ return;
+ var origSlices = thread.tempCpuSlices;
+ delete thread.tempCpuSlices;
+
+ origSlices.sort(function(x, y) {
+ var delta = x.start - y.start;
+ if (delta == 0) {
+ // Break ties using the original slice ordering.
+ return x.index - y.index;
+ } else {
+ return delta;
+ }
+ });
+
+ // 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];
+ slices.push(new tracing.TimelineSlice('Running', runningId,
+ slice.start, {}, slice.duration));
+ }
+ for (var i = 1; i < origSlices.length; i++) {
+ var prevSlice = origSlices[i - 1];
+ var nextSlice = origSlices[i];
+ var midDuration = nextSlice.start - prevSlice.end;
+ if (prevSlice.args.stateWhenDescheduled == 'S') {
+ slices.push(new tracing.TimelineSlice('Sleeping', sleepingId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'R') {
+ slices.push(new tracing.TimelineSlice('Runnable', runnableId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'D') {
+ slices.push(new tracing.TimelineSlice('I/O Wait', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'T') {
+ slices.push(new tracing.TimelineSlice('__TASK_STOPPED', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 't') {
+ slices.push(new tracing.TimelineSlice('debug', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'Z') {
+ slices.push(new tracing.TimelineSlice('Zombie', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'X') {
+ slices.push(new tracing.TimelineSlice('Exit Dead', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'x') {
+ slices.push(new tracing.TimelineSlice('Task Dead', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else if (prevSlice.args.stateWhenDescheduled == 'W') {
+ slices.push(new tracing.TimelineSlice('WakeKill', ioWaitId,
+ prevSlice.end, {}, midDuration));
+ } else {
+ throw 'Unrecognized state: ' + prevSlice.args.stateWhenDescheduled;
+ }
+
+ slices.push(new tracing.TimelineSlice('Running', runningId,
+ nextSlice.start, {}, nextSlice.duration));
+ }
+ thread.cpuSlices = slices;
+ });
+ },
+
+ /**
+ * Walks the slices stored on this.cpuStates_ and adjusts their timestamps
+ * based on any alignment metadata we discovered.
+ */
+ alignClocks: function() {
+ if (this.clockSyncRecords_.length == 0) {
+ // If this is an additional import, and no clock syncing records were
+ // found, then abort the import. Otherwise, just skip clock alignment.
+ if (!this.isAdditionalImport_)
+ return;
+
+ // Remove the newly imported CPU slices from the model.
+ this.abortImport();
+ return false;
+ }
+
+ // Shift all the slice times based on the sync record.
+ var sync = this.clockSyncRecords_[0];
+ var timeShift = sync.parentTS - sync.perfTS;
+ for (var cpuNumber in this.cpuStates_) {
+ var cpuState = this.cpuStates_[cpuNumber];
+ var cpu = cpuState.cpu;
+
+ for (var i = 0; i < cpu.slices.length; i++) {
+ var slice = cpu.slices[i];
+ slice.start = slice.start + timeShift;
+ slice.duration = slice.duration;
+ }
+
+ for (var counterName in cpu.counters) {
+ var counter = cpu.counters[counterName];
+ for (var sI = 0; sI < counter.timestamps.length; sI++)
+ counter.timestamps[sI] = (counter.timestamps[sI] + timeShift);
+ }
+ }
+ for (var kernelThreadName in this.kernelThreadStates_) {
+ var kthread = this.kernelThreadStates_[kernelThreadName];
+ var thread = kthread.thread;
+ for (var i = 0; i < thread.subRows[0].length; i++) {
+ thread.subRows[0][i].start += timeShift;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Removes any data that has been added to the model because of an error
+ * detected during the import.
+ */
+ abortImport: function() {
+ if (this.pushedEventsToThreads)
+ throw 'Cannot abort, have alrady pushedCpuDataToThreads.';
+
+ for (var cpuNumber in this.cpuStates_)
+ delete this.model_.cpus[cpuNumber];
+ for (var kernelThreadName in this.kernelThreadStates_) {
+ var kthread = this.kernelThreadStates_[kernelThreadName];
+ var thread = kthread.thread;
+ var process = thread.parent;
+ delete process.threads[thread.tid];
+ delete this.model_.processes[process.pid];
+ }
+ this.model_.importErrors.push(
+ 'Cannot import kernel trace without a clock sync.');
+ },
+
+ /**
+ * Records the fact that a pid has become runnable. This data will
+ * eventually get used to derive each thread's cpuSlices array.
+ */
+ markPidRunnable: function(ts, pid, comm, prio) {
+ // TODO(nduca): implement this functionality.
+ },
+
+ /**
+ * Helper to process a 'begin' event (e.g. initiate a slice).
+ * @param {ThreadState} state Thread state (holds slices).
+ * @param {string} name The trace event name.
+ * @param {number} ts The trace event begin timestamp.
+ */
+ processBegin: function(state, tname, name, ts, pid, tid) {
+ var colorId = tracing.getStringColorId(name);
+ var slice = new tracing.TimelineSlice(name, colorId, ts, null);
+ // XXX: Should these be removed from the slice before putting it into the
+ // model?
+ slice.pid = pid;
+ slice.tid = tid;
+ slice.threadName = tname;
+ state.openSlices.push(slice);
+ },
+
+ /**
+ * Helper to process an 'end' event (e.g. close a slice).
+ * @param {ThreadState} state Thread state (holds slices).
+ * @param {number} ts The trace event begin timestamp.
+ */
+ processEnd: function(state, ts) {
+ if (state.openSlices.length == 0) {
+ // Ignore E events that are unmatched.
+ return;
+ }
+ var slice = state.openSlices.pop();
+ slice.duration = ts - slice.start;
+
+ // Store the slice on the correct subrow.
+ var thread = this.model_.getOrCreateProcess(slice.pid).
+ getOrCreateThread(slice.tid);
+ if (!thread.name)
+ thread.name = slice.threadName;
+ this.threadsByLinuxPid[slice.tid] = thread;
+ var subRowIndex = state.openSlices.length;
+ thread.getSubrow(subRowIndex).push(slice);
+
+ // Add the slice to the subSlices array of its parent.
+ if (state.openSlices.length) {
+ var parentSlice = state.openSlices[state.openSlices.length - 1];
+ parentSlice.subSlices.push(slice);
+ }
+ },
+
+ /**
+ * Helper function that closes any open slices. This happens when a trace
+ * ends before an 'E' phase event can get posted. When that happens, this
+ * closes the slice at the highest timestamp we recorded and sets the
+ * didNotFinish flag to true.
+ */
+ autoCloseOpenSlices: function() {
+ // We need to know the model bounds in order to assign an end-time to
+ // the open slices.
+ this.model_.updateBounds();
+
+ // The model's max value in the trace is wrong at this point if there are
+ // un-closed events. To close those events, we need the true global max
+ // value. To compute this, build a list of timestamps that weren't
+ // included in the max calculation, then compute the real maximum based
+ // on that.
+ var openTimestamps = [];
+ for (var kpid in this.threadStateByKPID_) {
+ var state = this.threadStateByKPID_[kpid];
+ for (var i = 0; i < state.openSlices.length; i++) {
+ var slice = state.openSlices[i];
+ openTimestamps.push(slice.start);
+ for (var s = 0; s < slice.subSlices.length; s++) {
+ var subSlice = slice.subSlices[s];
+ openTimestamps.push(subSlice.start);
+ if (subSlice.duration)
+ openTimestamps.push(subSlice.end);
+ }
+ }
+ }
+
+ // Figure out the maximum value of model.maxTimestamp and
+ // Math.max(openTimestamps). Made complicated by the fact that the model
+ // timestamps might be undefined.
+ var realMaxTimestamp;
+ if (this.model_.maxTimestamp) {
+ realMaxTimestamp = Math.max(this.model_.maxTimestamp,
+ Math.max.apply(Math, openTimestamps));
+ } else {
+ realMaxTimestamp = Math.max.apply(Math, openTimestamps);
+ }
+
+ // Automatically close any slices are still open. These occur in a number
+ // of reasonable situations, e.g. deadlock. This pass ensures the open
+ // slices make it into the final model.
+ for (var kpid in this.threadStateByKPID_) {
+ var state = this.threadStateByKPID_[kpid];
+ while (state.openSlices.length > 0) {
+ var slice = state.openSlices.pop();
+ slice.duration = realMaxTimestamp - slice.start;
+ slice.didNotFinish = true;
+
+ // Store the slice on the correct subrow.
+ var thread = this.model_.getOrCreateProcess(slice.pid)
+ .getOrCreateThread(slice.tid);
+ var subRowIndex = state.openSlices.length;
+ thread.getSubrow(subRowIndex).push(slice);
+
+ // Add the slice to the subSlices array of its parent.
+ if (state.openSlices.length) {
+ var parentSlice = state.openSlices[state.openSlices.length - 1];
+ parentSlice.subSlices.push(slice);
+ }
+ }
+ }
+ },
+
+ /**
+ * Helper that creates and adds samples to a TimelineCounter object based on
+ * 'C' phase events.
+ */
+ processCounter: function(name, ts, value, pid) {
+ var ctr = this.model_.getOrCreateProcess(pid)
+ .getOrCreateCounter('', name);
+
+ // Initialize the counter's series fields if needed.
+ //
+ if (ctr.numSeries == 0) {
+ ctr.seriesNames.push('state');
+ ctr.seriesColors.push(
+ tracing.getStringColorId(ctr.name + '.' + 'state'));
+ }
+
+ // Add the sample values.
+ ctr.timestamps.push(ts);
+ ctr.samples.push(value);
+ },
+
+
+ /**
+ * Walks the this.events_ structure and creates TimelineCpu objects.
+ */
+ importCpuData: function() {
+ this.lines_ = this.events_.split('\n');
+
+ for (var lineNumber = 0; lineNumber < this.lines_.length; ++lineNumber) {
+ var line = this.lines_[lineNumber];
+ if (/^#/.exec(line) || line.length == 0)
+ continue;
+ var eventBase = lineRE.exec(line);
+ if (!eventBase) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Unrecognized line: ' + line);
+ continue;
+ }
+
+ var cpuState = this.getOrCreateCpuState(parseInt(eventBase[2]));
+ var ts = parseFloat(eventBase[3]) * 1000;
+
+ var eventName = eventBase[4];
+
+ if (eventName == 'sched_switch') {
+ var event = schedSwitchRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed sched_switch event');
+ continue;
+ }
+
+ var prevState = event[4];
+ var nextComm = event[5];
+ var nextPid = parseInt(event[6]);
+ var nextPrio = parseInt(event[7]);
+ cpuState.switchRunningLinuxPid(
+ this, prevState, ts, nextPid, nextComm, nextPrio);
+
+ } else if (eventName == 'sched_wakeup') {
+ var event = schedWakeupRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed sched_wakeup event');
+ continue;
+ }
+
+ var comm = event[1];
+ var pid = parseInt(event[2]);
+ var prio = parseInt(event[3]);
+ this.markPidRunnable(ts, pid, comm, prio);
+
+ } else if (eventName == 'cpu_frequency') {
+ var event = /state=(\d+) cpu_id=(\d+)/.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed cpu_frequency event');
+ continue;
+ }
+ var targetCpuNumber = parseInt(event[2]);
+ var targetCpu = this.getOrCreateCpuState(targetCpuNumber);
+ var freqCounter =
+ targetCpu.cpu.getOrCreateCounter('', 'Frequency');
+ if (freqCounter.numSeries == 0) {
+ freqCounter.seriesNames.push('state');
+ freqCounter.seriesColors.push(
+ tracing.getStringColorId(freqCounter.name + '.' + 'state'));
+ }
+ var freqState = parseInt(event[1]);
+ freqCounter.timestamps.push(ts);
+ freqCounter.samples.push(freqState);
+ } else if (eventName == 'cpufreq_interactive_already' ||
+ eventName == 'cpufreq_interactive_target') {
+ var event = /cpu=(\d+) load=(\d+) cur=(\d+) targ=(\d+)/.
+ exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed cpufreq_interactive_* event');
+ continue;
+ }
+ var targetCpuNumber = parseInt(event[1]);
+ var targetCpu = this.getOrCreateCpuState(targetCpuNumber);
+ var loadCounter =
+ targetCpu.cpu.getOrCreateCounter('', 'Load');
+ if (loadCounter.numSeries == 0) {
+ loadCounter.seriesNames.push('state');
+ loadCounter.seriesColors.push(
+ tracing.getStringColorId(loadCounter.name + '.' + 'state'));
+ }
+ var loadState = parseInt(event[2]);
+ loadCounter.timestamps.push(ts);
+ loadCounter.samples.push(loadState);
+ loadCounter.maxTotal = 100;
+ loadCounter.skipUpdateBounds = true;
+ } else if (eventName == 'workqueue_execute_start') {
+ var event = workqueueExecuteStartRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed workqueue_execute_start event');
+ continue;
+ }
+ var kthread = this.getOrCreateKernelThread(eventBase[1]);
+ kthread.openSliceTS = ts;
+ kthread.openSlice = event[2];
+
+ } else if (eventName == 'workqueue_execute_end') {
+ var event = workqueueExecuteEndRE.exec(eventBase[5]);
+ if (!event) {
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Malformed workqueue_execute_start event');
+ continue;
+ }
+ var kthread = this.getOrCreateKernelThread(eventBase[1]);
+ if (kthread.openSlice) {
+ var slice = new tracing.TimelineSlice(kthread.openSlice,
+ tracing.getStringColorId(kthread.openSlice),
+ kthread.openSliceTS,
+ {},
+ ts - kthread.openSliceTS);
+
+ kthread.thread.subRows[0].push(slice);
+ }
+ kthread.openSlice = undefined;
+
+ } else if (eventName == '0') { // trace_mark's show up with 0 prefixes.
+ var event = traceEventClockSyncRE.exec(eventBase[5]);
+ if (event)
+ this.clockSyncRecords_.push({
+ perfTS: ts,
+ parentTS: event[1] * 1000
+ });
+ else {
+ var tid = this.parsePid(eventBase[1]);
+ var tname = this.parseThreadName(eventBase[1]);
+ var kpid = tid;
+
+ if (!(kpid in this.threadStateByKPID_))
+ this.threadStateByKPID_[kpid] = new ThreadState();
+ var state = this.threadStateByKPID_[kpid];
+
+ var event = eventBase[5].split('|')
+ switch (event[0]) {
+ case 'B':
+ var pid = parseInt(event[1]);
+ var name = event[2];
+ this.processBegin(state, tname, name, ts, pid, tid);
+ break;
+ case 'E':
+ this.processEnd(state, ts);
+ break;
+ case 'C':
+ var pid = parseInt(event[1]);
+ var name = event[2];
+ var value = parseInt(event[3]);
+ this.processCounter(name, ts, value, pid);
+ break;
+ default:
+ this.model_.importErrors.push('Line ' + (lineNumber + 1) +
+ ': Unrecognized event: ' + eventBase[5]);
+ }
+ }
+ }
+ }
+
+ // Autoclose any open slices.
+ var hasOpenSlices = false;
+ for (var kpid in this.threadStateByKPID_) {
+ var state = this.threadStateByKPID_[kpid];
+ hasOpenSlices |= state.openSlices.length > 0;
+ }
+ if (hasOpenSlices)
+ this.autoCloseOpenSlices();
+ }
+ };
+
+ tracing.TimelineModel.registerImporter(LinuxPerfImporter);
+
+ return {
+ LinuxPerfImporter: LinuxPerfImporter,
+ _LinuxPerfImporterTestExports: TestExports
+ };
+
+});
+// Copyright (c) 2011 The Chromium 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 TraceEventImporter imports TraceEvent-formatted data
+ * into the provided timeline model.
+ */
+cr.define('tracing', function() {
+ function ThreadState(tid) {
+ this.openSlices = [];
+ this.openNonNestedSlices = {};
+ }
+
+ function TraceEventImporter(model, eventData) {
+ this.model_ = model;
+
+ if (typeof(eventData) === 'string' || eventData instanceof String) {
+ // 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] == '[') {
+ n = eventData.length;
+ if (eventData[n - 1] != ']' && eventData[n - 1] != '\n') {
+ eventData = eventData + ']';
+ } else if (eventData[n - 2] != ']' && eventData[n - 1] == '\n') {
+ eventData = eventData + ']';
+ } else if (eventData[n - 3] != ']' && eventData[n - 2] == '\r' &&
+ eventData[n - 1] == '\n') {
+ eventData = eventData + ']';
+ }
+ }
+ this.events_ = JSON.parse(eventData);
+
+ } else {
+ this.events_ = eventData;
+ }
+
+ // 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)
+ this.events_ = this.events_.traceEvents;
+
+ // To allow simple indexing of threads, we store all the threads by a
+ // PTID. A ptid is a pid and tid joined together x:y fashion, eg
+ // 1024:130. The ptid is a unique key for a thread in the trace.
+ this.threadStateByPTID_ = {};
+ }
+
+ /**
+ * @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) {
+ return eventData[0] == '{' || eventData[0] == '[';
+ }
+
+ // Might just be an array of events
+ if (eventData instanceof Array && eventData[0].ph)
+ return true;
+
+ // Might be an object with a traceEvents field in it.
+ if (eventData.traceEvents)
+ return eventData.traceEvents instanceof Array &&
+ eventData.traceEvents[0].ph;
+
+ return false;
+ };
+
+ TraceEventImporter.prototype = {
+
+ __proto__: Object.prototype,
+
+ /**
+ * Helper to process a 'begin' event (e.g. initiate a slice).
+ * @param {ThreadState} state Thread state (holds slices).
+ * @param {Object} event The current trace event.
+ */
+ processBegin: function(index, state, event) {
+ var colorId = tracing.getStringColorId(event.name);
+ var slice =
+ { index: index,
+ slice: new tracing.TimelineSlice(event.name, colorId,
+ event.ts / 1000,
+ event.args) };
+
+ if (event.uts)
+ slice.slice.startInUserTime = event.uts / 1000;
+
+ if (event.args['ui-nest'] === '0') {
+ var sliceID = event.name;
+ for (var x in event.args)
+ sliceID += ';' + event.args[x];
+ if (state.openNonNestedSlices[sliceID])
+ this.model_.importErrors.push('Event ' + sliceID + ' already open.');
+ state.openNonNestedSlices[sliceID] = slice;
+ } else {
+ state.openSlices.push(slice);
+ }
+ },
+
+ /**
+ * Helper to process an 'end' event (e.g. close a slice).
+ * @param {ThreadState} state Thread state (holds slices).
+ * @param {Object} event The current trace event.
+ */
+ processEnd: function(state, event) {
+ if (event.args['ui-nest'] === '0') {
+ var sliceID = event.name;
+ for (var x in event.args)
+ sliceID += ';' + event.args[x];
+ var slice = state.openNonNestedSlices[sliceID];
+ if (!slice)
+ return;
+ slice.slice.duration = (event.ts / 1000) - slice.slice.start;
+ if (event.uts)
+ slice.durationInUserTime = (event.uts / 1000) -
+ slice.slice.startInUserTime;
+
+ // Store the slice in a non-nested subrow.
+ var thread =
+ this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ thread.addNonNestedSlice(slice.slice);
+ delete state.openNonNestedSlices[name];
+ } else {
+ if (state.openSlices.length == 0) {
+ // Ignore E events that are unmatched.
+ return;
+ }
+ var slice = state.openSlices.pop().slice;
+ slice.duration = (event.ts / 1000) - slice.start;
+ if (event.uts)
+ slice.durationInUserTime = (event.uts / 1000) - slice.startInUserTime;
+
+ // Store the slice on the correct subrow.
+ var thread = this.model_.getOrCreateProcess(event.pid).
+ getOrCreateThread(event.tid);
+ var subRowIndex = state.openSlices.length;
+ thread.getSubrow(subRowIndex).push(slice);
+
+ // Add the slice to the subSlices array of its parent.
+ if (state.openSlices.length) {
+ var parentSlice = state.openSlices[state.openSlices.length - 1];
+ parentSlice.slice.subSlices.push(slice);
+ }
+ }
+ },
+
+ /**
+ * Helper function that closes any open slices. This happens when a trace
+ * ends before an 'E' phase event can get posted. When that happens, this
+ * closes the slice at the highest timestamp we recorded and sets the
+ * didNotFinish flag to true.
+ */
+ autoCloseOpenSlices: function() {
+ // We need to know the model bounds in order to assign an end-time to
+ // the open slices.
+ this.model_.updateBounds();
+
+ // The model's max value in the trace is wrong at this point if there are
+ // un-closed events. To close those events, we need the true global max
+ // value. To compute this, build a list of timestamps that weren't
+ // included in the max calculation, then compute the real maximum based
+ // on that.
+ var openTimestamps = [];
+ for (var ptid in this.threadStateByPTID_) {
+ var state = this.threadStateByPTID_[ptid];
+ for (var i = 0; i < state.openSlices.length; i++) {
+ var slice = state.openSlices[i];
+ openTimestamps.push(slice.slice.start);
+ for (var s = 0; s < slice.slice.subSlices.length; s++) {
+ var subSlice = slice.slice.subSlices[s];
+ openTimestamps.push(subSlice.start);
+ if (subSlice.duration)
+ openTimestamps.push(subSlice.end);
+ }
+ }
+ }
+
+ // Figure out the maximum value of model.maxTimestamp and
+ // Math.max(openTimestamps). Made complicated by the fact that the model
+ // timestamps might be undefined.
+ var realMaxTimestamp;
+ if (this.model_.maxTimestamp) {
+ realMaxTimestamp = Math.max(this.model_.maxTimestamp,
+ Math.max.apply(Math, openTimestamps));
+ } else {
+ realMaxTimestamp = Math.max.apply(Math, openTimestamps);
+ }
+
+ // Automatically close any slices are still open. These occur in a number
+ // of reasonable situations, e.g. deadlock. This pass ensures the open
+ // slices make it into the final model.
+ for (var ptid in this.threadStateByPTID_) {
+ var state = this.threadStateByPTID_[ptid];
+ while (state.openSlices.length > 0) {
+ var slice = state.openSlices.pop();
+ slice.slice.duration = realMaxTimestamp - slice.slice.start;
+ slice.slice.didNotFinish = true;
+ var event = this.events_[slice.index];
+
+ // Store the slice on the correct subrow.
+ var thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+ var subRowIndex = state.openSlices.length;
+ thread.getSubrow(subRowIndex).push(slice.slice);
+
+ // Add the slice to the subSlices array of its parent.
+ if (state.openSlices.length) {
+ var parentSlice = state.openSlices[state.openSlices.length - 1];
+ parentSlice.slice.subSlices.push(slice.slice);
+ }
+ }
+ }
+ },
+
+ /**
+ * Helper that creates and adds samples to a TimelineCounter object based on
+ * 'C' phase events.
+ */
+ processCounter: 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.seriesNames.push(seriesName);
+ ctr.seriesColors.push(
+ tracing.getStringColorId(ctr.name + '.' + seriesName));
+ }
+ if (ctr.numSeries == 0) {
+ this.model_.importErrors.push('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;
+ }
+ }
+
+ // Add the sample values.
+ ctr.timestamps.push(event.ts / 1000);
+ for (var i = 0; i < ctr.numSeries; i++) {
+ var seriesName = ctr.seriesNames[i];
+ if (event.args[seriesName] === undefined) {
+ ctr.samples.push(0);
+ continue;
+ }
+ ctr.samples.push(event.args[seriesName]);
+ }
+ },
+
+ /**
+ * Walks through the events_ list and outputs the structures discovered to
+ * model_.
+ */
+ importEvents: function() {
+ // Walk through events
+ var events = this.events_;
+ for (var eI = 0; eI < events.length; eI++) {
+ var event = events[eI];
+ var ptid = event.pid + ':' + event.tid;
+
+ if (!(ptid in this.threadStateByPTID_))
+ this.threadStateByPTID_[ptid] = new ThreadState();
+ var state = this.threadStateByPTID_[ptid];
+
+ if (event.ph == 'B') {
+ this.processBegin(eI, state, event);
+ } else if (event.ph == 'E') {
+ this.processEnd(state, event);
+ } else if (event.ph == 'I') {
+ // Treat an Instant event as a duration 0 slice.
+ // TimelineSliceTrack's redraw() knows how to handle this.
+ this.processBegin(eI, state, event);
+ this.processEnd(state, event);
+ } else if (event.ph == 'C') {
+ this.processCounter(event);
+ } else if (event.ph == 'M') {
+ if (event.name == 'thread_name') {
+ var thread = this.model_.getOrCreateProcess(event.pid)
+ .getOrCreateThread(event.tid);
+ thread.name = event.args.name;
+ } else {
+ this.model_.importErrors.push(
+ 'Unrecognized metadata name: ' + event.name);
+ }
+ } else {
+ this.model_.importErrors.push(
+ 'Unrecognized event phase: ' + event.ph +
+ '(' + event.name + ')');
+ }
+ }
+
+ // Autoclose any open slices.
+ var hasOpenSlices = false;
+ for (var ptid in this.threadStateByPTID_) {
+ var state = this.threadStateByPTID_[ptid];
+ hasOpenSlices |= state.openSlices.length > 0;
+ }
+ if (hasOpenSlices)
+ this.autoCloseOpenSlices();
+ }
+ };
+
+ tracing.TimelineModel.registerImporter(TraceEventImporter);
+
+ return {
+ TraceEventImporter: TraceEventImporter
+ };
+});
+// Copyright (c) 2011 The Chromium 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 Helper functions for doing intersections and iteration
+ * over sorted arrays and intervals.
+ *
+ */
+cr.define('tracing', 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():*} mapLoFn 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]))) {
+ return 0;
+ } else {
+ return -1;
+ }
+ } else if (first <= ary.length &&
+ loVal >= mapLoFn(ary[first - 1]) &&
+ loVal < mapLoFn(ary[first - 1]) + mapWidthFn(ary[first - 1])) {
+ return first - 1;
+ } 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 (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]);
+ 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;
+ }
+
+ return {
+ findLowIndexInSortedArray: findLowIndexInSortedArray,
+ findLowIndexInSortedIntervals: findLowIndexInSortedIntervals,
+ iterateOverIntersectingIntervals: iterateOverIntersectingIntervals,
+ getIntersectingIntervals: getIntersectingIntervals
+ };
+});
+// Copyright (c) 2011 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+cr.define('tracing', function() {
+ /**
+ * Uses an embedded iframe to measure provided elements without forcing layout
+ * on the main document.
+ * @constructor
+ * @extends {Object}
+ */
+ function MeasuringStick() {
+ var iframe = document.createElement('iframe');
+ iframe.style.cssText = 'width:100%;height:0;border:0;visibility:hidden';
+ document.body.appendChild(iframe);
+ this._doc = iframe.contentDocument;
+ this._window = iframe.contentWindow;
+ this._doc.body.style.cssText = 'padding:0;margin:0;overflow:hidden';
+
+ var stylesheets = document.querySelectorAll('link[rel=stylesheet]');
+ for (var i = 0; i < stylesheets.length; i++) {
+ var stylesheet = stylesheets[i];
+ var link = this._doc.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = stylesheet.href;
+ this._doc.head.appendChild(link);
+ }
+ }
+
+ MeasuringStick.prototype = {
+ __proto__: Object.prototype,
+
+ /**
+ * Measures the provided element without forcing layout on the main
+ * document.
+ */
+ measure: function(element) {
+ this._doc.body.appendChild(element);
+ var style = this._window.getComputedStyle(element);
+ var width = parseInt(style.width, 10);
+ var height = parseInt(style.height, 10);
+ this._doc.body.removeChild(element);
+ return { width: width, height: height };
+ }
+ };
+
+ return {
+ MeasuringStick: MeasuringStick
+ };
+});
+// Copyright (c) 2011 The Chromium 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 Interactive visualizaiton of TimelineModel objects
+ * based loosely on gantt charts. Each thread in the TimelineModel is given a
+ * set of TimelineTracks, one per subrow in the thread. The Timeline class
+ * acts as a controller, creating the individual tracks, while TimelineTracks
+ * do actual drawing.
+ *
+ * Visually, the Timeline produces (prettier) visualizations like the following:
+ * Thread1: AAAAAAAAAA AAAAA
+ * BBBB BB
+ * Thread2: CCCCCC CCCCC
+ *
+ */
+cr.define('tracing', function() {
+
+ /**
+ * 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 view space,
+ * as well as the math for centering the viewport in various interesting
+ * ways.
+ *
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+ function TimelineViewport(parentEl) {
+ this.parentEl_ = parentEl;
+ this.scaleX_ = 1;
+ this.panX_ = 0;
+ this.gridTimebase_ = 0;
+ this.gridStep_ = 1000 / 60;
+ this.gridEnabled_ = false;
+ this.hasCalledSetupFunction_ = false;
+
+ this.onResizeBoundToThis_ = this.onResize_.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);
+ }
+
+ TimelineViewport.prototype = {
+ __proto__: cr.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 isAttachedToDocument_() {
+ var cur = this.parentEl_;
+ while (cur.parentNode)
+ cur = cur.parentNode;
+ return cur == this.parentEl_.ownerDocument;
+ },
+
+ 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.isAttachedToDocument_ || 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.onResizeBoundToThis_);
+ }
+
+ var curSize = this.clientWidth + 'x' + this.clientHeight;
+ if (this.pendingSetFunction_) {
+ this.lastSize_ = curSize;
+ this.pendingSetFunction_();
+ 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() {
+ cr.dispatchSimpleEvent(this, 'change');
+ },
+
+ detach: function() {
+ if (this.checkForAttachInterval_) {
+ window.clearInterval(this.checkForAttachInterval_);
+ this.checkForAttachInterval_ = undefined;
+ }
+ this.iframe_.removeEventListener('resize', this.onResizeBoundToThis_);
+ this.parentEl_.removeChild(this.iframe_);
+ },
+
+ get scaleX() {
+ return this.scaleX_;
+ },
+ set scaleX(s) {
+ var changed = this.scaleX_ != s;
+ if (changed) {
+ this.scaleX_ = s;
+ this.dispatchChangeEvent();
+ }
+ },
+
+ get panX() {
+ return this.panX_;
+ },
+ set panX(p) {
+ var changed = this.panX_ != p;
+ if (changed) {
+ this.panX_ = p;
+ this.dispatchChangeEvent();
+ }
+ },
+
+ setPanAndScale: function(p, s) {
+ var changed = this.scaleX_ != s || this.panX_ != p;
+ if (changed) {
+ this.scaleX_ = s;
+ this.panX_ = p;
+ this.dispatchChangeEvent();
+ }
+ },
+
+ 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_;
+ },
+
+ 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 Error('unrecognized string for viewPos. left|center|right');
+ }
+ }
+ this.panX = (viewX / this.scaleX_) - worldX;
+ },
+
+ 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;
+ cr.dispatchSimpleEvent(this, 'change');
+ },
+
+ get gridStep() {
+ return this.gridStep_;
+ },
+
+ applyTransformToCanavs: function(ctx) {
+ ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0);
+ }
+ };
+
+ /**
+ * Renders a TimelineModel into a div element, making one
+ * TimelineTrack for each subrow in each thread of the model, managing
+ * overall track layout, and handling user interaction with the
+ * viewport.
+ *
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ Timeline = cr.ui.define('div');
+
+ Timeline.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ model_: null,
+
+ decorate: function() {
+ this.classList.add('timeline');
+
+ this.viewport_ = new TimelineViewport(this);
+
+ this.tracks_ = this.ownerDocument.createElement('div');
+ this.appendChild(this.tracks_);
+
+ this.dragBox_ = this.ownerDocument.createElement('div');
+ this.dragBox_.className = 'timeline-drag-box';
+ this.appendChild(this.dragBox_);
+ this.hideDragBox_();
+
+ this.bindEventListener_(document, 'keypress', this.onKeypress_, this);
+ this.bindEventListener_(document, 'keydown', this.onKeydown_, this);
+ this.bindEventListener_(document, 'mousedown', this.onMouseDown_, this);
+ this.bindEventListener_(document, 'mousemove', this.onMouseMove_, this);
+ this.bindEventListener_(document, 'mouseup', this.onMouseUp_, this);
+ this.bindEventListener_(document, 'dblclick', this.onDblClick_, this);
+
+ this.lastMouseViewPos_ = {x: 0, y: 0};
+
+ this.selection_ = [];
+ },
+
+ /**
+ * 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);
+ },
+
+ detach: function() {
+ for (var i = 0; i < this.tracks_.children.length; i++)
+ this.tracks_.children[i].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 Error('Model cannot be null');
+ if (this.model) {
+ throw Error('Cannot set model twice.');
+ }
+ this.model_ = model;
+
+ // Figure out all the headings.
+ var allHeadings = [];
+ model.getAllThreads().forEach(function(t) {
+ allHeadings.push(t.userFriendlyName);
+ });
+ model.getAllCounters().forEach(function(c) {
+ allHeadings.push(c.name);
+ });
+ model.getAllCpus().forEach(function(c) {
+ allHeadings.push('CPU ' + c.cpuNumber);
+ });
+
+ // Figure out the maximum heading size.
+ var maxHeadingWidth = 0;
+ var measuringStick = new tracing.MeasuringStick();
+ var headingEl = document.createElement('div');
+ headingEl.style.position = 'fixed';
+ headingEl.className = 'timeline-canvas-based-track-title';
+ allHeadings.forEach(function(text) {
+ headingEl.textContent = text + ':__';
+ var w = measuringStick.measure(headingEl).width;
+ // Limit heading width to 300px.
+ if (w > 300)
+ w = 300;
+ if (w > maxHeadingWidth)
+ maxHeadingWidth = w;
+ });
+ maxHeadingWidth = maxHeadingWidth + 'px';
+
+ // Reset old tracks.
+ for (var i = 0; i < this.tracks_.children.length; i++)
+ this.tracks_.children[i].detach();
+ this.tracks_.textContent = '';
+
+ // Get a sorted list of CPUs
+ var cpus = model.getAllCpus();
+ cpus.sort(tracing.TimelineCpu.compare);
+
+ // Create tracks for each CPU.
+ cpus.forEach(function(cpu) {
+ var track = new tracing.TimelineCpuTrack();
+ track.heading = 'CPU ' + cpu.cpuNumber + ':';
+ track.headingWidth = maxHeadingWidth;
+ track.viewport = this.viewport_;
+ track.cpu = cpu;
+ this.tracks_.appendChild(track);
+
+ for (var counterName in cpu.counters) {
+ var counter = cpu.counters[counterName];
+ track = new tracing.TimelineCounterTrack();
+ track.heading = 'CPU ' + cpu.cpuNumber + ' ' + counter.name + ':';
+ track.headingWidth = maxHeadingWidth;
+ track.viewport = this.viewport_;
+ track.counter = counter;
+ this.tracks_.appendChild(track);
+ }
+ }.bind(this));
+
+ // Get a sorted list of processes.
+ var processes = model.getAllProcesses();
+ processes.sort(tracing.TimelineProcess.compare);
+
+ // Create tracks for each process.
+ processes.forEach(function(process) {
+ // Add counter tracks for this process.
+ var counters = [];
+ for (var tid in process.counters)
+ counters.push(process.counters[tid]);
+ counters.sort(tracing.TimelineCounter.compare);
+
+ // Create the counters for this process.
+ counters.forEach(function(counter) {
+ var track = new tracing.TimelineCounterTrack();
+ track.heading = counter.name + ':';
+ track.headingWidth = maxHeadingWidth;
+ track.viewport = this.viewport_;
+ track.counter = counter;
+ this.tracks_.appendChild(track);
+ }.bind(this));
+
+ // Get a sorted list of threads.
+ var threads = [];
+ for (var tid in process.threads)
+ threads.push(process.threads[tid]);
+ threads.sort(tracing.TimelineThread.compare);
+
+ // Create the threads.
+ threads.forEach(function(thread) {
+ var track = new tracing.TimelineThreadTrack();
+ track.heading = thread.userFriendlyName + ':';
+ track.tooltip = thread.userFriendlyDetials;
+ track.headingWidth = maxHeadingWidth;
+ track.viewport = this.viewport_;
+ track.thread = thread;
+ this.tracks_.appendChild(track);
+ }.bind(this));
+ }.bind(this));
+
+ // Set up a reasonable viewport.
+ this.viewport_.setWhenPossible(function() {
+ var rangeTimestamp = this.model_.maxTimestamp -
+ this.model_.minTimestamp;
+ var w = this.firstCanvas.width;
+ var scaleX = w / rangeTimestamp;
+ var panX = -this.model_.minTimestamp;
+ this.viewport_.setPanAndScale(panX, scaleX);
+ }.bind(this));
+ },
+
+ /**
+ * @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 keybaord input.
+ */
+ set focusElement(value) {
+ this.focusElement_ = value;
+ },
+
+ get listenToKeys_() {
+ if (!this.focusElement_)
+ return true;
+ if (this.focusElement.tabIndex >= 0)
+ return document.activeElement == this.focusElement;
+ return true;
+ },
+
+ onKeypress_: function(e) {
+ var vp = this.viewport_;
+ if (!this.firstCanvas)
+ return;
+ if (!this.listenToKeys_)
+ return;
+ var viewWidth = this.firstCanvas.clientWidth;
+ var curMouseV, curCenterW;
+ switch (e.keyCode) {
+ case 101: // e
+ var vX = this.lastMouseViewPos_.x;
+ var wX = vp.xViewToWorld(this.lastMouseViewPos_.x);
+ var distFromCenter = vX - (viewWidth / 2);
+ var percFromCenter = distFromCenter / viewWidth;
+ var percFromCenterSq = percFromCenter * percFromCenter;
+ vp.xPanWorldPosToViewPos(wX, 'center', viewWidth);
+ break;
+ case 119: // w
+ this.zoomBy_(1.5);
+ break;
+ case 115: // s
+ this.zoomBy_(1 / 1.5);
+ break;
+ case 103: // g
+ this.onGridToggle_(true);
+ break;
+ case 71: // G
+ this.onGridToggle_(false);
+ break;
+ case 87: // W
+ this.zoomBy_(10);
+ break;
+ case 83: // S
+ this.zoomBy_(1 / 10);
+ break;
+ case 97: // a
+ vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1);
+ break;
+ case 100: // d
+ vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1);
+ break;
+ case 65: // A
+ vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5);
+ break;
+ case 68: // D
+ vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5);
+ break;
+ }
+ },
+
+ // Not all keys send a keypress.
+ onKeydown_: function(e) {
+ if (!this.listenToKeys_)
+ return;
+ switch (e.keyCode) {
+ case 37: // left arrow
+ this.selectPrevious_(e);
+ e.preventDefault();
+ break;
+ case 39: // right arrow
+ this.selectNext_(e);
+ e.preventDefault();
+ break;
+ case 9: // TAB
+ if (this.focusElement.tabIndex == -1) {
+ if (e.shiftKey)
+ this.selectPrevious_(e);
+ else
+ this.selectNext_(e);
+ e.preventDefault();
+ }
+ break;
+ }
+ },
+
+ /**
+ * Zoom in or out on the timeline by the given scale factor.
+ * @param {integer} scale The scale factor to apply. If <1, zooms out.
+ */
+ zoomBy_: function(scale) {
+ if (!this.firstCanvas)
+ return;
+ var vp = this.viewport_;
+ var viewWidth = this.firstCanvas.clientWidth;
+ var curMouseV = this.lastMouseViewPos_.x;
+ var curCenterW = vp.xViewToWorld(curMouseV);
+ vp.scaleX = vp.scaleX * scale;
+ vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth);
+ },
+
+ /** Select the next slice on the timeline. Applies to each track. */
+ selectNext_: function(e) {
+ this.selectAdjoining_(e, true);
+ },
+
+ /** Select the previous slice on the timeline. Applies to each track. */
+ selectPrevious_: function(e) {
+ this.selectAdjoining_(e, false);
+ },
+
+ /**
+ * Helper for selection previous or next.
+ * @param {Event} The current event.
+ * @param {boolean} forwardp If true, select one forward (next).
+ * Else, select previous.
+ */
+ selectAdjoining_: function(e, forwardp) {
+ var i, track, slice, adjoining;
+ var selection = [];
+ // Clear old selection; try and select next.
+ for (i = 0; i < this.selection_.length; i++) {
+ adjoining = undefined;
+ this.selection_[i].slice.selected = false;
+ track = this.selection_[i].track;
+ slice = this.selection_[i].slice;
+ if (slice) {
+ if (forwardp)
+ adjoining = track.pickNext(slice);
+ else
+ adjoining = track.pickPrevious(slice);
+ }
+ if (adjoining != undefined)
+ selection.push({track: track, slice: adjoining});
+ }
+ this.selection = selection;
+ e.preventDefault();
+ },
+
+ get keyHelp() {
+ var help = 'Keyboard shortcuts:\n' +
+ ' w/s : Zoom in/out (with shift: go faster)\n' +
+ ' a/d : Pan left/right\n' +
+ ' e : Center on mouse\n' +
+ ' g/G : Shows grid at the start/end of the selected task\n';
+
+ if (this.focusElement.tabIndex) {
+ help += ' <- : Select previous event on current timeline\n' +
+ ' -> : Select next event on current timeline\n';
+ } else {
+ help += ' <-,^TAB : Select previous event on current timeline\n' +
+ ' ->, TAB : Select next event on current timeline\n';
+ }
+ help +=
+ '\n' +
+ 'Dbl-click to zoom in; Shift dbl-click to zoom out\n';
+ return help;
+ },
+
+ get selection() {
+ return this.selection_;
+ },
+
+ set selection(selection) {
+ // Clear old selection.
+ for (i = 0; i < this.selection_.length; i++)
+ this.selection_[i].slice.selected = false;
+
+ this.selection_ = selection;
+
+ cr.dispatchSimpleEvent(this, 'selectionChange');
+ for (i = 0; i < this.selection_.length; i++)
+ this.selection_[i].slice.selected = true;
+ this.viewport_.dispatchChangeEvent(); // Triggers a redraw.
+ },
+
+ get firstCanvas() {
+ return this.tracks_.firstChild ?
+ this.tracks_.firstChild.firstCanvas : undefined;
+ },
+
+ hideDragBox_: function() {
+ this.dragBox_.style.left = '-1000px';
+ this.dragBox_.style.top = '-1000px';
+ this.dragBox_.style.width = 0;
+ this.dragBox_.style.height = 0;
+ },
+
+ setDragBoxPosition_: function(eDown, eCur) {
+ var loX = Math.min(eDown.clientX, eCur.clientX);
+ var hiX = Math.max(eDown.clientX, eCur.clientX);
+ var loY = Math.min(eDown.clientY, eCur.clientY);
+ var hiY = Math.max(eDown.clientY, eCur.clientY);
+
+ this.dragBox_.style.left = loX + 'px';
+ this.dragBox_.style.top = loY + 'px';
+ this.dragBox_.style.width = hiX - loX + 'px';
+ this.dragBox_.style.height = hiY - loY + 'px';
+
+ var canv = this.firstCanvas;
+ var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft);
+ var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft);
+
+ var roundedDuration = Math.round((hiWX - loWX) * 100) / 100;
+ this.dragBox_.textContent = roundedDuration + 'ms';
+
+ var e = new cr.Event('selectionChanging');
+ e.loWX = loWX;
+ e.hiWX = hiWX;
+ this.dispatchEvent(e);
+ },
+
+ onGridToggle_: function(left) {
+ var tb;
+ if (left)
+ tb = Math.min.apply(Math, this.selection_.map(
+ function(x) { return x.slice.start; }));
+ else
+ tb = Math.max.apply(Math, this.selection_.map(
+ function(x) { return x.slice.end; }));
+
+ // Shift the timebase left until its just left of minTimestamp.
+ var numInterfvalsSinceStart = Math.ceil((tb - this.model_.minTimestamp) /
+ this.viewport_.gridStep_);
+ this.viewport_.gridTimebase = tb -
+ (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_;
+ this.viewport_.gridEnabled = true;
+ },
+
+ onMouseDown_: function(e) {
+ rect = this.tracks_.getClientRects()[0];
+ var inside = rect &&
+ e.clientX >= rect.left &&
+ e.clientX < rect.right &&
+ e.clientY >= rect.top &&
+ e.clientY < rect.bottom;
+ if (!inside)
+ return;
+
+ var canv = this.firstCanvas;
+ var pos = {
+ x: e.clientX - canv.offsetLeft,
+ y: e.clientY - canv.offsetTop
+ };
+
+ var wX = this.viewport_.xViewToWorld(pos.x);
+
+ this.dragBeginEvent_ = e;
+ e.preventDefault();
+ if (this.focusElement.tabIndex >= 0)
+ this.focusElement.focus();
+ },
+
+ onMouseMove_: function(e) {
+ if (!this.firstCanvas)
+ return;
+ var canv = this.firstCanvas;
+ var pos = {
+ x: e.clientX - canv.offsetLeft,
+ y: e.clientY - canv.offsetTop
+ };
+
+ // Remember position. Used during keyboard zooming.
+ this.lastMouseViewPos_ = pos;
+
+ // Update the drag box
+ if (this.dragBeginEvent_) {
+ this.setDragBoxPosition_(this.dragBeginEvent_, e);
+ }
+ },
+
+ onMouseUp_: function(e) {
+ var i;
+ if (this.dragBeginEvent_) {
+ // Stop the dragging.
+ this.hideDragBox_();
+ var eDown = this.dragBeginEvent_;
+ this.dragBeginEvent_ = null;
+
+ // Figure out extents of the drag.
+ var loX = Math.min(eDown.clientX, e.clientX);
+ var hiX = Math.max(eDown.clientX, e.clientX);
+ var loY = Math.min(eDown.clientY, e.clientY);
+ var hiY = Math.max(eDown.clientY, e.clientY);
+
+ // Convert to worldspace.
+ var canv = this.firstCanvas;
+ var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft);
+ var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft);
+
+ // Figure out what has been hit.
+ var selection = [];
+ function addHit(type, track, slice) {
+ selection.push({track: track, slice: slice});
+ }
+ for (i = 0; i < this.tracks_.children.length; i++) {
+ var track = this.tracks_.children[i];
+
+ // Only check tracks that insersect the rect.
+ var trackClientRect = track.getBoundingClientRect();
+ var a = Math.max(loY, trackClientRect.top);
+ var b = Math.min(hiY, trackClientRect.bottom);
+ if (a <= b) {
+ track.pickRange(loWX, hiWX, loY, hiY, addHit);
+ }
+ }
+ // Activate the new selection.
+ this.selection = selection;
+ }
+ },
+
+ onDblClick_: function(e) {
+ var scale = 4;
+ if (e.shiftKey)
+ scale = 1 / scale;
+ this.zoomBy_(scale);
+ e.preventDefault();
+ }
+ };
+
+ /**
+ * The TimelineModel being viewed by the timeline
+ * @type {TimelineModel}
+ */
+ cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS);
+
+ return {
+ Timeline: Timeline,
+ TimelineViewport: TimelineViewport
+ };
+});
+// Copyright (c) 2011 The Chromium 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 Renders an array of slices into the provided div,
+ * using a child canvas element. Uses a FastRectRenderer to draw only
+ * the visible slices.
+ */
+cr.define('tracing', function() {
+
+ var pallette = tracing.getPallette();
+ var highlightIdBoost = tracing.getPalletteHighlightIdBoost();
+
+ var textWidthMap = { };
+ function quickMeasureText(ctx, text) {
+ var w = textWidthMap[text];
+ if (!w) {
+ w = ctx.measureText(text).width;
+ textWidthMap[text] = w;
+ }
+ return w;
+ }
+
+ /**
+ * A generic track that contains other tracks as its children.
+ * @constructor
+ */
+ var TimelineContainerTrack = cr.ui.define('div');
+ TimelineContainerTrack.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate: function() {
+ this.tracks_ = [];
+ },
+
+ detach: function() {
+ for (var i = 0; i < this.tracks_.length; i++)
+ this.tracks_[i].detach();
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ set viewport(v) {
+ this.viewport_ = v;
+ for (var i = 0; i < this.tracks_.length; i++)
+ this.tracks_[i].viewport = v;
+ this.updateChildTracks_();
+ },
+
+ get firstCanvas() {
+ if (this.tracks_.length)
+ return this.tracks_[0].firstCanvas;
+ return undefined;
+ },
+
+ /**
+ * Picks a slice, if any, at a given location.
+ * @param {number} wX X location to search at, in worldspace.
+ * @param {number} wY Y location to search at, in offset space.
+ * offset space.
+ * @param {function():*} onHitCallback Callback to call with the slice,
+ * if one is found.
+ * @return {boolean} true if a slice was found, otherwise false.
+ */
+ pick: function(wX, wY, onHitCallback) {
+ for (var i = 0; i < this.tracks_.length; i++) {
+ var trackClientRect = this.tracks_[i].getBoundingClientRect();
+ if (wY >= trackClientRect.top && wY < trackClientRect.bottom)
+ return this.tracks_[i].pick(wX, onHitCallback);
+ }
+ return false;
+ },
+
+ /**
+ * Finds slices intersecting the given interval.
+ * @param {number} loWX Lower X bound of the interval to search, in
+ * worldspace.
+ * @param {number} hiWX Upper X bound of the interval to search, in
+ * worldspace.
+ * @param {number} loY Lower Y bound of the interval to search, in
+ * offset space.
+ * @param {number} hiY Upper Y bound of the interval to search, in
+ * offset space.
+ * @param {function():*} onHitCallback Function to call for each slice
+ * intersecting the interval.
+ */
+ pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) {
+ 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].pickRange(loWX, hiWX, loY, hiY, onHitCallback);
+ }
+ }
+ };
+
+ /**
+ * Visualizes a TimelineThread using a series of of TimelineSliceTracks.
+ * @constructor
+ */
+ var TimelineThreadTrack = cr.ui.define(TimelineContainerTrack);
+ TimelineThreadTrack.prototype = {
+ __proto__: TimelineContainerTrack.prototype,
+
+ decorate: function() {
+ this.classList.add('timeline-thread-track');
+ },
+
+ get thread(thread) {
+ return this.thread_;
+ },
+
+ set thread(thread) {
+ this.thread_ = thread;
+ this.updateChildTracks_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(value) {
+ this.tooltip_ = value;
+ this.updateChildTracks_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(h) {
+ this.heading_ = h;
+ this.updateChildTracks_();
+ },
+
+ get headingWidth() {
+ return this.headingWidth_;
+ },
+
+ set headingWidth(width) {
+ this.headingWidth_ = width;
+ this.updateChildTracks_();
+ },
+
+ addTrack_: function(slices) {
+ var track = new TimelineSliceTrack();
+ track.heading = '';
+ track.slices = slices;
+ track.headingWidth = this.headingWidth_;
+ track.viewport = this.viewport_;
+
+ this.tracks_.push(track);
+ this.appendChild(track);
+ return track;
+ },
+
+ updateChildTracks_: function() {
+ this.detach();
+ this.textContent = '';
+ this.tracks_ = [];
+ if (this.thread_) {
+ if (this.thread_.cpuSlices) {
+ var track = this.addTrack_(this.thread_.cpuSlices);
+ track.height = '4px';
+ }
+
+ for (var srI = 0; srI < this.thread_.nonNestedSubRows.length; ++srI) {
+ this.addTrack_(this.thread_.nonNestedSubRows[srI]);
+ }
+ for (var srI = 0; srI < this.thread_.subRows.length; ++srI) {
+ this.addTrack_(this.thread_.subRows[srI]);
+ }
+ if (this.tracks_.length > 0) {
+ if (this.thread_.cpuSlices) {
+ this.tracks_[1].heading = this.heading_;
+ this.tracks_[1].tooltip = this.tooltip_;
+ } else {
+ this.tracks_[0].heading = this.heading_;
+ this.tracks_[0].tooltip = this.tooltip_;
+ }
+ }
+ }
+ }
+ };
+
+ /**
+ * Visualizes a TimelineCpu using a series of of TimelineSliceTracks.
+ * @constructor
+ */
+ var TimelineCpuTrack = cr.ui.define(TimelineContainerTrack);
+ TimelineCpuTrack.prototype = {
+ __proto__: TimelineContainerTrack.prototype,
+
+ decorate: function() {
+ this.classList.add('timeline-thread-track');
+ },
+
+ get cpu(cpu) {
+ return this.cpu_;
+ },
+
+ set cpu(cpu) {
+ this.cpu_ = cpu;
+ this.updateChildTracks_();
+ },
+
+ get tooltip() {
+ return this.tooltip_;
+ },
+
+ set tooltip(value) {
+ this.tooltip_ = value;
+ this.updateChildTracks_();
+ },
+
+ get heading() {
+ return this.heading_;
+ },
+
+ set heading(h) {
+ this.heading_ = h;
+ this.updateChildTracks_();
+ },
+
+ get headingWidth() {
+ return this.headingWidth_;
+ },
+
+ set headingWidth(width) {
+ this.headingWidth_ = width;
+ this.updateChildTracks_();
+ },
+
+ updateChildTracks_: function() {
+ this.detach();
+ this.textContent = '';
+ this.tracks_ = [];
+ if (this.cpu_) {
+ var track = new TimelineSliceTrack();
+ track.slices = this.cpu_.slices;
+ track.headingWidth = this.headingWidth_;
+ track.viewport = this.viewport_;
+
+ this.tracks_.push(track);
+ this.appendChild(track);
+
+ this.tracks_[0].heading = this.heading_;
+ this.tracks_[0].tooltip = this.tooltip_;
+ }
+ }
+ };
+
+ /**
+ * A canvas-based track constructed. Provides the basic heading and
+ * invalidation-managment infrastructure. Subclasses must implement drawing
+ * and picking code.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ var CanvasBasedTrack = cr.ui.define('div');
+
+ CanvasBasedTrack.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate: function() {
+ this.className = 'timeline-canvas-based-track';
+ this.slices_ = null;
+
+ this.headingDiv_ = document.createElement('div');
+ this.headingDiv_.className = 'timeline-canvas-based-track-title';
+ this.appendChild(this.headingDiv_);
+
+ this.canvasContainer_ = document.createElement('div');
+ this.canvasContainer_.className =
+ 'timeline-canvas-based-track-canvas-container';
+ this.appendChild(this.canvasContainer_);
+ this.canvas_ = document.createElement('canvas');
+ this.canvas_.className = 'timeline-canvas-based-track-canvas';
+ this.canvasContainer_.appendChild(this.canvas_);
+
+ this.ctx_ = this.canvas_.getContext('2d');
+ },
+
+ detach: function() {
+ if (this.viewport_)
+ this.viewport_.removeEventListener('change',
+ this.viewportChangeBoundToThis_);
+ },
+
+ set headingWidth(width) {
+ this.headingDiv_.style.width = width;
+ },
+
+ get heading() {
+ return this.headingDiv_.textContent;
+ },
+
+ set heading(text) {
+ this.headingDiv_.textContent = text;
+ },
+
+ set tooltip(text) {
+ this.headingDiv_.title = text;
+ },
+
+ get viewport() {
+ return this.viewport_;
+ },
+
+ set viewport(v) {
+ this.viewport_ = v;
+ if (this.viewport_)
+ this.viewport_.removeEventListener('change',
+ this.viewportChangeBoundToThis_);
+ this.viewport_ = v;
+ if (this.viewport_) {
+ this.viewportChangeBoundToThis_ = this.viewportChange_.bind(this);
+ this.viewport_.addEventListener('change',
+ this.viewportChangeBoundToThis_);
+ }
+ this.invalidate();
+ },
+
+ viewportChange_: function() {
+ this.invalidate();
+ },
+
+ invalidate: function() {
+ if (this.rafPending_)
+ return;
+ webkitRequestAnimationFrame(function() {
+ this.rafPending_ = false;
+ if (!this.viewport_)
+ return;
+
+ if (this.canvas_.width != this.canvasContainer_.clientWidth)
+ this.canvas_.width = this.canvasContainer_.clientWidth;
+ if (this.canvas_.height != this.canvasContainer_.clientHeight)
+ this.canvas_.height = this.canvasContainer_.clientHeight;
+
+ this.redraw();
+ }.bind(this), this);
+ this.rafPending_ = true;
+ },
+
+ get firstCanvas() {
+ return this.canvas_;
+ }
+
+ };
+
+ /**
+ * A track that displays an array of TimelineSlice objects.
+ * @constructor
+ * @extends {CanvasBasedTrack}
+ */
+
+ var TimelineSliceTrack = cr.ui.define(CanvasBasedTrack);
+
+ TimelineSliceTrack.prototype = {
+
+ __proto__: CanvasBasedTrack.prototype,
+
+ decorate: function() {
+ this.classList.add('timeline-slice-track');
+ },
+
+ get slices() {
+ return this.slices_;
+ },
+
+ set slices(slices) {
+ this.slices_ = slices;
+ this.invalidate();
+ },
+
+ set height(height) {
+ this.style.height = height;
+ },
+
+ redraw: function() {
+ var ctx = this.ctx_;
+ var canvasW = this.canvas_.width;
+ var canvasH = this.canvas_.height;
+
+ ctx.clearRect(0, 0, canvasW, canvasH);
+
+ // Culling parameters.
+ var vp = this.viewport_;
+ var pixWidth = vp.xViewVectorToWorld(1);
+ var viewLWorld = vp.xViewToWorld(0);
+ var viewRWorld = vp.xViewToWorld(canvasW);
+
+ // Draw grid without a transform because the scale
+ // affects line width.
+ if (vp.gridEnabled) {
+ var x = vp.gridTimebase;
+ ctx.beginPath();
+ while (x < viewRWorld) {
+ if (x >= viewLWorld) {
+ // Do conversion to viewspace here rather than on
+ // x to avoid precision issues.
+ var vx = vp.xWorldToView(x);
+ ctx.moveTo(vx, 0);
+ ctx.lineTo(vx, canvasH);
+ }
+ x += vp.gridStep;
+ }
+ ctx.strokeStyle = 'rgba(255,0,0,0.25)';
+ ctx.stroke();
+ }
+
+ // Begin rendering in world space.
+ ctx.save();
+ vp.applyTransformToCanavs(ctx);
+
+ // Slices.
+ var tr = new tracing.FastRectRenderer(ctx, viewLWorld, 2 * pixWidth,
+ 2 * pixWidth, viewRWorld, pallette);
+ tr.setYandH(0, canvasH);
+ var slices = this.slices_;
+ for (var i = 0; i < slices.length; ++i) {
+ var slice = slices[i];
+ var x = slice.start;
+ // Less than 0.001 causes short events to disappear when zoomed in.
+ var w = Math.max(slice.duration, 0.001);
+ var colorId = slice.selected ?
+ slice.colorId + highlightIdBoost :
+ slice.colorId;
+
+ if (w < pixWidth)
+ w = pixWidth;
+ if (slice.duration > 0) {
+ tr.fillRect(x, w, colorId);
+ } else {
+ // Instant: draw a triangle. If zoomed too far, collapse
+ // into the FastRectRenderer.
+ if (pixWidth > 0.001) {
+ tr.fillRect(x, pixWidth, colorId);
+ } else {
+ ctx.fillStyle = pallette[colorId];
+ ctx.beginPath();
+ ctx.moveTo(x - (4 * pixWidth), canvasH);
+ ctx.lineTo(x, 0);
+ ctx.lineTo(x + (4 * pixWidth), canvasH);
+ ctx.closePath();
+ ctx.fill();
+ }
+ }
+ }
+ tr.flush();
+ ctx.restore();
+
+ // Labels.
+ if (canvasH > 8) {
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'top';
+ ctx.font = '10px sans-serif';
+ ctx.strokeStyle = 'rgb(0,0,0)';
+ ctx.fillStyle = 'rgb(0,0,0)';
+ // Don't render text until until it is 20px wide
+ var quickDiscardThresshold = pixWidth * 20;
+ for (var i = 0; i < slices.length; ++i) {
+ var slice = slices[i];
+ if (slice.duration > quickDiscardThresshold) {
+ var title = slice.title;
+ if (slice.didNotFinish) {
+ title += ' (Did Not Finish)';
+ }
+ var labelWidth = quickMeasureText(ctx, title) + 2;
+ var labelWidthWorld = pixWidth * labelWidth;
+
+ if (labelWidthWorld < slice.duration) {
+ var cX = vp.xWorldToView(slice.start + 0.5 * slice.duration);
+ ctx.fillText(title, cX, 2.5, labelWidth);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Picks a slice, if any, at a given location.
+ * @param {number} wX X location to search at, in worldspace.
+ * @param {number} wY Y location to search at, in offset space.
+ * offset space.
+ * @param {function():*} onHitCallback Callback to call with the slice,
+ * if one is found.
+ * @return {boolean} true if a slice was found, otherwise false.
+ */
+ pick: function(wX, wY, onHitCallback) {
+ var clientRect = this.getBoundingClientRect();
+ if (wY < clientRect.top || wY >= clientRect.bottom)
+ return false;
+ var x = tracing.findLowIndexInSortedIntervals(this.slices_,
+ function(x) { return x.start; },
+ function(x) { return x.duration; },
+ wX);
+ if (x >= 0 && x < this.slices_.length) {
+ onHitCallback('slice', this, this.slices_[x]);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Finds slices intersecting the given interval.
+ * @param {number} loWX Lower X bound of the interval to search, in
+ * worldspace.
+ * @param {number} hiWX Upper X bound of the interval to search, in
+ * worldspace.
+ * @param {number} loY Lower Y bound of the interval to search, in
+ * offset space.
+ * @param {number} hiY Upper Y bound of the interval to search, in
+ * offset space.
+ * @param {function():*} onHitCallback Function to call for each slice
+ * intersecting the interval.
+ */
+ pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) {
+ var clientRect = this.getBoundingClientRect();
+ var a = Math.max(loY, clientRect.top);
+ var b = Math.min(hiY, clientRect.bottom);
+ if (a > b)
+ return;
+
+ var that = this;
+ function onPickHit(slice) {
+ onHitCallback('slice', that, slice);
+ }
+ tracing.iterateOverIntersectingIntervals(this.slices_,
+ function(x) { return x.start; },
+ function(x) { return x.duration; },
+ loWX, hiWX,
+ onPickHit);
+ },
+
+ /**
+ * Find the index for the given slice.
+ * @return {index} Index of the given slice, or undefined.
+ * @private
+ */
+ indexOfSlice_: function(slice) {
+ var index = tracing.findLowIndexInSortedArray(this.slices_,
+ function(x) { return x.start; },
+ slice.start);
+ while (index < this.slices_.length &&
+ slice.start == this.slices_[index].start &&
+ slice.colorId != this.slices_[index].colorId) {
+ index++;
+ }
+ return index < this.slices_.length ? index : undefined;
+ },
+
+ /**
+ * Return the next slice, if any, after the given slice.
+ * @param {slice} The previous slice.
+ * @return {slice} The next slice, or undefined.
+ * @private
+ */
+ pickNext: function(slice) {
+ var index = this.indexOfSlice_(slice);
+ if (index != undefined) {
+ if (index < this.slices_.length - 1)
+ index++;
+ else
+ index = undefined;
+ }
+ return index != undefined ? this.slices_[index] : undefined;
+ },
+
+ /**
+ * Return the previous slice, if any, before the given slice.
+ * @param {slice} A slice.
+ * @return {slice} The previous slice, or undefined.
+ */
+ pickPrevious: function(slice) {
+ var index = this.indexOfSlice_(slice);
+ if (index == 0)
+ return undefined;
+ else if ((index != undefined) && (index > 0))
+ index--;
+ return index != undefined ? this.slices_[index] : undefined;
+ }
+
+ };
+
+ /**
+ * A track that displays a TimelineCounter object.
+ * @constructor
+ * @extends {CanvasBasedTrack}
+ */
+
+ var TimelineCounterTrack = cr.ui.define(CanvasBasedTrack);
+
+ TimelineCounterTrack.prototype = {
+
+ __proto__: CanvasBasedTrack.prototype,
+
+ decorate: function() {
+ this.classList.add('timeline-counter-track');
+ },
+
+ get counter() {
+ return this.counter_;
+ },
+
+ set counter(counter) {
+ this.counter_ = counter;
+ this.invalidate();
+ },
+
+ redraw: function() {
+ var ctr = this.counter_;
+ var ctx = this.ctx_;
+ var canvasW = this.canvas_.width;
+ var canvasH = this.canvas_.height;
+
+ ctx.clearRect(0, 0, canvasW, canvasH);
+
+ // Culling parametrs.
+ var vp = this.viewport_;
+ var pixWidth = vp.xViewVectorToWorld(1);
+ var viewLWorld = vp.xViewToWorld(0);
+ var viewRWorld = vp.xViewToWorld(canvasW);
+
+ // Drop sampels that are less than skipDistancePix apart.
+ var skipDistancePix = 1;
+ var skipDistanceWorld = vp.xViewVectorToWorld(skipDistancePix);
+
+ // Begin rendering in world space.
+ ctx.save();
+ vp.applyTransformToCanavs(ctx);
+
+ // Figure out where drawing should begin.
+ var numSeries = ctr.numSeries;
+ var numSamples = ctr.numSamples;
+ var startIndex = tracing.findLowIndexInSortedArray(ctr.timestamps,
+ function() {
+ },
+ viewLWorld);
+
+ // Draw indices one by one until we fall off the viewRWorld.
+ var yScale = canvasH / ctr.maxTotal;
+ for (var seriesIndex = ctr.numSeries - 1;
+ seriesIndex >= 0; seriesIndex--) {
+ var colorId = ctr.seriesColors[seriesIndex];
+ ctx.fillStyle = pallette[colorId];
+ ctx.beginPath();
+
+ // Set iLast and xLast such that the first sample we draw is the
+ // startIndex sample.
+ var iLast = startIndex - 1;
+ var xLast = iLast >= 0 ? ctr.timestamps[iLast] - skipDistanceWorld : -1;
+ var yLastView = canvasH;
+
+ // Iterate over samples from iLast 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) {
+ var i = iLast + 1;
+ if (i >= numSamples) {
+ ctx.lineTo(xLast, yLastView);
+ ctx.lineTo(xLast + 8 * pixWidth, yLastView);
+ ctx.lineTo(xLast + 8 * pixWidth, canvasH);
+ break;
+ }
+
+ var x = ctr.timestamps[i];
+
+ var y = ctr.totals[i * numSeries + seriesIndex];
+ var yView = canvasH - (yScale * y);
+
+ if (x > viewRWorld) {
+ ctx.lineTo(x, yLastView);
+ ctx.lineTo(x, canvasH);
+ break;
+ }
+
+ if (x - xLast < skipDistanceWorld) {
+ iLast = i;
+ continue;
+ }
+
+ if (!hasMoved) {
+ ctx.moveTo(viewLWorld, canvasH);
+ hasMoved = true;
+ }
+ ctx.lineTo(x, yLastView);
+ ctx.lineTo(x, yView);
+ iLast = i;
+ xLast = x;
+ yLastView = yView;
+ }
+ ctx.closePath();
+ ctx.fill();
+ }
+ ctx.restore();
+ },
+
+ /**
+ * Picks a slice, if any, at a given location.
+ * @param {number} wX X location to search at, in worldspace.
+ * @param {number} wY Y location to search at, in offset space.
+ * offset space.
+ * @param {function():*} onHitCallback Callback to call with the slice,
+ * if one is found.
+ * @return {boolean} true if a slice was found, otherwise false.
+ */
+ pick: function(wX, wY, onHitCallback) {
+ },
+
+ /**
+ * Finds slices intersecting the given interval.
+ * @param {number} loWX Lower X bound of the interval to search, in
+ * worldspace.
+ * @param {number} hiWX Upper X bound of the interval to search, in
+ * worldspace.
+ * @param {number} loY Lower Y bound of the interval to search, in
+ * offset space.
+ * @param {number} hiY Upper Y bound of the interval to search, in
+ * offset space.
+ * @param {function():*} onHitCallback Function to call for each slice
+ * intersecting the interval.
+ */
+ pickRange: function(loWX, hiWX, loY, hiY, onHitCallback) {
+ }
+
+ };
+
+ return {
+ TimelineCounterTrack: TimelineCounterTrack,
+ TimelineSliceTrack: TimelineSliceTrack,
+ TimelineThreadTrack: TimelineThreadTrack,
+ TimelineCpuTrack: TimelineCpuTrack
+ };
+});
+// Copyright (c) 2011 The Chromium 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 TimelineView visualizes TRACE_EVENT events using the
+ * tracing.Timeline component.
+ */
+cr.define('tracing', function() {
+ function tsRound(ts) {
+ return Math.round(ts * 1000.0) / 1000.0;
+ }
+ function getPadding(text, width) {
+ width = width || 0;
+
+ if (typeof text != 'string')
+ text = String(text);
+
+ if (text.length >= width)
+ return '';
+
+ var pad = '';
+ for (var i = 0; i < width - text.length; i++)
+ pad += ' ';
+ return pad;
+ }
+
+ function leftAlign(text, width) {
+ return text + getPadding(text, width);
+ }
+
+ function rightAlign(text, width) {
+ return getPadding(text, width) + text;
+ }
+
+ /**
+ * TimelineView
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ TimelineView = cr.ui.define('div');
+
+ TimelineView.prototype = {
+ __proto__: HTMLDivElement.prototype,
+
+ decorate: function() {
+ this.classList.add('timeline-view');
+
+ this.timelineContainer_ = document.createElement('div');
+ this.timelineContainer_.className = 'timeline-container';
+
+ var summaryContainer_ = document.createElement('div');
+ summaryContainer_.className = 'summary-container';
+
+ this.summaryEl_ = document.createElement('pre');
+ this.summaryEl_.className = 'summary';
+
+ summaryContainer_.appendChild(this.summaryEl_);
+ this.appendChild(this.timelineContainer_);
+ this.appendChild(summaryContainer_);
+
+ this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
+ },
+
+ set traceData(traceData) {
+ this.model = new tracing.TimelineModel(traceData);
+ },
+
+ get model(model) {
+ return this.timelineModel_;
+ },
+
+ set model(model) {
+ this.timelineModel_ = model;
+
+ // remove old timeline
+ this.timelineContainer_.textContent = '';
+
+ // create new timeline if needed
+ if (this.timelineModel_.minTimestamp !== undefined) {
+ if (this.timeline_)
+ this.timeline_.detach();
+ this.timeline_ = new tracing.Timeline();
+ this.timeline_.model = this.timelineModel_;
+ this.timeline_.focusElement = this.parentElement;
+ this.timelineContainer_.appendChild(this.timeline_);
+ this.timeline_.addEventListener('selectionChange',
+ this.onSelectionChangedBoundToThis_);
+ this.onSelectionChanged_();
+ } else {
+ this.timeline_ = null;
+ }
+ },
+
+ get timeline() {
+ return this.timeline_;
+ },
+
+ onSelectionChanged_: function(e) {
+ var timeline = this.timeline_;
+ var selection = timeline.selection;
+ if (!selection.length) {
+ var oldScrollTop = this.timelineContainer_.scrollTop;
+ this.summaryEl_.textContent = timeline.keyHelp;
+ this.timelineContainer_.scrollTop = oldScrollTop;
+ return;
+ }
+
+ var text = '';
+ if (selection.length == 1) {
+ var c0Width = 14;
+ var slice = selection[0].slice;
+ text = 'Selected item:\n';
+ text += leftAlign('Title', c0Width) + ': ' + slice.title + '\n';
+ text += leftAlign('Start', c0Width) + ': ' +
+ tsRound(slice.start) + ' ms\n';
+ text += leftAlign('Duration', c0Width) + ': ' +
+ tsRound(slice.duration) + ' ms\n';
+ if (slice.durationInUserTime)
+ text += leftAlign('Duration (U)', c0Width) + ': ' +
+ tsRound(slice.durationInUserTime) + ' ms\n';
+
+ var n = 0;
+ for (var argName in slice.args) {
+ n += 1;
+ }
+ if (n > 0) {
+ text += leftAlign('Args', c0Width) + ':\n';
+ for (var argName in slice.args) {
+ var argVal = slice.args[argName];
+ text += leftAlign(' ' + argName, c0Width) + ': ' + argVal + '\n';
+ }
+ }
+ } else {
+ var c0Width = 55;
+ var c1Width = 12;
+ var c2Width = 5;
+ text = 'Selection summary:\n';
+ var tsLo = Math.min.apply(Math, selection.map(
+ function(s) {return s.slice.start;}));
+ var tsHi = Math.max.apply(Math, selection.map(
+ function(s) {return s.slice.end;}));
+
+ // compute total selection duration
+ var titles = selection.map(function(i) { return i.slice.title; });
+
+ var slicesByTitle = {};
+ for (var i = 0; i < selection.length; i++) {
+ var slice = selection[i].slice;
+ if (!slicesByTitle[slice.title])
+ slicesByTitle[slice.title] = {
+ slices: []
+ };
+ slicesByTitle[slice.title].slices.push(slice);
+ }
+ var totalDuration = 0;
+ for (var sliceGroupTitle in slicesByTitle) {
+ var sliceGroup = slicesByTitle[sliceGroupTitle];
+ var duration = 0;
+ for (i = 0; i < sliceGroup.slices.length; i++)
+ duration += sliceGroup.slices[i].duration;
+ totalDuration += duration;
+
+ text += ' ' +
+ leftAlign(sliceGroupTitle, c0Width) + ': ' +
+ rightAlign(tsRound(duration) + 'ms', c1Width) + ' ' +
+ rightAlign(String(sliceGroup.slices.length), c2Width) +
+ ' occurrences' + '\n';
+ }
+
+ text += leftAlign('*Totals', c0Width) + ' : ' +
+ rightAlign(tsRound(totalDuration) + 'ms', c1Width) + ' ' +
+ rightAlign(String(selection.length), c2Width) + ' occurrences' +
+ '\n';
+
+ text += '\n';
+
+ text += leftAlign('Selection start', c0Width) + ' : ' +
+ rightAlign(tsRound(tsLo) + 'ms', c1Width) +
+ '\n';
+ text += leftAlign('Selection extent', c0Width) + ' : ' +
+ rightAlign(tsRound(tsHi - tsLo) + 'ms', c1Width) +
+ '\n';
+ }
+
+ // done
+ var oldScrollTop = this.timelineContainer_.scrollTop;
+ this.summaryEl_.textContent = text;
+ this.timelineContainer_.scrollTop = oldScrollTop;
+ }
+ };
+
+ return {
+ TimelineView: TimelineView
+ };
+});
+// Copyright (c) 2011 The Chromium 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 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 is the max pallete index encountered.
+ *
+ * Make sure to flush the trackRenderer before finishing drawing in order
+ * to commit any queued drawing operations.
+ */
+cr.define('tracing', function() {
+
+ /**
+ * Creates a fast rect renderer with a specific set of culling rules
+ * and color pallette.
+ * @param {GraphicsContext2D} ctx Canvas2D drawing context.
+ * @param {number} vpLeft The leftmost visible part of the drawing viewport.
+ * @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 {number} vpRight The rightmost visible part of the viewport.
+ * @param {Array} pallette The color pallete for drawing. Pallette slots
+ * should map to valid Canvas fillStyle strings.
+ *
+ * @constructor
+ */
+ function FastRectRenderer(ctx, vpLeft, minRectSize, maxMergeDist, vpRight,
+ pallette) {
+ this.ctx_ = ctx;
+ this.vpLeft_ = vpLeft;
+ this.minRectSize_ = minRectSize;
+ this.maxMergeDist_ = maxMergeDist;
+ this.vpRight_ = vpRight;
+ this.pallette_ = pallette;
+ }
+
+ FastRectRenderer.prototype = {
+ y_: 0,
+ h_: 0,
+ merging_: false,
+ mergeStartX_: 0,
+ mergeCurRight_: 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.
+ */
+ fillRect: function(x, w, colorId) {
+ var r = x + w;
+ if (r < this.vpLeft_ || x > this.vpRight_) return;
+ 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;
+ } else {
+ this.mergeCurRight_ = r;
+ this.mergedColorId = Math.max(this.mergedColorId, colorId);
+ }
+ } else {
+ if (this.merging_)
+ this.flush();
+ this.ctx_.fillStyle = this.pallette_[colorId];
+ 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_.fillRect(this.mergeStartX_, this.y_,
+ this.mergeCurRight_ - this.mergeStartX_, this.h_);
+ this.merging_ = false;
+ }
+ }
+ };
+
+ return {
+ FastRectRenderer: FastRectRenderer
+ };
+
+});
+// Copyright (c) 2011 The Chromium 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 Helper functions for use in tracing tests.
+ */
+
+
+/**
+ * goog.testing.assertion's assertEquals tweaked to do equality-to-a-constant.
+ * @param {*} a First value.
+ * @param {*} b Second value.
+ */
+function assertAlmostEquals(a, b) {
+ _validateArguments(2, arguments);
+ var var1 = nonCommentArg(1, 2, arguments);
+ var var2 = nonCommentArg(2, 2, arguments);
+ _assert(commentArg(2, arguments), Math.abs(var1 - var2) < 0.00001,
+ 'Expected ' + _displayStringForValue(var1) + ' but was ' +
+ _displayStringForValue(var2));
+}
+
+cr.define('test_utils', function() {
+ function getAsync(url, cb) {
+ var req = new XMLHttpRequest();
+ req.open('GET', url, true);
+ req.onreadystatechange = function(aEvt) {
+ if (req.readyState == 4) {
+ window.setTimeout(function() {
+ if (req.status == 200) {
+ cb(req.responseText);
+ } else {
+ console.log('Failed to load ' + url);
+ }
+ }, 0);
+ }
+ };
+ req.send(null);
+ }
+ return {
+ getAsync: getAsync
+ };
+});
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..4116787
--- /dev/null
+++ b/style.css
@@ -0,0 +1 @@
+.timeline-drag-box{background-color:rgba(0,0,255,0.25);border:1px solid #000060 font-size:75%;position:fixed;}.timeline-thread-track{-webkit-box-orient:vertical;display:-webkit-box;padding:1px 0;}.timeline-thread-track:not(:first-child){border-top:1px solid #D0D0D0;}.timeline-canvas-based-track{-webkit-box-orient:horizontal;-webkit-box-align:stretch;background-color:white;display:-webkit-box;margin:0;padding:0;padding-right:5px;}.timeline-canvas-based-track-title{overflow:hidden;padding-right:5px;text-align:right;text-overflow:ellipsis;white-space:nowrap;}.timeline-canvas-based-track-canvas-container{-webkit-box-flex:1;width:100%;}.timeline-canvas-based-track-canvas{-webkit-box-flex:1;display:block;height:100%;width:100%;}.timeline-slice-track{height:18px;}.timeline-counter-track{height:30px;}.timeline-view{padding:0;-webkit-box-orient:vertical;-webkit-box-flex:1;display:-webkit-box;}.timeline-view>.timeline{display:-webkit-box;-webkit-box-flex:1;overflow:auto;}.timeline-view .timeline-container{-webkit-box-flex:1;display:-webkit-box;overflow:auto;}.timeline-view .timeline-container>*{-webkit-box-flex:1;}.timeline-view .summary-container *{-webkit-user-select:text;}.timeline-view .summary-container{border-top:1px solid black;max-height:250px;min-height:250px;font-family:monospace;overflow:auto;}.timeline-view .selection{margin:2px;}.timeline-view .selection ul{margin:0;}
\ No newline at end of file
diff --git a/systrace.py b/systrace.py
index ddaaa63..76cbb33 100755
--- a/systrace.py
+++ b/systrace.py
@@ -1,20 +1,8 @@
#!/usr/bin/env python
-#
-# Copyright 2012, The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
"""Android system-wide tracing utility.
@@ -58,9 +46,15 @@
else:
parser.error('the trace buffer size must be a positive number')
+ script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
+ css_filename = os.path.join(script_dir, 'style.css')
+ js_filename = os.path.join(script_dir, 'script.js')
+ css = open(css_filename).read()
+ js = open(js_filename).read()
+
html_filename = options.output_file
html_file = open(html_filename, 'w')
- html_file.write(html_prefix)
+ html_file.write(html_prefix % (css, js))
trace_started = False
leftovers = ''
@@ -107,22 +101,8 @@
<html>
<head i18n-values="dir:textdirection;">
<title>Android System Trace</title>
-<link rel="stylesheet" href="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/timeline.css">
-<link rel="stylesheet" href="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/timeline_view.css">
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/shared/js/cr.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/shared/js/cr/event_target.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/shared/js/cr/ui.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/shared/js/util.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/timeline_model.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/linux_perf_importer.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/trace_event_importer.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/sorted_array_utils.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/measuring_stick.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/timeline.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/timeline_track.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/timeline_view.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/fast_rect_renderer.js"></script>
-<script src="http://www.corp.google.com/~jgennis/android_tracing/0.1/viewer/test_utils.js"></script>
+<style type="text/css">%s</style>
+<script language="javascript">%s</script>
<style>
.view {
overflow: hidden;
diff --git a/update.sh b/update.sh
new file mode 100755
index 0000000..5d0df3d
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+CSS_FILES="
+ www/viewer/timeline.css
+ www/viewer/timeline_view.css
+"
+
+JS_FILES="
+ www/shared/js/cr.js
+ www/shared/js/cr/event_target.js
+ www/shared/js/cr/ui.js
+ www/shared/js/util.js
+ www/viewer/timeline_model.js
+ www/viewer/linux_perf_importer.js
+ www/viewer/trace_event_importer.js
+ www/viewer/sorted_array_utils.js
+ www/viewer/measuring_stick.js
+ www/viewer/timeline.js
+ www/viewer/timeline_track.js
+ www/viewer/timeline_view.js
+ www/viewer/fast_rect_renderer.js
+ www/viewer/test_utils.js
+"
+
+cat $CSS_FILES | yui-compressor --type css -o style.css
+if [ "$?" -ne 0 ]; then
+ echo "failed to update style.css"
+ exit
+else
+ echo "updated style.css"
+fi
+
+#cat $JS_FILES | yui-compressor --type js -o script.js
+cat $JS_FILES > script.js
+if [ "$?" -ne 0 ]; then
+ echo "failed to update script.js"
+ exit
+else
+ echo "updated script.js"
+fi
+