Add ENABLE_MEDIA_ROUTER define to builds other than Android and iOS.
[chromium-blink-merge.git] / chrome / browser / resources / profiler / profiler.js
blob9095872d3631a7e2aa539677d7152b97bdbc7c1a
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var g_browserBridge;
6 var g_mainView;
8 // TODO(eroman): The handling of "max" across snapshots is not correct.
9 // For starters the browser needs to be aware to generate new maximums.
10 // Secondly, we need to take into account the "max" of intermediary snapshots,
11 // not just the terminal ones.
13 /**
14 * Main entry point called once the page has loaded.
16 function onLoad() {
17 g_browserBridge = new BrowserBridge();
18 g_mainView = new MainView();
21 document.addEventListener('DOMContentLoaded', onLoad);
23 /**
24 * This class provides a "bridge" for communicating between the javascript and
25 * the browser. Used as a singleton.
27 var BrowserBridge = (function() {
28 'use strict';
30 /**
31 * @constructor
33 function BrowserBridge() {
36 BrowserBridge.prototype = {
37 //--------------------------------------------------------------------------
38 // Messages sent to the browser
39 //--------------------------------------------------------------------------
41 sendGetData: function() {
42 chrome.send('getData');
45 //--------------------------------------------------------------------------
46 // Messages received from the browser.
47 //--------------------------------------------------------------------------
49 receivedData: function(data) {
50 // TODO(eroman): The browser should give an indication of which snapshot
51 // this data belongs to. For now we always assume it is for the latest.
52 g_mainView.addDataToSnapshot(data);
56 return BrowserBridge;
57 })();
59 /**
60 * This class handles the presentation of our profiler view. Used as a
61 * singleton.
63 var MainView = (function() {
64 'use strict';
66 // --------------------------------------------------------------------------
67 // Important IDs in the HTML document
68 // --------------------------------------------------------------------------
70 // The search box to filter results.
71 var FILTER_SEARCH_ID = 'filter-search';
73 // The container node to put all the "Group by" dropdowns into.
74 var GROUP_BY_CONTAINER_ID = 'group-by-container';
76 // The container node to put all the "Sort by" dropdowns into.
77 var SORT_BY_CONTAINER_ID = 'sort-by-container';
79 // The DIV to put all the tables into.
80 var RESULTS_DIV_ID = 'results-div';
82 // The container node to put all the column (visibility) checkboxes into.
83 var COLUMN_TOGGLES_CONTAINER_ID = 'column-toggles-container';
85 // The container node to put all the column (merge) checkboxes into.
86 var COLUMN_MERGE_TOGGLES_CONTAINER_ID = 'column-merge-toggles-container';
88 // The anchor which toggles visibility of column checkboxes.
89 var EDIT_COLUMNS_LINK_ID = 'edit-columns-link';
91 // The container node to show/hide when toggling the column checkboxes.
92 var EDIT_COLUMNS_ROW = 'edit-columns-row';
94 // The checkbox which controls whether things like "Worker Threads" and
95 // "PAC threads" will be merged together.
96 var MERGE_SIMILAR_THREADS_CHECKBOX_ID = 'merge-similar-threads-checkbox';
98 var TOGGLE_SNAPSHOTS_LINK_ID = 'snapshots-link';
99 var SNAPSHOTS_ROW = 'snapshots-row';
100 var SNAPSHOT_SELECTION_SUMMARY_ID = 'snapshot-selection-summary';
101 var TAKE_SNAPSHOT_BUTTON_ID = 'take-snapshot-button';
103 var SAVE_SNAPSHOTS_BUTTON_ID = 'save-snapshots-button';
104 var SNAPSHOT_FILE_LOADER_ID = 'snapshot-file-loader';
105 var LOAD_ERROR_ID = 'file-load-error';
107 var DOWNLOAD_ANCHOR_ID = 'download-anchor';
109 // --------------------------------------------------------------------------
110 // Row keys
111 // --------------------------------------------------------------------------
113 // Each row of our data is an array of values rather than a dictionary. This
114 // avoids some overhead from repeating the key string multiple times, and
115 // speeds up the property accesses a bit. The following keys are well-known
116 // indexes into the array for various properties.
118 // Note that the declaration order will also define the default display order.
120 var BEGIN_KEY = 1; // Start at 1 rather than 0 to simplify sorting code.
121 var END_KEY = BEGIN_KEY;
123 var KEY_COUNT = END_KEY++;
124 var KEY_RUN_TIME = END_KEY++;
125 var KEY_AVG_RUN_TIME = END_KEY++;
126 var KEY_MAX_RUN_TIME = END_KEY++;
127 var KEY_QUEUE_TIME = END_KEY++;
128 var KEY_AVG_QUEUE_TIME = END_KEY++;
129 var KEY_MAX_QUEUE_TIME = END_KEY++;
130 var KEY_BIRTH_THREAD = END_KEY++;
131 var KEY_DEATH_THREAD = END_KEY++;
132 var KEY_PROCESS_TYPE = END_KEY++;
133 var KEY_PROCESS_ID = END_KEY++;
134 var KEY_FUNCTION_NAME = END_KEY++;
135 var KEY_SOURCE_LOCATION = END_KEY++;
136 var KEY_FILE_NAME = END_KEY++;
137 var KEY_LINE_NUMBER = END_KEY++;
139 var NUM_KEYS = END_KEY - BEGIN_KEY;
141 // --------------------------------------------------------------------------
142 // Aggregators
143 // --------------------------------------------------------------------------
145 // To generalize computing/displaying the aggregate "counts" for each column,
146 // we specify an optional "Aggregator" class to use with each property.
148 // The following are actually "Aggregator factories". They create an
149 // aggregator instance by calling 'create()'. The instance is then fed
150 // each row one at a time via the 'consume()' method. After all rows have
151 // been consumed, the 'getValueAsText()' method will return the aggregated
152 // value.
155 * This aggregator counts the number of unique values that were fed to it.
157 var UniquifyAggregator = (function() {
158 function Aggregator(key) {
159 this.key_ = key;
160 this.valuesSet_ = {};
163 Aggregator.prototype = {
164 consume: function(e) {
165 this.valuesSet_[e[this.key_]] = true;
168 getValueAsText: function() {
169 return getDictionaryKeys(this.valuesSet_).length + ' unique';
173 return {
174 create: function(key) { return new Aggregator(key); }
176 })();
179 * This aggregator sums a numeric field.
181 var SumAggregator = (function() {
182 function Aggregator(key) {
183 this.key_ = key;
184 this.sum_ = 0;
187 Aggregator.prototype = {
188 consume: function(e) {
189 this.sum_ += e[this.key_];
192 getValue: function() {
193 return this.sum_;
196 getValueAsText: function() {
197 return formatNumberAsText(this.getValue());
201 return {
202 create: function(key) { return new Aggregator(key); }
204 })();
207 * This aggregator computes an average by summing two
208 * numeric fields, and then dividing the totals.
210 var AvgAggregator = (function() {
211 function Aggregator(numeratorKey, divisorKey) {
212 this.numeratorKey_ = numeratorKey;
213 this.divisorKey_ = divisorKey;
215 this.numeratorSum_ = 0;
216 this.divisorSum_ = 0;
219 Aggregator.prototype = {
220 consume: function(e) {
221 this.numeratorSum_ += e[this.numeratorKey_];
222 this.divisorSum_ += e[this.divisorKey_];
225 getValue: function() {
226 return this.numeratorSum_ / this.divisorSum_;
229 getValueAsText: function() {
230 return formatNumberAsText(this.getValue());
234 return {
235 create: function(numeratorKey, divisorKey) {
236 return {
237 create: function(key) {
238 return new Aggregator(numeratorKey, divisorKey);
243 })();
246 * This aggregator finds the maximum for a numeric field.
248 var MaxAggregator = (function() {
249 function Aggregator(key) {
250 this.key_ = key;
251 this.max_ = -Infinity;
254 Aggregator.prototype = {
255 consume: function(e) {
256 this.max_ = Math.max(this.max_, e[this.key_]);
259 getValue: function() {
260 return this.max_;
263 getValueAsText: function() {
264 return formatNumberAsText(this.getValue());
268 return {
269 create: function(key) { return new Aggregator(key); }
271 })();
273 // --------------------------------------------------------------------------
274 // Key properties
275 // --------------------------------------------------------------------------
277 // Custom comparator for thread names (sorts main thread and IO thread
278 // higher than would happen lexicographically.)
279 var threadNameComparator =
280 createLexicographicComparatorWithExceptions([
281 'CrBrowserMain',
282 'Chrome_IOThread',
283 'Chrome_FileThread',
284 'Chrome_HistoryThread',
285 'Chrome_DBThread',
286 'Still_Alive',
289 function diffFuncForCount(a, b) {
290 return b - a;
293 function diffFuncForMax(a, b) {
294 return b;
298 * Enumerates information about various keys. Such as whether their data is
299 * expected to be numeric or is a string, a descriptive name (title) for the
300 * property, and what function should be used to aggregate the property when
301 * displayed in a column.
303 * --------------------------------------
304 * The following properties are required:
305 * --------------------------------------
307 * [name]: This is displayed as the column's label.
308 * [aggregator]: Aggregator factory that is used to compute an aggregate
309 * value for this column.
311 * --------------------------------------
312 * The following properties are optional:
313 * --------------------------------------
315 * [inputJsonKey]: The corresponding key for this property in the original
316 * JSON dictionary received from the browser. If this is
317 * present, values for this key will be automatically
318 * populated during import.
319 * [comparator]: A comparator function for sorting this column.
320 * [textPrinter]: A function that transforms values into the user-displayed
321 * text shown in the UI. If unspecified, will default to the
322 * "toString()" function.
323 * [cellAlignment]: The horizonal alignment to use for columns of this
324 * property (for instance 'right'). If unspecified will
325 * default to left alignment.
326 * [sortDescending]: When first clicking on this column, we will default to
327 * sorting by |comparator| in ascending order. If this
328 * property is true, we will reverse that to descending.
329 * [diff]: Function to call to compute a "difference" value between
330 * parameters (a, b). This is used when calculating the difference
331 * between two snapshots. Diffing numeric quantities generally
332 * involves subtracting, but some fields like max may need to do
333 * something different.
335 var KEY_PROPERTIES = [];
337 KEY_PROPERTIES[KEY_PROCESS_ID] = {
338 name: 'PID',
339 cellAlignment: 'right',
340 aggregator: UniquifyAggregator,
343 KEY_PROPERTIES[KEY_PROCESS_TYPE] = {
344 name: 'Process type',
345 aggregator: UniquifyAggregator,
348 KEY_PROPERTIES[KEY_BIRTH_THREAD] = {
349 name: 'Birth thread',
350 inputJsonKey: 'birth_thread',
351 aggregator: UniquifyAggregator,
352 comparator: threadNameComparator,
355 KEY_PROPERTIES[KEY_DEATH_THREAD] = {
356 name: 'Exec thread',
357 inputJsonKey: 'death_thread',
358 aggregator: UniquifyAggregator,
359 comparator: threadNameComparator,
362 KEY_PROPERTIES[KEY_FUNCTION_NAME] = {
363 name: 'Function name',
364 inputJsonKey: 'birth_location.function_name',
365 aggregator: UniquifyAggregator,
368 KEY_PROPERTIES[KEY_FILE_NAME] = {
369 name: 'File name',
370 inputJsonKey: 'birth_location.file_name',
371 aggregator: UniquifyAggregator,
374 KEY_PROPERTIES[KEY_LINE_NUMBER] = {
375 name: 'Line number',
376 cellAlignment: 'right',
377 inputJsonKey: 'birth_location.line_number',
378 aggregator: UniquifyAggregator,
381 KEY_PROPERTIES[KEY_COUNT] = {
382 name: 'Count',
383 cellAlignment: 'right',
384 sortDescending: true,
385 textPrinter: formatNumberAsText,
386 inputJsonKey: 'death_data.count',
387 aggregator: SumAggregator,
388 diff: diffFuncForCount,
391 KEY_PROPERTIES[KEY_QUEUE_TIME] = {
392 name: 'Total queue time',
393 cellAlignment: 'right',
394 sortDescending: true,
395 textPrinter: formatNumberAsText,
396 inputJsonKey: 'death_data.queue_ms',
397 aggregator: SumAggregator,
398 diff: diffFuncForCount,
401 KEY_PROPERTIES[KEY_MAX_QUEUE_TIME] = {
402 name: 'Max queue time',
403 cellAlignment: 'right',
404 sortDescending: true,
405 textPrinter: formatNumberAsText,
406 inputJsonKey: 'death_data.queue_ms_max',
407 aggregator: MaxAggregator,
408 diff: diffFuncForMax,
411 KEY_PROPERTIES[KEY_RUN_TIME] = {
412 name: 'Total run time',
413 cellAlignment: 'right',
414 sortDescending: true,
415 textPrinter: formatNumberAsText,
416 inputJsonKey: 'death_data.run_ms',
417 aggregator: SumAggregator,
418 diff: diffFuncForCount,
421 KEY_PROPERTIES[KEY_AVG_RUN_TIME] = {
422 name: 'Avg run time',
423 cellAlignment: 'right',
424 sortDescending: true,
425 textPrinter: formatNumberAsText,
426 aggregator: AvgAggregator.create(KEY_RUN_TIME, KEY_COUNT),
429 KEY_PROPERTIES[KEY_MAX_RUN_TIME] = {
430 name: 'Max run time',
431 cellAlignment: 'right',
432 sortDescending: true,
433 textPrinter: formatNumberAsText,
434 inputJsonKey: 'death_data.run_ms_max',
435 aggregator: MaxAggregator,
436 diff: diffFuncForMax,
439 KEY_PROPERTIES[KEY_AVG_QUEUE_TIME] = {
440 name: 'Avg queue time',
441 cellAlignment: 'right',
442 sortDescending: true,
443 textPrinter: formatNumberAsText,
444 aggregator: AvgAggregator.create(KEY_QUEUE_TIME, KEY_COUNT),
447 KEY_PROPERTIES[KEY_SOURCE_LOCATION] = {
448 name: 'Source location',
449 type: 'string',
450 aggregator: UniquifyAggregator,
454 * Returns the string name for |key|.
456 function getNameForKey(key) {
457 var props = KEY_PROPERTIES[key];
458 if (props == undefined)
459 throw 'Did not define properties for key: ' + key;
460 return props.name;
464 * Ordered list of all keys. This is the order we generally want
465 * to display the properties in. Default to declaration order.
467 var ALL_KEYS = [];
468 for (var k = BEGIN_KEY; k < END_KEY; ++k)
469 ALL_KEYS.push(k);
471 // --------------------------------------------------------------------------
472 // Default settings
473 // --------------------------------------------------------------------------
476 * List of keys for those properties which we want to initially omit
477 * from the table. (They can be re-enabled by clicking [Edit columns]).
479 var INITIALLY_HIDDEN_KEYS = [
480 KEY_FILE_NAME,
481 KEY_LINE_NUMBER,
482 KEY_QUEUE_TIME,
486 * The ordered list of grouping choices to expose in the "Group by"
487 * dropdowns. We don't include the numeric properties, since they
488 * leads to awkward bucketing.
490 var GROUPING_DROPDOWN_CHOICES = [
491 KEY_PROCESS_TYPE,
492 KEY_PROCESS_ID,
493 KEY_BIRTH_THREAD,
494 KEY_DEATH_THREAD,
495 KEY_FUNCTION_NAME,
496 KEY_SOURCE_LOCATION,
497 KEY_FILE_NAME,
498 KEY_LINE_NUMBER,
502 * The ordered list of sorting choices to expose in the "Sort by"
503 * dropdowns.
505 var SORT_DROPDOWN_CHOICES = ALL_KEYS;
508 * The ordered list of all columns that can be displayed in the tables (not
509 * including whatever has been hidden via [Edit Columns]).
511 var ALL_TABLE_COLUMNS = ALL_KEYS;
514 * The initial keys to sort by when loading the page (can be changed later).
516 var INITIAL_SORT_KEYS = [-KEY_COUNT];
519 * The default sort keys to use when nothing has been specified.
521 var DEFAULT_SORT_KEYS = [-KEY_COUNT];
524 * The initial keys to group by when loading the page (can be changed later).
526 var INITIAL_GROUP_KEYS = [];
529 * The columns to give the option to merge on.
531 var MERGEABLE_KEYS = [
532 KEY_PROCESS_ID,
533 KEY_PROCESS_TYPE,
534 KEY_BIRTH_THREAD,
535 KEY_DEATH_THREAD,
539 * The columns to merge by default.
541 var INITIALLY_MERGED_KEYS = [];
544 * The full set of columns which define the "identity" for a row. A row is
545 * considered equivalent to another row if it matches on all of these
546 * fields. This list is used when merging the data, to determine which rows
547 * should be merged together. The remaining columns not listed in
548 * IDENTITY_KEYS will be aggregated.
550 var IDENTITY_KEYS = [
551 KEY_BIRTH_THREAD,
552 KEY_DEATH_THREAD,
553 KEY_PROCESS_TYPE,
554 KEY_PROCESS_ID,
555 KEY_FUNCTION_NAME,
556 KEY_SOURCE_LOCATION,
557 KEY_FILE_NAME,
558 KEY_LINE_NUMBER,
562 * The time (in milliseconds) to wait after receiving new data before
563 * re-drawing it to the screen. The reason we wait a bit is to avoid
564 * repainting repeatedly during the loading phase (which can slow things
565 * down). Note that this only slows down the addition of new data. It does
566 * not impact the latency of user-initiated operations like sorting or
567 * merging.
569 var PROCESS_DATA_DELAY_MS = 500;
572 * The initial number of rows to display (the rest are hidden) when no
573 * grouping is selected. We use a higher limit than when grouping is used
574 * since there is a lot of vertical real estate.
576 var INITIAL_UNGROUPED_ROW_LIMIT = 30;
579 * The initial number of rows to display (rest are hidden) for each group.
581 var INITIAL_GROUP_ROW_LIMIT = 10;
584 * The number of extra rows to show/hide when clicking the "Show more" or
585 * "Show less" buttons.
587 var LIMIT_INCREMENT = 10;
589 // --------------------------------------------------------------------------
590 // General utility functions
591 // --------------------------------------------------------------------------
594 * Returns a list of all the keys in |dict|.
596 function getDictionaryKeys(dict) {
597 var keys = [];
598 for (var key in dict) {
599 keys.push(key);
601 return keys;
605 * Formats the number |x| as a decimal integer. Strips off any decimal parts,
606 * and comma separates the number every 3 characters.
608 function formatNumberAsText(x) {
609 var orig = x.toFixed(0);
611 var parts = [];
612 for (var end = orig.length; end > 0; ) {
613 var chunk = Math.min(end, 3);
614 parts.push(orig.substr(end - chunk, chunk));
615 end -= chunk;
617 return parts.reverse().join(',');
621 * Simple comparator function which works for both strings and numbers.
623 function simpleCompare(a, b) {
624 if (a == b)
625 return 0;
626 if (a < b)
627 return -1;
628 return 1;
632 * Returns a comparator function that compares values lexicographically,
633 * but special-cases the values in |orderedList| to have a higher
634 * rank.
636 function createLexicographicComparatorWithExceptions(orderedList) {
637 var valueToRankMap = {};
638 for (var i = 0; i < orderedList.length; ++i)
639 valueToRankMap[orderedList[i]] = i;
641 function getCustomRank(x) {
642 var rank = valueToRankMap[x];
643 if (rank == undefined)
644 rank = Infinity; // Unmatched.
645 return rank;
648 return function(a, b) {
649 var aRank = getCustomRank(a);
650 var bRank = getCustomRank(b);
652 // Not matched by any of our exceptions.
653 if (aRank == bRank)
654 return simpleCompare(a, b);
656 if (aRank < bRank)
657 return -1;
658 return 1;
663 * Returns dict[key]. Note that if |key| contains periods (.), they will be
664 * interpreted as meaning a sub-property.
666 function getPropertyByPath(dict, key) {
667 var cur = dict;
668 var parts = key.split('.');
669 for (var i = 0; i < parts.length; ++i) {
670 if (cur == undefined)
671 return undefined;
672 cur = cur[parts[i]];
674 return cur;
678 * Creates and appends a DOM node of type |tagName| to |parent|. Optionally,
679 * sets the new node's text to |opt_text|. Returns the newly created node.
681 function addNode(parent, tagName, opt_text) {
682 var n = parent.ownerDocument.createElement(tagName);
683 parent.appendChild(n);
684 if (opt_text != undefined) {
685 addText(n, opt_text);
687 return n;
691 * Adds |text| to |parent|.
693 function addText(parent, text) {
694 var textNode = parent.ownerDocument.createTextNode(text);
695 parent.appendChild(textNode);
696 return textNode;
700 * Deletes all the strings in |array| which appear in |valuesToDelete|.
702 function deleteValuesFromArray(array, valuesToDelete) {
703 var valueSet = arrayToSet(valuesToDelete);
704 for (var i = 0; i < array.length; ) {
705 if (valueSet[array[i]]) {
706 array.splice(i, 1);
707 } else {
708 i++;
714 * Deletes all the repeated ocurrences of strings in |array|.
716 function deleteDuplicateStringsFromArray(array) {
717 // Build up set of each entry in array.
718 var seenSoFar = {};
720 for (var i = 0; i < array.length; ) {
721 var value = array[i];
722 if (seenSoFar[value]) {
723 array.splice(i, 1);
724 } else {
725 seenSoFar[value] = true;
726 i++;
732 * Builds a map out of the array |list|.
734 function arrayToSet(list) {
735 var set = {};
736 for (var i = 0; i < list.length; ++i)
737 set[list[i]] = true;
738 return set;
741 function trimWhitespace(text) {
742 var m = /^\s*(.*)\s*$/.exec(text);
743 return m[1];
747 * Selects the option in |select| which has a value of |value|.
749 function setSelectedOptionByValue(select, value) {
750 for (var i = 0; i < select.options.length; ++i) {
751 if (select.options[i].value == value) {
752 select.options[i].selected = true;
753 return true;
756 return false;
760 * Adds a checkbox to |parent|. The checkbox will have a label on its right
761 * with text |label|. Returns the checkbox input node.
763 function addLabeledCheckbox(parent, label) {
764 var labelNode = addNode(parent, 'label');
765 var checkbox = addNode(labelNode, 'input');
766 checkbox.type = 'checkbox';
767 addText(labelNode, label);
768 return checkbox;
772 * Return the last component in a path which is separated by either forward
773 * slashes or backslashes.
775 function getFilenameFromPath(path) {
776 var lastSlash = Math.max(path.lastIndexOf('/'),
777 path.lastIndexOf('\\'));
778 if (lastSlash == -1)
779 return path;
781 return path.substr(lastSlash + 1);
785 * Returns the current time in milliseconds since unix epoch.
787 function getTimeMillis() {
788 return (new Date()).getTime();
792 * Toggle a node between hidden/invisible.
794 function toggleNodeDisplay(n) {
795 if (n.style.display == '') {
796 n.style.display = 'none';
797 } else {
798 n.style.display = '';
803 * Set the visibility state of a node.
805 function setNodeDisplay(n, visible) {
806 if (visible) {
807 n.style.display = '';
808 } else {
809 n.style.display = 'none';
813 // --------------------------------------------------------------------------
814 // Functions that augment, bucket, and compute aggregates for the input data.
815 // --------------------------------------------------------------------------
818 * Adds new derived properties to row. Mutates the provided dictionary |e|.
820 function augmentDataRow(e) {
821 computeDataRowAverages(e);
822 e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']';
825 function computeDataRowAverages(e) {
826 e[KEY_AVG_QUEUE_TIME] = e[KEY_QUEUE_TIME] / e[KEY_COUNT];
827 e[KEY_AVG_RUN_TIME] = e[KEY_RUN_TIME] / e[KEY_COUNT];
831 * Creates and initializes an aggregator object for each key in |columns|.
832 * Returns an array whose keys are values from |columns|, and whose
833 * values are Aggregator instances.
835 function initializeAggregates(columns) {
836 var aggregates = [];
838 for (var i = 0; i < columns.length; ++i) {
839 var key = columns[i];
840 var aggregatorFactory = KEY_PROPERTIES[key].aggregator;
841 aggregates[key] = aggregatorFactory.create(key);
844 return aggregates;
847 function consumeAggregates(aggregates, row) {
848 for (var key in aggregates)
849 aggregates[key].consume(row);
852 function bucketIdenticalRows(rows, identityKeys, propertyGetterFunc) {
853 var identicalRows = {};
854 for (var i = 0; i < rows.length; ++i) {
855 var r = rows[i];
857 var rowIdentity = [];
858 for (var j = 0; j < identityKeys.length; ++j)
859 rowIdentity.push(propertyGetterFunc(r, identityKeys[j]));
860 rowIdentity = rowIdentity.join('\n');
862 var l = identicalRows[rowIdentity];
863 if (!l) {
864 l = [];
865 identicalRows[rowIdentity] = l;
867 l.push(r);
869 return identicalRows;
873 * Merges the rows in |origRows|, by collapsing the columns listed in
874 * |mergeKeys|. Returns an array with the merged rows (in no particular
875 * order).
877 * If |mergeSimilarThreads| is true, then threads with a similar name will be
878 * considered equivalent. For instance, "WorkerThread-1" and "WorkerThread-2"
879 * will be remapped to "WorkerThread-*".
881 * If |outputAsDictionary| is false then the merged rows will be returned as a
882 * flat list. Otherwise the result will be a dictionary, where each row
883 * has a unique key.
885 function mergeRows(origRows, mergeKeys, mergeSimilarThreads,
886 outputAsDictionary) {
887 // Define a translation function for each property. Normally we copy over
888 // properties as-is, but if we have been asked to "merge similar threads" we
889 // we will remap the thread names that end in a numeric suffix.
890 var propertyGetterFunc;
892 if (mergeSimilarThreads) {
893 propertyGetterFunc = function(row, key) {
894 var value = row[key];
895 // If the property is a thread name, try to remap it.
896 if (key == KEY_BIRTH_THREAD || key == KEY_DEATH_THREAD) {
897 var m = /^(.*[^\d])(\d+)$/.exec(value);
898 if (m)
899 value = m[1] + '*';
901 return value;
903 } else {
904 propertyGetterFunc = function(row, key) { return row[key]; };
907 // Determine which sets of properties a row needs to match on to be
908 // considered identical to another row.
909 var identityKeys = IDENTITY_KEYS.slice(0);
910 deleteValuesFromArray(identityKeys, mergeKeys);
912 // Set |aggregateKeys| to everything else, since we will be aggregating
913 // their value as part of the merge.
914 var aggregateKeys = ALL_KEYS.slice(0);
915 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
916 deleteValuesFromArray(aggregateKeys, mergeKeys);
918 // Group all the identical rows together, bucketed into |identicalRows|.
919 var identicalRows =
920 bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc);
922 var mergedRows = outputAsDictionary ? {} : [];
924 // Merge the rows and save the results to |mergedRows|.
925 for (var k in identicalRows) {
926 // We need to smash the list |l| down to a single row...
927 var l = identicalRows[k];
929 var newRow = [];
931 if (outputAsDictionary) {
932 mergedRows[k] = newRow;
933 } else {
934 mergedRows.push(newRow);
937 // Copy over all the identity columns to the new row (since they
938 // were the same for each row matched).
939 for (var i = 0; i < identityKeys.length; ++i)
940 newRow[identityKeys[i]] = propertyGetterFunc(l[0], identityKeys[i]);
942 // Compute aggregates for the other columns.
943 var aggregates = initializeAggregates(aggregateKeys);
945 // Feed the rows to the aggregators.
946 for (var i = 0; i < l.length; ++i)
947 consumeAggregates(aggregates, l[i]);
949 // Suck out the data generated by the aggregators.
950 for (var aggregateKey in aggregates)
951 newRow[aggregateKey] = aggregates[aggregateKey].getValue();
954 return mergedRows;
958 * Takes two dictionaries data1 and data2, and returns a new flat list which
959 * represents the difference between them. The exact meaning of "difference"
960 * is column specific, but for most numeric fields (like the count, or total
961 * time), it is found by subtracting.
963 * Rows in data1 and data2 are expected to use the same scheme for the keys.
964 * In other words, data1[k] is considered the analagous row to data2[k].
966 function subtractSnapshots(data1, data2, columnsToExclude) {
967 // These columns are computed from the other columns. We won't bother
968 // diffing/aggregating these, but rather will derive them again from the
969 // final row.
970 var COMPUTED_AGGREGATE_KEYS = [KEY_AVG_QUEUE_TIME, KEY_AVG_RUN_TIME];
972 // These are the keys which determine row equality. Since we are not doing
973 // any merging yet at this point, it is simply the list of all identity
974 // columns.
975 var identityKeys = IDENTITY_KEYS.slice(0);
976 deleteValuesFromArray(identityKeys, columnsToExclude);
978 // The columns to compute via aggregation is everything else.
979 var aggregateKeys = ALL_KEYS.slice(0);
980 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
981 deleteValuesFromArray(aggregateKeys, COMPUTED_AGGREGATE_KEYS);
982 deleteValuesFromArray(aggregateKeys, columnsToExclude);
984 var diffedRows = [];
986 for (var rowId in data2) {
987 var row1 = data1[rowId];
988 var row2 = data2[rowId];
990 var newRow = [];
992 // Copy over all the identity columns to the new row (since they
993 // were the same for each row matched).
994 for (var i = 0; i < identityKeys.length; ++i)
995 newRow[identityKeys[i]] = row2[identityKeys[i]];
997 // Diff the two rows.
998 if (row1) {
999 for (var i = 0; i < aggregateKeys.length; ++i) {
1000 var aggregateKey = aggregateKeys[i];
1001 var a = row1[aggregateKey];
1002 var b = row2[aggregateKey];
1004 var diffFunc = KEY_PROPERTIES[aggregateKey].diff;
1005 newRow[aggregateKey] = diffFunc(a, b);
1007 } else {
1008 // If the the row doesn't appear in snapshot1, then there is nothing to
1009 // diff, so just copy row2 as is.
1010 for (var i = 0; i < aggregateKeys.length; ++i) {
1011 var aggregateKey = aggregateKeys[i];
1012 newRow[aggregateKey] = row2[aggregateKey];
1016 if (newRow[KEY_COUNT] == 0) {
1017 // If a row's count has gone to zero, it means there were no new
1018 // occurrences of it in the second snapshot, so remove it.
1019 continue;
1022 // Since we excluded the averages during the diffing phase, re-compute
1023 // them using the diffed totals.
1024 computeDataRowAverages(newRow);
1025 diffedRows.push(newRow);
1028 return diffedRows;
1031 // --------------------------------------------------------------------------
1032 // HTML drawing code
1033 // --------------------------------------------------------------------------
1035 function getTextValueForProperty(key, value) {
1036 if (value == undefined) {
1037 // A value may be undefined as a result of having merging rows. We
1038 // won't actually draw it, but this might be called by the filter.
1039 return '';
1042 var textPrinter = KEY_PROPERTIES[key].textPrinter;
1043 if (textPrinter)
1044 return textPrinter(value);
1045 return value.toString();
1049 * Renders the property value |value| into cell |td|. The name of this
1050 * property is |key|.
1052 function drawValueToCell(td, key, value) {
1053 // Get a text representation of the value.
1054 var text = getTextValueForProperty(key, value);
1056 // Apply the desired cell alignment.
1057 var cellAlignment = KEY_PROPERTIES[key].cellAlignment;
1058 if (cellAlignment)
1059 td.align = cellAlignment;
1061 if (key == KEY_SOURCE_LOCATION) {
1062 // Linkify the source column so it jumps to the source code. This doesn't
1063 // take into account the particular code this build was compiled from, or
1064 // local edits to source. It should however work correctly for top of tree
1065 // builds.
1066 var m = /^(.*) \[(\d+)\]$/.exec(text);
1067 if (m) {
1068 var filepath = m[1];
1069 var filename = getFilenameFromPath(filepath);
1070 var linenumber = m[2];
1072 var link = addNode(td, 'a', filename + ' [' + linenumber + ']');
1073 // http://chromesrc.appspot.com is a server I wrote specifically for
1074 // this task. It redirects to the appropriate source file; the file
1075 // paths given by the compiler can be pretty crazy and different
1076 // between platforms.
1077 link.href = 'http://chromesrc.appspot.com/?path=' +
1078 encodeURIComponent(filepath) + '&line=' + linenumber;
1079 link.target = '_blank';
1080 return;
1084 // String values can get pretty long. If the string contains no spaces, then
1085 // CSS fails to wrap it, and it overflows the cell causing the table to get
1086 // really big. We solve this using a hack: insert a <wbr> element after
1087 // every single character. This will allow the rendering engine to wrap the
1088 // value, and hence avoid it overflowing!
1089 var kMinLengthBeforeWrap = 20;
1091 addText(td, text.substr(0, kMinLengthBeforeWrap));
1092 for (var i = kMinLengthBeforeWrap; i < text.length; ++i) {
1093 addNode(td, 'wbr');
1094 addText(td, text.substr(i, 1));
1098 // --------------------------------------------------------------------------
1099 // Helper code for handling the sort and grouping dropdowns.
1100 // --------------------------------------------------------------------------
1102 function addOptionsForGroupingSelect(select) {
1103 // Add "no group" choice.
1104 addNode(select, 'option', '---').value = '';
1106 for (var i = 0; i < GROUPING_DROPDOWN_CHOICES.length; ++i) {
1107 var key = GROUPING_DROPDOWN_CHOICES[i];
1108 var option = addNode(select, 'option', getNameForKey(key));
1109 option.value = key;
1113 function addOptionsForSortingSelect(select) {
1114 // Add "no sort" choice.
1115 addNode(select, 'option', '---').value = '';
1117 // Add a divider.
1118 addNode(select, 'optgroup').label = '';
1120 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1121 var key = SORT_DROPDOWN_CHOICES[i];
1122 addNode(select, 'option', getNameForKey(key)).value = key;
1125 // Add a divider.
1126 addNode(select, 'optgroup').label = '';
1128 // Add the same options, but for descending.
1129 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1130 var key = SORT_DROPDOWN_CHOICES[i];
1131 var n = addNode(select, 'option', getNameForKey(key) + ' (DESC)');
1132 n.value = reverseSortKey(key);
1137 * Helper function used to update the sorting and grouping lists after a
1138 * dropdown changes.
1140 function updateKeyListFromDropdown(list, i, select) {
1141 // Update the list.
1142 if (i < list.length) {
1143 list[i] = select.value;
1144 } else {
1145 list.push(select.value);
1148 // Normalize the list, so setting 'none' as primary zeros out everything
1149 // else.
1150 for (var i = 0; i < list.length; ++i) {
1151 if (list[i] == '') {
1152 list.splice(i, list.length - i);
1153 break;
1159 * Comparator for property |key|, having values |value1| and |value2|.
1160 * If the key has defined a custom comparator use it. Otherwise use a
1161 * default "less than" comparison.
1163 function compareValuesForKey(key, value1, value2) {
1164 var comparator = KEY_PROPERTIES[key].comparator;
1165 if (comparator)
1166 return comparator(value1, value2);
1167 return simpleCompare(value1, value2);
1170 function reverseSortKey(key) {
1171 return -key;
1174 function sortKeyIsReversed(key) {
1175 return key < 0;
1178 function sortKeysMatch(key1, key2) {
1179 return Math.abs(key1) == Math.abs(key2);
1182 function getKeysForCheckedBoxes(checkboxes) {
1183 var keys = [];
1184 for (var k in checkboxes) {
1185 if (checkboxes[k].checked)
1186 keys.push(k);
1188 return keys;
1191 // --------------------------------------------------------------------------
1194 * @constructor
1196 function MainView() {
1197 // Make sure we have a definition for each key.
1198 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1199 if (!KEY_PROPERTIES[k])
1200 throw 'KEY_PROPERTIES[] not defined for key: ' + k;
1203 this.init_();
1206 MainView.prototype = {
1207 addDataToSnapshot: function(data) {
1208 // TODO(eroman): We need to know which snapshot this data belongs to!
1209 // For now we assume it is the most recent snapshot.
1210 var snapshotIndex = this.snapshots_.length - 1;
1212 var snapshot = this.snapshots_[snapshotIndex];
1214 var pid = data.process_id;
1215 var ptype = data.process_type;
1217 // Save the browser's representation of the data
1218 snapshot.origData.push(data);
1220 // Augment each data row with the process information.
1221 var rows = data.list;
1222 for (var i = 0; i < rows.length; ++i) {
1223 // Transform the data from a dictionary to an array. This internal
1224 // representation is more compact and faster to access.
1225 var origRow = rows[i];
1226 var newRow = [];
1228 newRow[KEY_PROCESS_ID] = pid;
1229 newRow[KEY_PROCESS_TYPE] = ptype;
1231 // Copy over the known properties which have a 1:1 mapping with JSON.
1232 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1233 var inputJsonKey = KEY_PROPERTIES[k].inputJsonKey;
1234 if (inputJsonKey != undefined) {
1235 newRow[k] = getPropertyByPath(origRow, inputJsonKey);
1239 if (newRow[KEY_COUNT] == 0) {
1240 // When resetting the data, it is possible for the backend to give us
1241 // counts of "0". There is no point adding these rows (in fact they
1242 // will cause us to do divide by zeros when calculating averages and
1243 // stuff), so we skip past them.
1244 continue;
1247 // Add our computed properties.
1248 augmentDataRow(newRow);
1250 snapshot.flatData.push(newRow);
1253 if (!arrayToSet(this.getSelectedSnapshotIndexes_())[snapshotIndex]) {
1254 // Optimization: If this snapshot is not a data dependency for the
1255 // current display, then don't bother updating anything.
1256 return;
1259 // We may end up calling addDataToSnapshot_() repeatedly (once for each
1260 // process). To avoid this from slowing us down we do bulk updates on a
1261 // timer.
1262 this.updateMergedDataSoon_();
1265 updateMergedDataSoon_: function() {
1266 if (this.updateMergedDataPending_) {
1267 // If a delayed task has already been posted to re-merge the data,
1268 // then we don't need to do anything extra.
1269 return;
1272 // Otherwise schedule updateMergedData_() to be called later. We want it
1273 // to be called no more than once every PROCESS_DATA_DELAY_MS
1274 // milliseconds.
1276 if (this.lastUpdateMergedDataTime_ == undefined)
1277 this.lastUpdateMergedDataTime_ = 0;
1279 var timeSinceLastMerge = getTimeMillis() - this.lastUpdateMergedDataTime_;
1280 var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge);
1282 var functionToRun = function() {
1283 // Do the actual update.
1284 this.updateMergedData_();
1285 // Keep track of when we last ran.
1286 this.lastUpdateMergedDataTime_ = getTimeMillis();
1287 this.updateMergedDataPending_ = false;
1288 }.bind(this);
1290 this.updateMergedDataPending_ = true;
1291 window.setTimeout(functionToRun, timeToWait);
1295 * Returns a list of the currently selected snapshots. This list is
1296 * guaranteed to be of length 1 or 2.
1298 getSelectedSnapshotIndexes_: function() {
1299 var indexes = this.getSelectedSnapshotBoxes_();
1300 for (var i = 0; i < indexes.length; ++i)
1301 indexes[i] = indexes[i].__index;
1302 return indexes;
1306 * Same as getSelectedSnapshotIndexes_(), only it returns the actual
1307 * checkbox input DOM nodes rather than the snapshot ID.
1309 getSelectedSnapshotBoxes_: function() {
1310 // Figure out which snaphots to use for our data.
1311 var boxes = [];
1312 for (var i = 0; i < this.snapshots_.length; ++i) {
1313 var box = this.getSnapshotCheckbox_(i);
1314 if (box.checked)
1315 boxes.push(box);
1317 return boxes;
1321 * Re-draw the description that explains which snapshots are currently
1322 * selected (if two snapshots were selected we explain that the *difference*
1323 * between them is being displayed).
1325 updateSnapshotSelectionSummaryDiv_: function() {
1326 var summaryDiv = $(SNAPSHOT_SELECTION_SUMMARY_ID);
1328 var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1329 if (selectedSnapshots.length == 0) {
1330 // This can occur during an attempt to load a file or following file
1331 // load failure. We just ignore it and move on.
1332 } else if (selectedSnapshots.length == 1) {
1333 // If only one snapshot is chosen then we will display that snapshot's
1334 // data in its entirety.
1335 this.flatData_ = this.snapshots_[selectedSnapshots[0]].flatData;
1337 // Don't bother displaying any text when just 1 snapshot is selected,
1338 // since it is obvious what this should do.
1339 summaryDiv.innerText = '';
1340 } else if (selectedSnapshots.length == 2) {
1341 // Otherwise if two snapshots were chosen, show the difference between
1342 // them.
1343 var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1344 var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1346 var timeDeltaInSeconds =
1347 ((snapshot2.time - snapshot1.time) / 1000).toFixed(0);
1349 // Explain that what is being shown is the difference between two
1350 // snapshots.
1351 summaryDiv.innerText =
1352 'Showing the difference between snapshots #' +
1353 selectedSnapshots[0] + ' and #' +
1354 selectedSnapshots[1] + ' (' + timeDeltaInSeconds +
1355 ' seconds worth of data)';
1356 } else {
1357 // This shouldn't be possible...
1358 throw 'Unexpected number of selected snapshots';
1362 updateMergedData_: function() {
1363 // Retrieve the merge options.
1364 var mergeColumns = this.getMergeColumns_();
1365 var shouldMergeSimilarThreads = this.shouldMergeSimilarThreads_();
1367 var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1369 // We do merges a bit differently depending if we are displaying the diffs
1370 // between two snapshots, or just displaying a single snapshot.
1371 if (selectedSnapshots.length == 1) {
1372 var snapshot = this.snapshots_[selectedSnapshots[0]];
1373 this.mergedData_ = mergeRows(snapshot.flatData,
1374 mergeColumns,
1375 shouldMergeSimilarThreads,
1376 false);
1378 } else if (selectedSnapshots.length == 2) {
1379 var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1380 var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1382 // Merge the data for snapshot1.
1383 var mergedRows1 = mergeRows(snapshot1.flatData,
1384 mergeColumns,
1385 shouldMergeSimilarThreads,
1386 true);
1388 // Merge the data for snapshot2.
1389 var mergedRows2 = mergeRows(snapshot2.flatData,
1390 mergeColumns,
1391 shouldMergeSimilarThreads,
1392 true);
1394 // Do a diff between the two snapshots.
1395 this.mergedData_ = subtractSnapshots(mergedRows1,
1396 mergedRows2,
1397 mergeColumns);
1398 } else {
1399 throw 'Unexpected number of selected snapshots';
1402 // Recompute filteredData_ (since it is derived from mergedData_)
1403 this.updateFilteredData_();
1406 updateFilteredData_: function() {
1407 // Recompute filteredData_.
1408 this.filteredData_ = [];
1409 var filterFunc = this.getFilterFunction_();
1410 for (var i = 0; i < this.mergedData_.length; ++i) {
1411 var r = this.mergedData_[i];
1412 if (!filterFunc(r)) {
1413 // Not matched by our filter, discard.
1414 continue;
1416 this.filteredData_.push(r);
1419 // Recompute groupedData_ (since it is derived from filteredData_)
1420 this.updateGroupedData_();
1423 updateGroupedData_: function() {
1424 // Recompute groupedData_.
1425 var groupKeyToData = {};
1426 var entryToGroupKeyFunc = this.getGroupingFunction_();
1427 for (var i = 0; i < this.filteredData_.length; ++i) {
1428 var r = this.filteredData_[i];
1430 var groupKey = entryToGroupKeyFunc(r);
1432 var groupData = groupKeyToData[groupKey];
1433 if (!groupData) {
1434 groupData = {
1435 key: JSON.parse(groupKey),
1436 aggregates: initializeAggregates(ALL_KEYS),
1437 rows: [],
1439 groupKeyToData[groupKey] = groupData;
1442 // Add the row to our list.
1443 groupData.rows.push(r);
1445 // Update aggregates for each column.
1446 consumeAggregates(groupData.aggregates, r);
1448 this.groupedData_ = groupKeyToData;
1450 // Figure out a display order for the groups themselves.
1451 this.sortedGroupKeys_ = getDictionaryKeys(groupKeyToData);
1452 this.sortedGroupKeys_.sort(this.getGroupSortingFunction_());
1454 // Sort the group data.
1455 this.sortGroupedData_();
1458 sortGroupedData_: function() {
1459 var sortingFunc = this.getSortingFunction_();
1460 for (var k in this.groupedData_)
1461 this.groupedData_[k].rows.sort(sortingFunc);
1463 // Every cached data dependency is now up to date, all that is left is
1464 // to actually draw the result.
1465 this.redrawData_();
1468 getVisibleColumnKeys_: function() {
1469 // Figure out what columns to include, based on the selected checkboxes.
1470 var columns = this.getSelectionColumns_();
1471 columns = columns.slice(0);
1473 // Eliminate columns which we are merging on.
1474 deleteValuesFromArray(columns, this.getMergeColumns_());
1476 // Eliminate columns which we are grouped on.
1477 if (this.sortedGroupKeys_.length > 0) {
1478 // The grouping will be the the same for each so just pick the first.
1479 var randomGroupKey = this.groupedData_[this.sortedGroupKeys_[0]].key;
1481 // The grouped properties are going to be the same for each row in our,
1482 // table, so avoid drawing them in our table!
1483 var keysToExclude = [];
1485 for (var i = 0; i < randomGroupKey.length; ++i)
1486 keysToExclude.push(randomGroupKey[i].key);
1487 deleteValuesFromArray(columns, keysToExclude);
1490 // If we are currently showing a "diff", hide the max columns, since we
1491 // are not populating it correctly. See the TODO at the top of this file.
1492 if (this.getSelectedSnapshotIndexes_().length > 1)
1493 deleteValuesFromArray(columns, [KEY_MAX_RUN_TIME, KEY_MAX_QUEUE_TIME]);
1495 return columns;
1498 redrawData_: function() {
1499 // Clear the results div, sine we may be overwriting older data.
1500 var parent = $(RESULTS_DIV_ID);
1501 parent.innerHTML = '';
1503 var columns = this.getVisibleColumnKeys_();
1505 // Draw each group.
1506 for (var i = 0; i < this.sortedGroupKeys_.length; ++i) {
1507 var k = this.sortedGroupKeys_[i];
1508 this.drawGroup_(parent, k, columns);
1513 * Renders the information for a particular group.
1515 drawGroup_: function(parent, groupKey, columns) {
1516 var groupData = this.groupedData_[groupKey];
1518 var div = addNode(parent, 'div');
1519 div.className = 'group-container';
1521 this.drawGroupTitle_(div, groupData.key);
1523 var table = addNode(div, 'table');
1525 this.drawDataTable_(table, groupData, columns, groupKey);
1529 * Draws a title into |parent| that describes |groupKey|.
1531 drawGroupTitle_: function(parent, groupKey) {
1532 if (groupKey.length == 0) {
1533 // Empty group key means there was no grouping.
1534 return;
1537 var parent = addNode(parent, 'div');
1538 parent.className = 'group-title-container';
1540 // Each component of the group key represents the "key=value" constraint
1541 // for this group. Show these as an AND separated list.
1542 for (var i = 0; i < groupKey.length; ++i) {
1543 if (i > 0)
1544 addNode(parent, 'i', ' and ');
1545 var e = groupKey[i];
1546 addNode(parent, 'b', getNameForKey(e.key) + ' = ');
1547 addNode(parent, 'span', e.value);
1552 * Renders a table which summarizes all |column| fields for |data|.
1554 drawDataTable_: function(table, data, columns, groupKey) {
1555 table.className = 'results-table';
1556 var thead = addNode(table, 'thead');
1557 var tbody = addNode(table, 'tbody');
1559 var displaySettings = this.getGroupDisplaySettings_(groupKey);
1560 var limit = displaySettings.limit;
1562 this.drawAggregateRow_(thead, data.aggregates, columns);
1563 this.drawTableHeader_(thead, columns);
1564 this.drawTableBody_(tbody, data.rows, columns, limit);
1565 this.drawTruncationRow_(tbody, data.rows.length, limit, columns.length,
1566 groupKey);
1569 drawTableHeader_: function(thead, columns) {
1570 var tr = addNode(thead, 'tr');
1571 for (var i = 0; i < columns.length; ++i) {
1572 var key = columns[i];
1573 var th = addNode(tr, 'th', getNameForKey(key));
1574 th.onclick = this.onClickColumn_.bind(this, key);
1576 // Draw an indicator if we are currently sorted on this column.
1577 // TODO(eroman): Should use an icon instead of asterisk!
1578 for (var j = 0; j < this.currentSortKeys_.length; ++j) {
1579 if (sortKeysMatch(this.currentSortKeys_[j], key)) {
1580 var sortIndicator = addNode(th, 'span', '*');
1581 sortIndicator.style.color = 'red';
1582 if (sortKeyIsReversed(this.currentSortKeys_[j])) {
1583 // Use double-asterisk for descending columns.
1584 addText(sortIndicator, '*');
1586 break;
1592 drawTableBody_: function(tbody, rows, columns, limit) {
1593 for (var i = 0; i < rows.length && i < limit; ++i) {
1594 var e = rows[i];
1596 var tr = addNode(tbody, 'tr');
1598 for (var c = 0; c < columns.length; ++c) {
1599 var key = columns[c];
1600 var value = e[key];
1602 var td = addNode(tr, 'td');
1603 drawValueToCell(td, key, value);
1609 * Renders a row that describes all the aggregate values for |columns|.
1611 drawAggregateRow_: function(tbody, aggregates, columns) {
1612 var tr = addNode(tbody, 'tr');
1613 tr.className = 'aggregator-row';
1615 for (var i = 0; i < columns.length; ++i) {
1616 var key = columns[i];
1617 var td = addNode(tr, 'td');
1619 // Most of our outputs are numeric, so we want to align them to the
1620 // right. However for the unique counts we will center.
1621 if (KEY_PROPERTIES[key].aggregator == UniquifyAggregator) {
1622 td.align = 'center';
1623 } else {
1624 td.align = 'right';
1627 var aggregator = aggregates[key];
1628 if (aggregator)
1629 td.innerText = aggregator.getValueAsText();
1634 * Renders a row which describes how many rows the table has, how many are
1635 * currently hidden, and a set of buttons to show more.
1637 drawTruncationRow_: function(tbody, numRows, limit, numColumns, groupKey) {
1638 var numHiddenRows = Math.max(numRows - limit, 0);
1639 var numVisibleRows = numRows - numHiddenRows;
1641 var tr = addNode(tbody, 'tr');
1642 tr.className = 'truncation-row';
1643 var td = addNode(tr, 'td');
1644 td.colSpan = numColumns;
1646 addText(td, numRows + ' rows');
1647 if (numHiddenRows > 0) {
1648 var s = addNode(td, 'span', ' (' + numHiddenRows + ' hidden) ');
1649 s.style.color = 'red';
1652 if (numVisibleRows > LIMIT_INCREMENT) {
1653 addNode(td, 'button', 'Show less').onclick =
1654 this.changeGroupDisplayLimit_.bind(
1655 this, groupKey, -LIMIT_INCREMENT);
1657 if (numVisibleRows > 0) {
1658 addNode(td, 'button', 'Show none').onclick =
1659 this.changeGroupDisplayLimit_.bind(this, groupKey, -Infinity);
1662 if (numHiddenRows > 0) {
1663 addNode(td, 'button', 'Show more').onclick =
1664 this.changeGroupDisplayLimit_.bind(this, groupKey, LIMIT_INCREMENT);
1665 addNode(td, 'button', 'Show all').onclick =
1666 this.changeGroupDisplayLimit_.bind(this, groupKey, Infinity);
1671 * Adjusts the row limit for group |groupKey| by |delta|.
1673 changeGroupDisplayLimit_: function(groupKey, delta) {
1674 // Get the current settings for this group.
1675 var settings = this.getGroupDisplaySettings_(groupKey, true);
1677 // Compute the adjusted limit.
1678 var newLimit = settings.limit;
1679 var totalNumRows = this.groupedData_[groupKey].rows.length;
1680 newLimit = Math.min(totalNumRows, newLimit);
1681 newLimit += delta;
1682 newLimit = Math.max(0, newLimit);
1684 // Update the settings with the new limit.
1685 settings.limit = newLimit;
1687 // TODO(eroman): It isn't necessary to redraw *all* the data. Really we
1688 // just need to insert the missing rows (everything else stays the same)!
1689 this.redrawData_();
1693 * Returns the rendering settings for group |groupKey|. This includes things
1694 * like how many rows to display in the table.
1696 getGroupDisplaySettings_: function(groupKey, opt_create) {
1697 var settings = this.groupDisplaySettings_[groupKey];
1698 if (!settings) {
1699 // If we don't have any settings for this group yet, create some
1700 // default ones.
1701 if (groupKey == '[]') {
1702 // (groupKey of '[]' is what we use for ungrouped data).
1703 settings = {limit: INITIAL_UNGROUPED_ROW_LIMIT};
1704 } else {
1705 settings = {limit: INITIAL_GROUP_ROW_LIMIT};
1707 if (opt_create)
1708 this.groupDisplaySettings_[groupKey] = settings;
1710 return settings;
1713 init_: function() {
1714 this.snapshots_ = [];
1716 // Start fetching the data from the browser; this will be our snapshot #0.
1717 this.takeSnapshot_();
1719 // Data goes through the following pipeline:
1720 // (1) Raw data received from browser, and transformed into our own
1721 // internal row format (where properties are indexed by KEY_*
1722 // constants.)
1723 // (2) We "augment" each row by adding some extra computed columns
1724 // (like averages).
1725 // (3) The rows are merged using current merge settings.
1726 // (4) The rows that don't match current search expression are
1727 // tossed out.
1728 // (5) The rows are organized into "groups" based on current settings,
1729 // and aggregate values are computed for each resulting group.
1730 // (6) The rows within each group are sorted using current settings.
1731 // (7) The grouped rows are drawn to the screen.
1732 this.mergedData_ = [];
1733 this.filteredData_ = [];
1734 this.groupedData_ = {};
1735 this.sortedGroupKeys_ = [];
1737 this.groupDisplaySettings_ = {};
1739 this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID));
1740 this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID));
1742 $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this);
1744 this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0);
1745 this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0);
1747 this.fillGroupingDropdowns_();
1748 this.fillSortingDropdowns_();
1750 $(EDIT_COLUMNS_LINK_ID).onclick =
1751 toggleNodeDisplay.bind(null, $(EDIT_COLUMNS_ROW));
1753 $(TOGGLE_SNAPSHOTS_LINK_ID).onclick =
1754 toggleNodeDisplay.bind(null, $(SNAPSHOTS_ROW));
1756 $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange =
1757 this.onMergeSimilarThreadsCheckboxChanged_.bind(this);
1759 $(TAKE_SNAPSHOT_BUTTON_ID).onclick = this.takeSnapshot_.bind(this);
1761 $(SAVE_SNAPSHOTS_BUTTON_ID).onclick = this.saveSnapshots_.bind(this);
1762 $(SNAPSHOT_FILE_LOADER_ID).onchange = this.loadFileChanged_.bind(this);
1765 takeSnapshot_: function() {
1766 // Start a new empty snapshot. Make note of the current time, so we know
1767 // when the snaphot was taken.
1768 this.snapshots_.push({flatData: [], origData: [], time: getTimeMillis()});
1770 // Update the UI to reflect the new snapshot.
1771 this.addSnapshotToList_(this.snapshots_.length - 1);
1773 // Ask the browser for the profiling data. We will receive the data
1774 // later through a callback to addDataToSnapshot_().
1775 g_browserBridge.sendGetData();
1778 saveSnapshots_: function() {
1779 var snapshots = [];
1780 for (var i = 0; i < this.snapshots_.length; ++i) {
1781 snapshots.push({ data: this.snapshots_[i].origData,
1782 timestamp: Math.floor(
1783 this.snapshots_[i].time / 1000) });
1786 var dump = {
1787 'userAgent': navigator.userAgent,
1788 'version': 1,
1789 'snapshots': snapshots
1792 var dumpText = JSON.stringify(dump, null, ' ');
1793 var textBlob = new Blob([dumpText],
1794 { type: 'octet/stream', endings: 'native' });
1795 var blobUrl = window.URL.createObjectURL(textBlob);
1796 $(DOWNLOAD_ANCHOR_ID).href = blobUrl;
1797 $(DOWNLOAD_ANCHOR_ID).click();
1800 loadFileChanged_: function() {
1801 this.loadSnapshots_($(SNAPSHOT_FILE_LOADER_ID).files[0]);
1804 loadSnapshots_: function(file) {
1805 if (file) {
1806 var fileReader = new FileReader();
1808 fileReader.onload = this.onLoadSnapshotsFile_.bind(this, file);
1809 fileReader.onerror = this.onLoadSnapshotsFileError_.bind(this, file);
1811 fileReader.readAsText(file);
1815 onLoadSnapshotsFile_: function(file, event) {
1816 try {
1817 var parsed = null;
1818 parsed = JSON.parse(event.target.result);
1820 if (parsed.version != 1) {
1821 throw new Error('Unrecognized version: ' + parsed.version);
1824 if (parsed.snapshots.length < 1) {
1825 throw new Error('File contains no data');
1828 this.displayLoadedFile_(file, parsed);
1829 this.hideFileLoadError_();
1830 } catch (error) {
1831 this.displayFileLoadError_('File load failure: ' + error.message);
1835 clearExistingSnapshots_: function() {
1836 var tbody = $('snapshots-tbody');
1837 this.snapshots_ = [];
1838 tbody.innerHTML = '';
1839 this.updateMergedDataSoon_();
1842 displayLoadedFile_: function(file, content) {
1843 this.clearExistingSnapshots_();
1844 $(TAKE_SNAPSHOT_BUTTON_ID).disabled = true;
1845 $(SAVE_SNAPSHOTS_BUTTON_ID).disabled = true;
1847 if (content.snapshots.length > 1) {
1848 setNodeDisplay($(SNAPSHOTS_ROW), true);
1851 for (var i = 0; i < content.snapshots.length; ++i) {
1852 var snapshot = content.snapshots[i];
1853 this.snapshots_.push({flatData: [], origData: [],
1854 time: snapshot.timestamp * 1000});
1855 this.addSnapshotToList_(this.snapshots_.length - 1);
1856 var snapshotData = snapshot.data;
1857 for (var j = 0; j < snapshotData.length; ++j) {
1858 this.addDataToSnapshot(snapshotData[j]);
1861 this.redrawData_();
1864 onLoadSnapshotsFileError_: function(file, filedata) {
1865 this.displayFileLoadError_('Error loading ' + file.name);
1868 displayFileLoadError_: function(message) {
1869 $(LOAD_ERROR_ID).textContent = message;
1870 $(LOAD_ERROR_ID).hidden = false;
1873 hideFileLoadError_: function() {
1874 $(LOAD_ERROR_ID).textContent = '';
1875 $(LOAD_ERROR_ID).hidden = true;
1878 getSnapshotCheckbox_: function(i) {
1879 return $(this.getSnapshotCheckboxId_(i));
1882 getSnapshotCheckboxId_: function(i) {
1883 return 'snapshotCheckbox-' + i;
1886 addSnapshotToList_: function(i) {
1887 var tbody = $('snapshots-tbody');
1889 var tr = addNode(tbody, 'tr');
1891 var id = this.getSnapshotCheckboxId_(i);
1893 var checkboxCell = addNode(tr, 'td');
1894 var checkbox = addNode(checkboxCell, 'input');
1895 checkbox.type = 'checkbox';
1896 checkbox.id = id;
1897 checkbox.__index = i;
1898 checkbox.onclick = this.onSnapshotCheckboxChanged_.bind(this);
1900 addNode(tr, 'td', '#' + i);
1902 var labelCell = addNode(tr, 'td');
1903 var l = addNode(labelCell, 'label');
1905 var dateString = new Date(this.snapshots_[i].time).toLocaleString();
1906 addText(l, dateString);
1907 l.htmlFor = id;
1909 // If we are on snapshot 0, make it the default.
1910 if (i == 0) {
1911 checkbox.checked = true;
1912 checkbox.__time = getTimeMillis();
1913 this.updateSnapshotCheckboxStyling_();
1917 updateSnapshotCheckboxStyling_: function() {
1918 for (var i = 0; i < this.snapshots_.length; ++i) {
1919 var checkbox = this.getSnapshotCheckbox_(i);
1920 checkbox.parentNode.parentNode.className =
1921 checkbox.checked ? 'selected_snapshot' : '';
1925 onSnapshotCheckboxChanged_: function(event) {
1926 // Keep track of when we clicked this box (for when we need to uncheck
1927 // older boxes).
1928 event.target.__time = getTimeMillis();
1930 // Find all the checked boxes. Either 1 or 2 can be checked. If a third
1931 // was just checked, then uncheck one of the earlier ones so we only have
1932 // 2.
1933 var checked = this.getSelectedSnapshotBoxes_();
1934 checked.sort(function(a, b) { return b.__time - a.__time; });
1935 if (checked.length > 2) {
1936 for (var i = 2; i < checked.length; ++i)
1937 checked[i].checked = false;
1938 checked.length = 2;
1941 // We should always have at least 1 selection. Prevent the user from
1942 // unselecting the final box.
1943 if (checked.length == 0)
1944 event.target.checked = true;
1946 this.updateSnapshotCheckboxStyling_();
1947 this.updateSnapshotSelectionSummaryDiv_();
1949 // Recompute mergedData_ (since it is derived from selected snapshots).
1950 this.updateMergedData_();
1953 fillSelectionCheckboxes_: function(parent) {
1954 this.selectionCheckboxes_ = {};
1956 var onChangeFunc = this.onSelectCheckboxChanged_.bind(this);
1958 for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) {
1959 var key = ALL_TABLE_COLUMNS[i];
1960 var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1961 checkbox.checked = true;
1962 checkbox.onchange = onChangeFunc;
1963 addText(parent, ' ');
1964 this.selectionCheckboxes_[key] = checkbox;
1967 for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
1968 this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
1972 getSelectionColumns_: function() {
1973 return getKeysForCheckedBoxes(this.selectionCheckboxes_);
1976 getMergeColumns_: function() {
1977 return getKeysForCheckedBoxes(this.mergeCheckboxes_);
1980 shouldMergeSimilarThreads_: function() {
1981 return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
1984 fillMergeCheckboxes_: function(parent) {
1985 this.mergeCheckboxes_ = {};
1987 var onChangeFunc = this.onMergeCheckboxChanged_.bind(this);
1989 for (var i = 0; i < MERGEABLE_KEYS.length; ++i) {
1990 var key = MERGEABLE_KEYS[i];
1991 var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1992 checkbox.onchange = onChangeFunc;
1993 addText(parent, ' ');
1994 this.mergeCheckboxes_[key] = checkbox;
1997 for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
1998 this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
2002 fillGroupingDropdowns_: function() {
2003 var parent = $(GROUP_BY_CONTAINER_ID);
2004 parent.innerHTML = '';
2006 for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) {
2007 // Add a dropdown.
2008 var select = addNode(parent, 'select');
2009 select.onchange = this.onChangedGrouping_.bind(this, select, i);
2011 addOptionsForGroupingSelect(select);
2013 if (i < this.currentGroupingKeys_.length) {
2014 var key = this.currentGroupingKeys_[i];
2015 setSelectedOptionByValue(select, key);
2020 fillSortingDropdowns_: function() {
2021 var parent = $(SORT_BY_CONTAINER_ID);
2022 parent.innerHTML = '';
2024 for (var i = 0; i <= this.currentSortKeys_.length; ++i) {
2025 // Add a dropdown.
2026 var select = addNode(parent, 'select');
2027 select.onchange = this.onChangedSorting_.bind(this, select, i);
2029 addOptionsForSortingSelect(select);
2031 if (i < this.currentSortKeys_.length) {
2032 var key = this.currentSortKeys_[i];
2033 setSelectedOptionByValue(select, key);
2038 onChangedGrouping_: function(select, i) {
2039 updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
2040 this.fillGroupingDropdowns_();
2041 this.updateGroupedData_();
2044 onChangedSorting_: function(select, i) {
2045 updateKeyListFromDropdown(this.currentSortKeys_, i, select);
2046 this.fillSortingDropdowns_();
2047 this.sortGroupedData_();
2050 onSelectCheckboxChanged_: function() {
2051 this.redrawData_();
2054 onMergeCheckboxChanged_: function() {
2055 this.updateMergedData_();
2058 onMergeSimilarThreadsCheckboxChanged_: function() {
2059 this.updateMergedData_();
2062 onChangedFilter_: function() {
2063 this.updateFilteredData_();
2067 * When left-clicking a column, change the primary sort order to that
2068 * column. If we were already sorted on that column then reverse the order.
2070 * When alt-clicking, add a secondary sort column. Similarly, if
2071 * alt-clicking a column which was already being sorted on, reverse its
2072 * order.
2074 onClickColumn_: function(key, event) {
2075 // If this property wants to start off in descending order rather then
2076 // ascending, flip it.
2077 if (KEY_PROPERTIES[key].sortDescending)
2078 key = reverseSortKey(key);
2080 // Scan through our sort order and see if we are already sorted on this
2081 // key. If so, reverse that sort ordering.
2082 var foundIndex = -1;
2083 for (var i = 0; i < this.currentSortKeys_.length; ++i) {
2084 var curKey = this.currentSortKeys_[i];
2085 if (sortKeysMatch(curKey, key)) {
2086 this.currentSortKeys_[i] = reverseSortKey(curKey);
2087 foundIndex = i;
2088 break;
2092 if (event.altKey) {
2093 if (foundIndex == -1) {
2094 // If we weren't already sorted on the column that was alt-clicked,
2095 // then add it to our sort.
2096 this.currentSortKeys_.push(key);
2098 } else {
2099 if (foundIndex != 0 ||
2100 !sortKeysMatch(this.currentSortKeys_[foundIndex], key)) {
2101 // If the column we left-clicked wasn't already our primary column,
2102 // make it so.
2103 this.currentSortKeys_ = [key];
2104 } else {
2105 // If the column we left-clicked was already our primary column (and
2106 // we just reversed it), remove any secondary sorts.
2107 this.currentSortKeys_.length = 1;
2111 this.fillSortingDropdowns_();
2112 this.sortGroupedData_();
2115 getSortingFunction_: function() {
2116 var sortKeys = this.currentSortKeys_.slice(0);
2118 // Eliminate the empty string keys (which means they were unspecified).
2119 deleteValuesFromArray(sortKeys, ['']);
2121 // If no sort is specified, use our default sort.
2122 if (sortKeys.length == 0)
2123 sortKeys = [DEFAULT_SORT_KEYS];
2125 return function(a, b) {
2126 for (var i = 0; i < sortKeys.length; ++i) {
2127 var key = Math.abs(sortKeys[i]);
2128 var factor = sortKeys[i] < 0 ? -1 : 1;
2130 var propA = a[key];
2131 var propB = b[key];
2133 var comparison = compareValuesForKey(key, propA, propB);
2134 comparison *= factor; // Possibly reverse the ordering.
2136 if (comparison != 0)
2137 return comparison;
2140 // Tie breaker.
2141 return simpleCompare(JSON.stringify(a), JSON.stringify(b));
2145 getGroupSortingFunction_: function() {
2146 return function(a, b) {
2147 var groupKey1 = JSON.parse(a);
2148 var groupKey2 = JSON.parse(b);
2150 for (var i = 0; i < groupKey1.length; ++i) {
2151 var comparison = compareValuesForKey(
2152 groupKey1[i].key,
2153 groupKey1[i].value,
2154 groupKey2[i].value);
2156 if (comparison != 0)
2157 return comparison;
2160 // Tie breaker.
2161 return simpleCompare(a, b);
2165 getFilterFunction_: function() {
2166 var searchStr = $(FILTER_SEARCH_ID).value;
2168 // Normalize the search expression.
2169 searchStr = trimWhitespace(searchStr);
2170 searchStr = searchStr.toLowerCase();
2172 return function(x) {
2173 // Match everything when there was no filter.
2174 if (searchStr == '')
2175 return true;
2177 // Treat the search text as a LOWERCASE substring search.
2178 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
2179 var propertyText = getTextValueForProperty(k, x[k]);
2180 if (propertyText.toLowerCase().indexOf(searchStr) != -1)
2181 return true;
2184 return false;
2188 getGroupingFunction_: function() {
2189 var groupings = this.currentGroupingKeys_.slice(0);
2191 // Eliminate the empty string groupings (which means they were
2192 // unspecified).
2193 deleteValuesFromArray(groupings, ['']);
2195 // Eliminate duplicate primary/secondary group by directives, since they
2196 // are redundant.
2197 deleteDuplicateStringsFromArray(groupings);
2199 return function(e) {
2200 var groupKey = [];
2202 for (var i = 0; i < groupings.length; ++i) {
2203 var entry = {key: groupings[i],
2204 value: e[groupings[i]]};
2205 groupKey.push(entry);
2208 return JSON.stringify(groupKey);
2213 return MainView;
2214 })();