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.
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.
14 * Main entry point called once the page has loaded.
17 g_browserBridge = new BrowserBridge();
18 g_mainView = new MainView();
21 document.addEventListener('DOMContentLoaded', onLoad);
24 * This class provides a "bridge" for communicating between the javascript and
25 * the browser. Used as a singleton.
27 var BrowserBridge = (function() {
33 function BrowserBridge() {
36 BrowserBridge.prototype = {
37 //--------------------------------------------------------------------------
38 // Messages sent to the browser
39 //--------------------------------------------------------------------------
41 sendGetData: function() {
42 chrome.send('getData');
45 sendResetData: function() {
46 chrome.send('resetData');
49 //--------------------------------------------------------------------------
50 // Messages received from the browser.
51 //--------------------------------------------------------------------------
53 receivedData: function(data) {
54 // TODO(eroman): The browser should give an indication of which snapshot
55 // this data belongs to. For now we always assume it is for the latest.
56 g_mainView.addDataToSnapshot(data);
64 * This class handles the presentation of our profiler view. Used as a
67 var MainView = (function() {
70 // --------------------------------------------------------------------------
71 // Important IDs in the HTML document
72 // --------------------------------------------------------------------------
74 // The search box to filter results.
75 var FILTER_SEARCH_ID = 'filter-search';
77 // The container node to put all the "Group by" dropdowns into.
78 var GROUP_BY_CONTAINER_ID = 'group-by-container';
80 // The container node to put all the "Sort by" dropdowns into.
81 var SORT_BY_CONTAINER_ID = 'sort-by-container';
83 // The DIV to put all the tables into.
84 var RESULTS_DIV_ID = 'results-div';
86 // The container node to put all the column (visibility) checkboxes into.
87 var COLUMN_TOGGLES_CONTAINER_ID = 'column-toggles-container';
89 // The container node to put all the column (merge) checkboxes into.
90 var COLUMN_MERGE_TOGGLES_CONTAINER_ID = 'column-merge-toggles-container';
92 // The anchor which toggles visibility of column checkboxes.
93 var EDIT_COLUMNS_LINK_ID = 'edit-columns-link';
95 // The container node to show/hide when toggling the column checkboxes.
96 var EDIT_COLUMNS_ROW = 'edit-columns-row';
98 // The checkbox which controls whether things like "Worker Threads" and
99 // "PAC threads" will be merged together.
100 var MERGE_SIMILAR_THREADS_CHECKBOX_ID = 'merge-similar-threads-checkbox';
102 var RESET_DATA_LINK_ID = 'reset-data-link';
104 var TOGGLE_SNAPSHOTS_LINK_ID = 'snapshots-link';
105 var SNAPSHOTS_ROW = 'snapshots-row';
106 var SNAPSHOT_SELECTION_SUMMARY_ID = 'snapshot-selection-summary';
107 var TAKE_SNAPSHOT_BUTTON_ID = 'take-snapshot-button';
109 var SAVE_SNAPSHOTS_BUTTON_ID = 'save-snapshots-button';
110 var SNAPSHOT_FILE_LOADER_ID = 'snapshot-file-loader';
111 var LOAD_ERROR_ID = 'file-load-error';
113 var DOWNLOAD_ANCHOR_ID = 'download-anchor';
115 // --------------------------------------------------------------------------
117 // --------------------------------------------------------------------------
119 // Each row of our data is an array of values rather than a dictionary. This
120 // avoids some overhead from repeating the key string multiple times, and
121 // speeds up the property accesses a bit. The following keys are well-known
122 // indexes into the array for various properties.
124 // Note that the declaration order will also define the default display order.
126 var BEGIN_KEY = 1; // Start at 1 rather than 0 to simplify sorting code.
127 var END_KEY = BEGIN_KEY;
129 var KEY_COUNT = END_KEY++;
130 var KEY_RUN_TIME = END_KEY++;
131 var KEY_AVG_RUN_TIME = END_KEY++;
132 var KEY_MAX_RUN_TIME = END_KEY++;
133 var KEY_QUEUE_TIME = END_KEY++;
134 var KEY_AVG_QUEUE_TIME = END_KEY++;
135 var KEY_MAX_QUEUE_TIME = END_KEY++;
136 var KEY_BIRTH_THREAD = END_KEY++;
137 var KEY_DEATH_THREAD = END_KEY++;
138 var KEY_PROCESS_TYPE = END_KEY++;
139 var KEY_PROCESS_ID = END_KEY++;
140 var KEY_FUNCTION_NAME = END_KEY++;
141 var KEY_SOURCE_LOCATION = END_KEY++;
142 var KEY_FILE_NAME = END_KEY++;
143 var KEY_LINE_NUMBER = END_KEY++;
145 var NUM_KEYS = END_KEY - BEGIN_KEY;
147 // --------------------------------------------------------------------------
149 // --------------------------------------------------------------------------
151 // To generalize computing/displaying the aggregate "counts" for each column,
152 // we specify an optional "Aggregator" class to use with each property.
154 // The following are actually "Aggregator factories". They create an
155 // aggregator instance by calling 'create()'. The instance is then fed
156 // each row one at a time via the 'consume()' method. After all rows have
157 // been consumed, the 'getValueAsText()' method will return the aggregated
161 * This aggregator counts the number of unique values that were fed to it.
163 var UniquifyAggregator = (function() {
164 function Aggregator(key) {
166 this.valuesSet_ = {};
169 Aggregator.prototype = {
170 consume: function(e) {
171 this.valuesSet_[e[this.key_]] = true;
174 getValueAsText: function() {
175 return getDictionaryKeys(this.valuesSet_).length + ' unique';
180 create: function(key) { return new Aggregator(key); }
185 * This aggregator sums a numeric field.
187 var SumAggregator = (function() {
188 function Aggregator(key) {
193 Aggregator.prototype = {
194 consume: function(e) {
195 this.sum_ += e[this.key_];
198 getValue: function() {
202 getValueAsText: function() {
203 return formatNumberAsText(this.getValue());
208 create: function(key) { return new Aggregator(key); }
213 * This aggregator computes an average by summing two
214 * numeric fields, and then dividing the totals.
216 var AvgAggregator = (function() {
217 function Aggregator(numeratorKey, divisorKey) {
218 this.numeratorKey_ = numeratorKey;
219 this.divisorKey_ = divisorKey;
221 this.numeratorSum_ = 0;
222 this.divisorSum_ = 0;
225 Aggregator.prototype = {
226 consume: function(e) {
227 this.numeratorSum_ += e[this.numeratorKey_];
228 this.divisorSum_ += e[this.divisorKey_];
231 getValue: function() {
232 return this.numeratorSum_ / this.divisorSum_;
235 getValueAsText: function() {
236 return formatNumberAsText(this.getValue());
241 create: function(numeratorKey, divisorKey) {
243 create: function(key) {
244 return new Aggregator(numeratorKey, divisorKey);
252 * This aggregator finds the maximum for a numeric field.
254 var MaxAggregator = (function() {
255 function Aggregator(key) {
257 this.max_ = -Infinity;
260 Aggregator.prototype = {
261 consume: function(e) {
262 this.max_ = Math.max(this.max_, e[this.key_]);
265 getValue: function() {
269 getValueAsText: function() {
270 return formatNumberAsText(this.getValue());
275 create: function(key) { return new Aggregator(key); }
279 // --------------------------------------------------------------------------
281 // --------------------------------------------------------------------------
283 // Custom comparator for thread names (sorts main thread and IO thread
284 // higher than would happen lexicographically.)
285 var threadNameComparator =
286 createLexicographicComparatorWithExceptions([
290 'Chrome_HistoryThread',
295 function diffFuncForCount(a, b) {
299 function diffFuncForMax(a, b) {
304 * Enumerates information about various keys. Such as whether their data is
305 * expected to be numeric or is a string, a descriptive name (title) for the
306 * property, and what function should be used to aggregate the property when
307 * displayed in a column.
309 * --------------------------------------
310 * The following properties are required:
311 * --------------------------------------
313 * [name]: This is displayed as the column's label.
314 * [aggregator]: Aggregator factory that is used to compute an aggregate
315 * value for this column.
317 * --------------------------------------
318 * The following properties are optional:
319 * --------------------------------------
321 * [inputJsonKey]: The corresponding key for this property in the original
322 * JSON dictionary received from the browser. If this is
323 * present, values for this key will be automatically
324 * populated during import.
325 * [comparator]: A comparator function for sorting this column.
326 * [textPrinter]: A function that transforms values into the user-displayed
327 * text shown in the UI. If unspecified, will default to the
328 * "toString()" function.
329 * [cellAlignment]: The horizonal alignment to use for columns of this
330 * property (for instance 'right'). If unspecified will
331 * default to left alignment.
332 * [sortDescending]: When first clicking on this column, we will default to
333 * sorting by |comparator| in ascending order. If this
334 * property is true, we will reverse that to descending.
335 * [diff]: Function to call to compute a "difference" value between
336 * parameters (a, b). This is used when calculating the difference
337 * between two snapshots. Diffing numeric quantities generally
338 * involves subtracting, but some fields like max may need to do
339 * something different.
341 var KEY_PROPERTIES = [];
343 KEY_PROPERTIES[KEY_PROCESS_ID] = {
345 cellAlignment: 'right',
346 aggregator: UniquifyAggregator,
349 KEY_PROPERTIES[KEY_PROCESS_TYPE] = {
350 name: 'Process type',
351 aggregator: UniquifyAggregator,
354 KEY_PROPERTIES[KEY_BIRTH_THREAD] = {
355 name: 'Birth thread',
356 inputJsonKey: 'birth_thread',
357 aggregator: UniquifyAggregator,
358 comparator: threadNameComparator,
361 KEY_PROPERTIES[KEY_DEATH_THREAD] = {
363 inputJsonKey: 'death_thread',
364 aggregator: UniquifyAggregator,
365 comparator: threadNameComparator,
368 KEY_PROPERTIES[KEY_FUNCTION_NAME] = {
369 name: 'Function name',
370 inputJsonKey: 'birth_location.function_name',
371 aggregator: UniquifyAggregator,
374 KEY_PROPERTIES[KEY_FILE_NAME] = {
376 inputJsonKey: 'birth_location.file_name',
377 aggregator: UniquifyAggregator,
380 KEY_PROPERTIES[KEY_LINE_NUMBER] = {
382 cellAlignment: 'right',
383 inputJsonKey: 'birth_location.line_number',
384 aggregator: UniquifyAggregator,
387 KEY_PROPERTIES[KEY_COUNT] = {
389 cellAlignment: 'right',
390 sortDescending: true,
391 textPrinter: formatNumberAsText,
392 inputJsonKey: 'death_data.count',
393 aggregator: SumAggregator,
394 diff: diffFuncForCount,
397 KEY_PROPERTIES[KEY_QUEUE_TIME] = {
398 name: 'Total queue time',
399 cellAlignment: 'right',
400 sortDescending: true,
401 textPrinter: formatNumberAsText,
402 inputJsonKey: 'death_data.queue_ms',
403 aggregator: SumAggregator,
404 diff: diffFuncForCount,
407 KEY_PROPERTIES[KEY_MAX_QUEUE_TIME] = {
408 name: 'Max queue time',
409 cellAlignment: 'right',
410 sortDescending: true,
411 textPrinter: formatNumberAsText,
412 inputJsonKey: 'death_data.queue_ms_max',
413 aggregator: MaxAggregator,
414 diff: diffFuncForMax,
417 KEY_PROPERTIES[KEY_RUN_TIME] = {
418 name: 'Total run time',
419 cellAlignment: 'right',
420 sortDescending: true,
421 textPrinter: formatNumberAsText,
422 inputJsonKey: 'death_data.run_ms',
423 aggregator: SumAggregator,
424 diff: diffFuncForCount,
427 KEY_PROPERTIES[KEY_AVG_RUN_TIME] = {
428 name: 'Avg run time',
429 cellAlignment: 'right',
430 sortDescending: true,
431 textPrinter: formatNumberAsText,
432 aggregator: AvgAggregator.create(KEY_RUN_TIME, KEY_COUNT),
435 KEY_PROPERTIES[KEY_MAX_RUN_TIME] = {
436 name: 'Max run time',
437 cellAlignment: 'right',
438 sortDescending: true,
439 textPrinter: formatNumberAsText,
440 inputJsonKey: 'death_data.run_ms_max',
441 aggregator: MaxAggregator,
442 diff: diffFuncForMax,
445 KEY_PROPERTIES[KEY_AVG_QUEUE_TIME] = {
446 name: 'Avg queue time',
447 cellAlignment: 'right',
448 sortDescending: true,
449 textPrinter: formatNumberAsText,
450 aggregator: AvgAggregator.create(KEY_QUEUE_TIME, KEY_COUNT),
453 KEY_PROPERTIES[KEY_SOURCE_LOCATION] = {
454 name: 'Source location',
456 aggregator: UniquifyAggregator,
460 * Returns the string name for |key|.
462 function getNameForKey(key) {
463 var props = KEY_PROPERTIES[key];
464 if (props == undefined)
465 throw 'Did not define properties for key: ' + key;
470 * Ordered list of all keys. This is the order we generally want
471 * to display the properties in. Default to declaration order.
474 for (var k = BEGIN_KEY; k < END_KEY; ++k)
477 // --------------------------------------------------------------------------
479 // --------------------------------------------------------------------------
482 * List of keys for those properties which we want to initially omit
483 * from the table. (They can be re-enabled by clicking [Edit columns]).
485 var INITIALLY_HIDDEN_KEYS = [
492 * The ordered list of grouping choices to expose in the "Group by"
493 * dropdowns. We don't include the numeric properties, since they
494 * leads to awkward bucketing.
496 var GROUPING_DROPDOWN_CHOICES = [
508 * The ordered list of sorting choices to expose in the "Sort by"
511 var SORT_DROPDOWN_CHOICES = ALL_KEYS;
514 * The ordered list of all columns that can be displayed in the tables (not
515 * including whatever has been hidden via [Edit Columns]).
517 var ALL_TABLE_COLUMNS = ALL_KEYS;
520 * The initial keys to sort by when loading the page (can be changed later).
522 var INITIAL_SORT_KEYS = [-KEY_COUNT];
525 * The default sort keys to use when nothing has been specified.
527 var DEFAULT_SORT_KEYS = [-KEY_COUNT];
530 * The initial keys to group by when loading the page (can be changed later).
532 var INITIAL_GROUP_KEYS = [];
535 * The columns to give the option to merge on.
537 var MERGEABLE_KEYS = [
545 * The columns to merge by default.
547 var INITIALLY_MERGED_KEYS = [];
550 * The full set of columns which define the "identity" for a row. A row is
551 * considered equivalent to another row if it matches on all of these
552 * fields. This list is used when merging the data, to determine which rows
553 * should be merged together. The remaining columns not listed in
554 * IDENTITY_KEYS will be aggregated.
556 var IDENTITY_KEYS = [
568 * The time (in milliseconds) to wait after receiving new data before
569 * re-drawing it to the screen. The reason we wait a bit is to avoid
570 * repainting repeatedly during the loading phase (which can slow things
571 * down). Note that this only slows down the addition of new data. It does
572 * not impact the latency of user-initiated operations like sorting or
575 var PROCESS_DATA_DELAY_MS = 500;
578 * The initial number of rows to display (the rest are hidden) when no
579 * grouping is selected. We use a higher limit than when grouping is used
580 * since there is a lot of vertical real estate.
582 var INITIAL_UNGROUPED_ROW_LIMIT = 30;
585 * The initial number of rows to display (rest are hidden) for each group.
587 var INITIAL_GROUP_ROW_LIMIT = 10;
590 * The number of extra rows to show/hide when clicking the "Show more" or
591 * "Show less" buttons.
593 var LIMIT_INCREMENT = 10;
595 // --------------------------------------------------------------------------
596 // General utility functions
597 // --------------------------------------------------------------------------
600 * Returns a list of all the keys in |dict|.
602 function getDictionaryKeys(dict) {
604 for (var key in dict) {
611 * Formats the number |x| as a decimal integer. Strips off any decimal parts,
612 * and comma separates the number every 3 characters.
614 function formatNumberAsText(x) {
615 var orig = x.toFixed(0);
618 for (var end = orig.length; end > 0; ) {
619 var chunk = Math.min(end, 3);
620 parts.push(orig.substr(end - chunk, chunk));
623 return parts.reverse().join(',');
627 * Simple comparator function which works for both strings and numbers.
629 function simpleCompare(a, b) {
638 * Returns a comparator function that compares values lexicographically,
639 * but special-cases the values in |orderedList| to have a higher
642 function createLexicographicComparatorWithExceptions(orderedList) {
643 var valueToRankMap = {};
644 for (var i = 0; i < orderedList.length; ++i)
645 valueToRankMap[orderedList[i]] = i;
647 function getCustomRank(x) {
648 var rank = valueToRankMap[x];
649 if (rank == undefined)
650 rank = Infinity; // Unmatched.
654 return function(a, b) {
655 var aRank = getCustomRank(a);
656 var bRank = getCustomRank(b);
658 // Not matched by any of our exceptions.
660 return simpleCompare(a, b);
669 * Returns dict[key]. Note that if |key| contains periods (.), they will be
670 * interpreted as meaning a sub-property.
672 function getPropertyByPath(dict, key) {
674 var parts = key.split('.');
675 for (var i = 0; i < parts.length; ++i) {
676 if (cur == undefined)
684 * Creates and appends a DOM node of type |tagName| to |parent|. Optionally,
685 * sets the new node's text to |opt_text|. Returns the newly created node.
687 function addNode(parent, tagName, opt_text) {
688 var n = parent.ownerDocument.createElement(tagName);
689 parent.appendChild(n);
690 if (opt_text != undefined) {
691 addText(n, opt_text);
697 * Adds |text| to |parent|.
699 function addText(parent, text) {
700 var textNode = parent.ownerDocument.createTextNode(text);
701 parent.appendChild(textNode);
706 * Deletes all the strings in |array| which appear in |valuesToDelete|.
708 function deleteValuesFromArray(array, valuesToDelete) {
709 var valueSet = arrayToSet(valuesToDelete);
710 for (var i = 0; i < array.length; ) {
711 if (valueSet[array[i]]) {
720 * Deletes all the repeated ocurrences of strings in |array|.
722 function deleteDuplicateStringsFromArray(array) {
723 // Build up set of each entry in array.
726 for (var i = 0; i < array.length; ) {
727 var value = array[i];
728 if (seenSoFar[value]) {
731 seenSoFar[value] = true;
738 * Builds a map out of the array |list|.
740 function arrayToSet(list) {
742 for (var i = 0; i < list.length; ++i)
747 function trimWhitespace(text) {
748 var m = /^\s*(.*)\s*$/.exec(text);
753 * Selects the option in |select| which has a value of |value|.
755 function setSelectedOptionByValue(select, value) {
756 for (var i = 0; i < select.options.length; ++i) {
757 if (select.options[i].value == value) {
758 select.options[i].selected = true;
766 * Adds a checkbox to |parent|. The checkbox will have a label on its right
767 * with text |label|. Returns the checkbox input node.
769 function addLabeledCheckbox(parent, label) {
770 var labelNode = addNode(parent, 'label');
771 var checkbox = addNode(labelNode, 'input');
772 checkbox.type = 'checkbox';
773 addText(labelNode, label);
778 * Return the last component in a path which is separated by either forward
779 * slashes or backslashes.
781 function getFilenameFromPath(path) {
782 var lastSlash = Math.max(path.lastIndexOf('/'),
783 path.lastIndexOf('\\'));
787 return path.substr(lastSlash + 1);
791 * Returns the current time in milliseconds since unix epoch.
793 function getTimeMillis() {
794 return (new Date()).getTime();
798 * Toggle a node between hidden/invisible.
800 function toggleNodeDisplay(n) {
801 if (n.style.display == '') {
802 n.style.display = 'none';
804 n.style.display = '';
809 * Set the visibility state of a node.
811 function setNodeDisplay(n, visible) {
813 n.style.display = '';
815 n.style.display = 'none';
819 // --------------------------------------------------------------------------
820 // Functions that augment, bucket, and compute aggregates for the input data.
821 // --------------------------------------------------------------------------
824 * Adds new derived properties to row. Mutates the provided dictionary |e|.
826 function augmentDataRow(e) {
827 computeDataRowAverages(e);
828 e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']';
831 function computeDataRowAverages(e) {
832 e[KEY_AVG_QUEUE_TIME] = e[KEY_QUEUE_TIME] / e[KEY_COUNT];
833 e[KEY_AVG_RUN_TIME] = e[KEY_RUN_TIME] / e[KEY_COUNT];
837 * Creates and initializes an aggregator object for each key in |columns|.
838 * Returns an array whose keys are values from |columns|, and whose
839 * values are Aggregator instances.
841 function initializeAggregates(columns) {
844 for (var i = 0; i < columns.length; ++i) {
845 var key = columns[i];
846 var aggregatorFactory = KEY_PROPERTIES[key].aggregator;
847 aggregates[key] = aggregatorFactory.create(key);
853 function consumeAggregates(aggregates, row) {
854 for (var key in aggregates)
855 aggregates[key].consume(row);
858 function bucketIdenticalRows(rows, identityKeys, propertyGetterFunc) {
859 var identicalRows = {};
860 for (var i = 0; i < rows.length; ++i) {
863 var rowIdentity = [];
864 for (var j = 0; j < identityKeys.length; ++j)
865 rowIdentity.push(propertyGetterFunc(r, identityKeys[j]));
866 rowIdentity = rowIdentity.join('\n');
868 var l = identicalRows[rowIdentity];
871 identicalRows[rowIdentity] = l;
875 return identicalRows;
879 * Merges the rows in |origRows|, by collapsing the columns listed in
880 * |mergeKeys|. Returns an array with the merged rows (in no particular
883 * If |mergeSimilarThreads| is true, then threads with a similar name will be
884 * considered equivalent. For instance, "WorkerThread-1" and "WorkerThread-2"
885 * will be remapped to "WorkerThread-*".
887 * If |outputAsDictionary| is false then the merged rows will be returned as a
888 * flat list. Otherwise the result will be a dictionary, where each row
891 function mergeRows(origRows, mergeKeys, mergeSimilarThreads,
892 outputAsDictionary) {
893 // Define a translation function for each property. Normally we copy over
894 // properties as-is, but if we have been asked to "merge similar threads" we
895 // we will remap the thread names that end in a numeric suffix.
896 var propertyGetterFunc;
898 if (mergeSimilarThreads) {
899 propertyGetterFunc = function(row, key) {
900 var value = row[key];
901 // If the property is a thread name, try to remap it.
902 if (key == KEY_BIRTH_THREAD || key == KEY_DEATH_THREAD) {
903 var m = /^(.*[^\d])(\d+)$/.exec(value);
910 propertyGetterFunc = function(row, key) { return row[key]; };
913 // Determine which sets of properties a row needs to match on to be
914 // considered identical to another row.
915 var identityKeys = IDENTITY_KEYS.slice(0);
916 deleteValuesFromArray(identityKeys, mergeKeys);
918 // Set |aggregateKeys| to everything else, since we will be aggregating
919 // their value as part of the merge.
920 var aggregateKeys = ALL_KEYS.slice(0);
921 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
922 deleteValuesFromArray(aggregateKeys, mergeKeys);
924 // Group all the identical rows together, bucketed into |identicalRows|.
926 bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc);
928 var mergedRows = outputAsDictionary ? {} : [];
930 // Merge the rows and save the results to |mergedRows|.
931 for (var k in identicalRows) {
932 // We need to smash the list |l| down to a single row...
933 var l = identicalRows[k];
937 if (outputAsDictionary) {
938 mergedRows[k] = newRow;
940 mergedRows.push(newRow);
943 // Copy over all the identity columns to the new row (since they
944 // were the same for each row matched).
945 for (var i = 0; i < identityKeys.length; ++i)
946 newRow[identityKeys[i]] = propertyGetterFunc(l[0], identityKeys[i]);
948 // Compute aggregates for the other columns.
949 var aggregates = initializeAggregates(aggregateKeys);
951 // Feed the rows to the aggregators.
952 for (var i = 0; i < l.length; ++i)
953 consumeAggregates(aggregates, l[i]);
955 // Suck out the data generated by the aggregators.
956 for (var aggregateKey in aggregates)
957 newRow[aggregateKey] = aggregates[aggregateKey].getValue();
964 * Takes two dictionaries data1 and data2, and returns a new flat list which
965 * represents the difference between them. The exact meaning of "difference"
966 * is column specific, but for most numeric fields (like the count, or total
967 * time), it is found by subtracting.
969 * Rows in data1 and data2 are expected to use the same scheme for the keys.
970 * In other words, data1[k] is considered the analagous row to data2[k].
972 function subtractSnapshots(data1, data2, columnsToExclude) {
973 // These columns are computed from the other columns. We won't bother
974 // diffing/aggregating these, but rather will derive them again from the
976 var COMPUTED_AGGREGATE_KEYS = [KEY_AVG_QUEUE_TIME, KEY_AVG_RUN_TIME];
978 // These are the keys which determine row equality. Since we are not doing
979 // any merging yet at this point, it is simply the list of all identity
981 var identityKeys = IDENTITY_KEYS.slice(0);
982 deleteValuesFromArray(identityKeys, columnsToExclude);
984 // The columns to compute via aggregation is everything else.
985 var aggregateKeys = ALL_KEYS.slice(0);
986 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
987 deleteValuesFromArray(aggregateKeys, COMPUTED_AGGREGATE_KEYS);
988 deleteValuesFromArray(aggregateKeys, columnsToExclude);
992 for (var rowId in data2) {
993 var row1 = data1[rowId];
994 var row2 = data2[rowId];
998 // Copy over all the identity columns to the new row (since they
999 // were the same for each row matched).
1000 for (var i = 0; i < identityKeys.length; ++i)
1001 newRow[identityKeys[i]] = row2[identityKeys[i]];
1003 // Diff the two rows.
1005 for (var i = 0; i < aggregateKeys.length; ++i) {
1006 var aggregateKey = aggregateKeys[i];
1007 var a = row1[aggregateKey];
1008 var b = row2[aggregateKey];
1010 var diffFunc = KEY_PROPERTIES[aggregateKey].diff;
1011 newRow[aggregateKey] = diffFunc(a, b);
1014 // If the the row doesn't appear in snapshot1, then there is nothing to
1015 // diff, so just copy row2 as is.
1016 for (var i = 0; i < aggregateKeys.length; ++i) {
1017 var aggregateKey = aggregateKeys[i];
1018 newRow[aggregateKey] = row2[aggregateKey];
1022 if (newRow[KEY_COUNT] == 0) {
1023 // If a row's count has gone to zero, it means there were no new
1024 // occurrences of it in the second snapshot, so remove it.
1028 // Since we excluded the averages during the diffing phase, re-compute
1029 // them using the diffed totals.
1030 computeDataRowAverages(newRow);
1031 diffedRows.push(newRow);
1037 // --------------------------------------------------------------------------
1038 // HTML drawing code
1039 // --------------------------------------------------------------------------
1041 function getTextValueForProperty(key, value) {
1042 if (value == undefined) {
1043 // A value may be undefined as a result of having merging rows. We
1044 // won't actually draw it, but this might be called by the filter.
1048 var textPrinter = KEY_PROPERTIES[key].textPrinter;
1050 return textPrinter(value);
1051 return value.toString();
1055 * Renders the property value |value| into cell |td|. The name of this
1056 * property is |key|.
1058 function drawValueToCell(td, key, value) {
1059 // Get a text representation of the value.
1060 var text = getTextValueForProperty(key, value);
1062 // Apply the desired cell alignment.
1063 var cellAlignment = KEY_PROPERTIES[key].cellAlignment;
1065 td.align = cellAlignment;
1067 if (key == KEY_SOURCE_LOCATION) {
1068 // Linkify the source column so it jumps to the source code. This doesn't
1069 // take into account the particular code this build was compiled from, or
1070 // local edits to source. It should however work correctly for top of tree
1072 var m = /^(.*) \[(\d+)\]$/.exec(text);
1074 var filepath = m[1];
1075 var filename = getFilenameFromPath(filepath);
1076 var linenumber = m[2];
1078 var link = addNode(td, 'a', filename + ' [' + linenumber + ']');
1079 // http://chromesrc.appspot.com is a server I wrote specifically for
1080 // this task. It redirects to the appropriate source file; the file
1081 // paths given by the compiler can be pretty crazy and different
1082 // between platforms.
1083 link.href = 'http://chromesrc.appspot.com/?path=' +
1084 encodeURIComponent(filepath) + '&line=' + linenumber;
1085 link.target = '_blank';
1090 // String values can get pretty long. If the string contains no spaces, then
1091 // CSS fails to wrap it, and it overflows the cell causing the table to get
1092 // really big. We solve this using a hack: insert a <wbr> element after
1093 // every single character. This will allow the rendering engine to wrap the
1094 // value, and hence avoid it overflowing!
1095 var kMinLengthBeforeWrap = 20;
1097 addText(td, text.substr(0, kMinLengthBeforeWrap));
1098 for (var i = kMinLengthBeforeWrap; i < text.length; ++i) {
1100 addText(td, text.substr(i, 1));
1104 // --------------------------------------------------------------------------
1105 // Helper code for handling the sort and grouping dropdowns.
1106 // --------------------------------------------------------------------------
1108 function addOptionsForGroupingSelect(select) {
1109 // Add "no group" choice.
1110 addNode(select, 'option', '---').value = '';
1112 for (var i = 0; i < GROUPING_DROPDOWN_CHOICES.length; ++i) {
1113 var key = GROUPING_DROPDOWN_CHOICES[i];
1114 var option = addNode(select, 'option', getNameForKey(key));
1119 function addOptionsForSortingSelect(select) {
1120 // Add "no sort" choice.
1121 addNode(select, 'option', '---').value = '';
1124 addNode(select, 'optgroup').label = '';
1126 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1127 var key = SORT_DROPDOWN_CHOICES[i];
1128 addNode(select, 'option', getNameForKey(key)).value = key;
1132 addNode(select, 'optgroup').label = '';
1134 // Add the same options, but for descending.
1135 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1136 var key = SORT_DROPDOWN_CHOICES[i];
1137 var n = addNode(select, 'option', getNameForKey(key) + ' (DESC)');
1138 n.value = reverseSortKey(key);
1143 * Helper function used to update the sorting and grouping lists after a
1146 function updateKeyListFromDropdown(list, i, select) {
1148 if (i < list.length) {
1149 list[i] = select.value;
1151 list.push(select.value);
1154 // Normalize the list, so setting 'none' as primary zeros out everything
1156 for (var i = 0; i < list.length; ++i) {
1157 if (list[i] == '') {
1158 list.splice(i, list.length - i);
1165 * Comparator for property |key|, having values |value1| and |value2|.
1166 * If the key has defined a custom comparator use it. Otherwise use a
1167 * default "less than" comparison.
1169 function compareValuesForKey(key, value1, value2) {
1170 var comparator = KEY_PROPERTIES[key].comparator;
1172 return comparator(value1, value2);
1173 return simpleCompare(value1, value2);
1176 function reverseSortKey(key) {
1180 function sortKeyIsReversed(key) {
1184 function sortKeysMatch(key1, key2) {
1185 return Math.abs(key1) == Math.abs(key2);
1188 function getKeysForCheckedBoxes(checkboxes) {
1190 for (var k in checkboxes) {
1191 if (checkboxes[k].checked)
1197 // --------------------------------------------------------------------------
1202 function MainView() {
1203 // Make sure we have a definition for each key.
1204 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1205 if (!KEY_PROPERTIES[k])
1206 throw 'KEY_PROPERTIES[] not defined for key: ' + k;
1212 MainView.prototype = {
1213 addDataToSnapshot: function(data) {
1214 // TODO(eroman): We need to know which snapshot this data belongs to!
1215 // For now we assume it is the most recent snapshot.
1216 var snapshotIndex = this.snapshots_.length - 1;
1218 var snapshot = this.snapshots_[snapshotIndex];
1220 var pid = data.process_id;
1221 var ptype = data.process_type;
1223 // Save the browser's representation of the data
1224 snapshot.origData.push(data);
1226 // Augment each data row with the process information.
1227 var rows = data.list;
1228 for (var i = 0; i < rows.length; ++i) {
1229 // Transform the data from a dictionary to an array. This internal
1230 // representation is more compact and faster to access.
1231 var origRow = rows[i];
1234 newRow[KEY_PROCESS_ID] = pid;
1235 newRow[KEY_PROCESS_TYPE] = ptype;
1237 // Copy over the known properties which have a 1:1 mapping with JSON.
1238 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1239 var inputJsonKey = KEY_PROPERTIES[k].inputJsonKey;
1240 if (inputJsonKey != undefined) {
1241 newRow[k] = getPropertyByPath(origRow, inputJsonKey);
1245 if (newRow[KEY_COUNT] == 0) {
1246 // When resetting the data, it is possible for the backend to give us
1247 // counts of "0". There is no point adding these rows (in fact they
1248 // will cause us to do divide by zeros when calculating averages and
1249 // stuff), so we skip past them.
1253 // Add our computed properties.
1254 augmentDataRow(newRow);
1256 snapshot.flatData.push(newRow);
1259 if (!arrayToSet(this.getSelectedSnapshotIndexes_())[snapshotIndex]) {
1260 // Optimization: If this snapshot is not a data dependency for the
1261 // current display, then don't bother updating anything.
1265 // We may end up calling addDataToSnapshot_() repeatedly (once for each
1266 // process). To avoid this from slowing us down we do bulk updates on a
1268 this.updateMergedDataSoon_();
1271 updateMergedDataSoon_: function() {
1272 if (this.updateMergedDataPending_) {
1273 // If a delayed task has already been posted to re-merge the data,
1274 // then we don't need to do anything extra.
1278 // Otherwise schedule updateMergedData_() to be called later. We want it
1279 // to be called no more than once every PROCESS_DATA_DELAY_MS
1282 if (this.lastUpdateMergedDataTime_ == undefined)
1283 this.lastUpdateMergedDataTime_ = 0;
1285 var timeSinceLastMerge = getTimeMillis() - this.lastUpdateMergedDataTime_;
1286 var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge);
1288 var functionToRun = function() {
1289 // Do the actual update.
1290 this.updateMergedData_();
1291 // Keep track of when we last ran.
1292 this.lastUpdateMergedDataTime_ = getTimeMillis();
1293 this.updateMergedDataPending_ = false;
1296 this.updateMergedDataPending_ = true;
1297 window.setTimeout(functionToRun, timeToWait);
1301 * Returns a list of the currently selected snapshots. This list is
1302 * guaranteed to be of length 1 or 2.
1304 getSelectedSnapshotIndexes_: function() {
1305 var indexes = this.getSelectedSnapshotBoxes_();
1306 for (var i = 0; i < indexes.length; ++i)
1307 indexes[i] = indexes[i].__index;
1312 * Same as getSelectedSnapshotIndexes_(), only it returns the actual
1313 * checkbox input DOM nodes rather than the snapshot ID.
1315 getSelectedSnapshotBoxes_: function() {
1316 // Figure out which snaphots to use for our data.
1318 for (var i = 0; i < this.snapshots_.length; ++i) {
1319 var box = this.getSnapshotCheckbox_(i);
1327 * Re-draw the description that explains which snapshots are currently
1328 * selected (if two snapshots were selected we explain that the *difference*
1329 * between them is being displayed).
1331 updateSnapshotSelectionSummaryDiv_: function() {
1332 var summaryDiv = $(SNAPSHOT_SELECTION_SUMMARY_ID);
1334 var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1335 if (selectedSnapshots.length == 0) {
1336 // This can occur during an attempt to load a file or following file
1337 // load failure. We just ignore it and move on.
1338 } else if (selectedSnapshots.length == 1) {
1339 // If only one snapshot is chosen then we will display that snapshot's
1340 // data in its entirety.
1341 this.flatData_ = this.snapshots_[selectedSnapshots[0]].flatData;
1343 // Don't bother displaying any text when just 1 snapshot is selected,
1344 // since it is obvious what this should do.
1345 summaryDiv.innerText = '';
1346 } else if (selectedSnapshots.length == 2) {
1347 // Otherwise if two snapshots were chosen, show the difference between
1349 var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1350 var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1352 var timeDeltaInSeconds =
1353 ((snapshot2.time - snapshot1.time) / 1000).toFixed(0);
1355 // Explain that what is being shown is the difference between two
1357 summaryDiv.innerText =
1358 'Showing the difference between snapshots #' +
1359 selectedSnapshots[0] + ' and #' +
1360 selectedSnapshots[1] + ' (' + timeDeltaInSeconds +
1361 ' seconds worth of data)';
1363 // This shouldn't be possible...
1364 throw 'Unexpected number of selected snapshots';
1368 updateMergedData_: function() {
1369 // Retrieve the merge options.
1370 var mergeColumns = this.getMergeColumns_();
1371 var shouldMergeSimilarThreads = this.shouldMergeSimilarThreads_();
1373 var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1375 // We do merges a bit differently depending if we are displaying the diffs
1376 // between two snapshots, or just displaying a single snapshot.
1377 if (selectedSnapshots.length == 1) {
1378 var snapshot = this.snapshots_[selectedSnapshots[0]];
1379 this.mergedData_ = mergeRows(snapshot.flatData,
1381 shouldMergeSimilarThreads,
1384 } else if (selectedSnapshots.length == 2) {
1385 var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1386 var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1388 // Merge the data for snapshot1.
1389 var mergedRows1 = mergeRows(snapshot1.flatData,
1391 shouldMergeSimilarThreads,
1394 // Merge the data for snapshot2.
1395 var mergedRows2 = mergeRows(snapshot2.flatData,
1397 shouldMergeSimilarThreads,
1400 // Do a diff between the two snapshots.
1401 this.mergedData_ = subtractSnapshots(mergedRows1,
1405 throw 'Unexpected number of selected snapshots';
1408 // Recompute filteredData_ (since it is derived from mergedData_)
1409 this.updateFilteredData_();
1412 updateFilteredData_: function() {
1413 // Recompute filteredData_.
1414 this.filteredData_ = [];
1415 var filterFunc = this.getFilterFunction_();
1416 for (var i = 0; i < this.mergedData_.length; ++i) {
1417 var r = this.mergedData_[i];
1418 if (!filterFunc(r)) {
1419 // Not matched by our filter, discard.
1422 this.filteredData_.push(r);
1425 // Recompute groupedData_ (since it is derived from filteredData_)
1426 this.updateGroupedData_();
1429 updateGroupedData_: function() {
1430 // Recompute groupedData_.
1431 var groupKeyToData = {};
1432 var entryToGroupKeyFunc = this.getGroupingFunction_();
1433 for (var i = 0; i < this.filteredData_.length; ++i) {
1434 var r = this.filteredData_[i];
1436 var groupKey = entryToGroupKeyFunc(r);
1438 var groupData = groupKeyToData[groupKey];
1441 key: JSON.parse(groupKey),
1442 aggregates: initializeAggregates(ALL_KEYS),
1445 groupKeyToData[groupKey] = groupData;
1448 // Add the row to our list.
1449 groupData.rows.push(r);
1451 // Update aggregates for each column.
1452 consumeAggregates(groupData.aggregates, r);
1454 this.groupedData_ = groupKeyToData;
1456 // Figure out a display order for the groups themselves.
1457 this.sortedGroupKeys_ = getDictionaryKeys(groupKeyToData);
1458 this.sortedGroupKeys_.sort(this.getGroupSortingFunction_());
1460 // Sort the group data.
1461 this.sortGroupedData_();
1464 sortGroupedData_: function() {
1465 var sortingFunc = this.getSortingFunction_();
1466 for (var k in this.groupedData_)
1467 this.groupedData_[k].rows.sort(sortingFunc);
1469 // Every cached data dependency is now up to date, all that is left is
1470 // to actually draw the result.
1474 getVisibleColumnKeys_: function() {
1475 // Figure out what columns to include, based on the selected checkboxes.
1476 var columns = this.getSelectionColumns_();
1477 columns = columns.slice(0);
1479 // Eliminate columns which we are merging on.
1480 deleteValuesFromArray(columns, this.getMergeColumns_());
1482 // Eliminate columns which we are grouped on.
1483 if (this.sortedGroupKeys_.length > 0) {
1484 // The grouping will be the the same for each so just pick the first.
1485 var randomGroupKey = this.groupedData_[this.sortedGroupKeys_[0]].key;
1487 // The grouped properties are going to be the same for each row in our,
1488 // table, so avoid drawing them in our table!
1489 var keysToExclude = [];
1491 for (var i = 0; i < randomGroupKey.length; ++i)
1492 keysToExclude.push(randomGroupKey[i].key);
1493 deleteValuesFromArray(columns, keysToExclude);
1496 // If we are currently showing a "diff", hide the max columns, since we
1497 // are not populating it correctly. See the TODO at the top of this file.
1498 if (this.getSelectedSnapshotIndexes_().length > 1)
1499 deleteValuesFromArray(columns, [KEY_MAX_RUN_TIME, KEY_MAX_QUEUE_TIME]);
1504 redrawData_: function() {
1505 // Clear the results div, sine we may be overwriting older data.
1506 var parent = $(RESULTS_DIV_ID);
1507 parent.innerHTML = '';
1509 var columns = this.getVisibleColumnKeys_();
1512 for (var i = 0; i < this.sortedGroupKeys_.length; ++i) {
1513 var k = this.sortedGroupKeys_[i];
1514 this.drawGroup_(parent, k, columns);
1519 * Renders the information for a particular group.
1521 drawGroup_: function(parent, groupKey, columns) {
1522 var groupData = this.groupedData_[groupKey];
1524 var div = addNode(parent, 'div');
1525 div.className = 'group-container';
1527 this.drawGroupTitle_(div, groupData.key);
1529 var table = addNode(div, 'table');
1531 this.drawDataTable_(table, groupData, columns, groupKey);
1535 * Draws a title into |parent| that describes |groupKey|.
1537 drawGroupTitle_: function(parent, groupKey) {
1538 if (groupKey.length == 0) {
1539 // Empty group key means there was no grouping.
1543 var parent = addNode(parent, 'div');
1544 parent.className = 'group-title-container';
1546 // Each component of the group key represents the "key=value" constraint
1547 // for this group. Show these as an AND separated list.
1548 for (var i = 0; i < groupKey.length; ++i) {
1550 addNode(parent, 'i', ' and ');
1551 var e = groupKey[i];
1552 addNode(parent, 'b', getNameForKey(e.key) + ' = ');
1553 addNode(parent, 'span', e.value);
1558 * Renders a table which summarizes all |column| fields for |data|.
1560 drawDataTable_: function(table, data, columns, groupKey) {
1561 table.className = 'results-table';
1562 var thead = addNode(table, 'thead');
1563 var tbody = addNode(table, 'tbody');
1565 var displaySettings = this.getGroupDisplaySettings_(groupKey);
1566 var limit = displaySettings.limit;
1568 this.drawAggregateRow_(thead, data.aggregates, columns);
1569 this.drawTableHeader_(thead, columns);
1570 this.drawTableBody_(tbody, data.rows, columns, limit);
1571 this.drawTruncationRow_(tbody, data.rows.length, limit, columns.length,
1575 drawTableHeader_: function(thead, columns) {
1576 var tr = addNode(thead, 'tr');
1577 for (var i = 0; i < columns.length; ++i) {
1578 var key = columns[i];
1579 var th = addNode(tr, 'th', getNameForKey(key));
1580 th.onclick = this.onClickColumn_.bind(this, key);
1582 // Draw an indicator if we are currently sorted on this column.
1583 // TODO(eroman): Should use an icon instead of asterisk!
1584 for (var j = 0; j < this.currentSortKeys_.length; ++j) {
1585 if (sortKeysMatch(this.currentSortKeys_[j], key)) {
1586 var sortIndicator = addNode(th, 'span', '*');
1587 sortIndicator.style.color = 'red';
1588 if (sortKeyIsReversed(this.currentSortKeys_[j])) {
1589 // Use double-asterisk for descending columns.
1590 addText(sortIndicator, '*');
1598 drawTableBody_: function(tbody, rows, columns, limit) {
1599 for (var i = 0; i < rows.length && i < limit; ++i) {
1602 var tr = addNode(tbody, 'tr');
1604 for (var c = 0; c < columns.length; ++c) {
1605 var key = columns[c];
1608 var td = addNode(tr, 'td');
1609 drawValueToCell(td, key, value);
1615 * Renders a row that describes all the aggregate values for |columns|.
1617 drawAggregateRow_: function(tbody, aggregates, columns) {
1618 var tr = addNode(tbody, 'tr');
1619 tr.className = 'aggregator-row';
1621 for (var i = 0; i < columns.length; ++i) {
1622 var key = columns[i];
1623 var td = addNode(tr, 'td');
1625 // Most of our outputs are numeric, so we want to align them to the
1626 // right. However for the unique counts we will center.
1627 if (KEY_PROPERTIES[key].aggregator == UniquifyAggregator) {
1628 td.align = 'center';
1633 var aggregator = aggregates[key];
1635 td.innerText = aggregator.getValueAsText();
1640 * Renders a row which describes how many rows the table has, how many are
1641 * currently hidden, and a set of buttons to show more.
1643 drawTruncationRow_: function(tbody, numRows, limit, numColumns, groupKey) {
1644 var numHiddenRows = Math.max(numRows - limit, 0);
1645 var numVisibleRows = numRows - numHiddenRows;
1647 var tr = addNode(tbody, 'tr');
1648 tr.className = 'truncation-row';
1649 var td = addNode(tr, 'td');
1650 td.colSpan = numColumns;
1652 addText(td, numRows + ' rows');
1653 if (numHiddenRows > 0) {
1654 var s = addNode(td, 'span', ' (' + numHiddenRows + ' hidden) ');
1655 s.style.color = 'red';
1658 if (numVisibleRows > LIMIT_INCREMENT) {
1659 addNode(td, 'button', 'Show less').onclick =
1660 this.changeGroupDisplayLimit_.bind(
1661 this, groupKey, -LIMIT_INCREMENT);
1663 if (numVisibleRows > 0) {
1664 addNode(td, 'button', 'Show none').onclick =
1665 this.changeGroupDisplayLimit_.bind(this, groupKey, -Infinity);
1668 if (numHiddenRows > 0) {
1669 addNode(td, 'button', 'Show more').onclick =
1670 this.changeGroupDisplayLimit_.bind(this, groupKey, LIMIT_INCREMENT);
1671 addNode(td, 'button', 'Show all').onclick =
1672 this.changeGroupDisplayLimit_.bind(this, groupKey, Infinity);
1677 * Adjusts the row limit for group |groupKey| by |delta|.
1679 changeGroupDisplayLimit_: function(groupKey, delta) {
1680 // Get the current settings for this group.
1681 var settings = this.getGroupDisplaySettings_(groupKey, true);
1683 // Compute the adjusted limit.
1684 var newLimit = settings.limit;
1685 var totalNumRows = this.groupedData_[groupKey].rows.length;
1686 newLimit = Math.min(totalNumRows, newLimit);
1688 newLimit = Math.max(0, newLimit);
1690 // Update the settings with the new limit.
1691 settings.limit = newLimit;
1693 // TODO(eroman): It isn't necessary to redraw *all* the data. Really we
1694 // just need to insert the missing rows (everything else stays the same)!
1699 * Returns the rendering settings for group |groupKey|. This includes things
1700 * like how many rows to display in the table.
1702 getGroupDisplaySettings_: function(groupKey, opt_create) {
1703 var settings = this.groupDisplaySettings_[groupKey];
1705 // If we don't have any settings for this group yet, create some
1707 if (groupKey == '[]') {
1708 // (groupKey of '[]' is what we use for ungrouped data).
1709 settings = {limit: INITIAL_UNGROUPED_ROW_LIMIT};
1711 settings = {limit: INITIAL_GROUP_ROW_LIMIT};
1714 this.groupDisplaySettings_[groupKey] = settings;
1720 this.snapshots_ = [];
1722 // Start fetching the data from the browser; this will be our snapshot #0.
1723 this.takeSnapshot_();
1725 // Data goes through the following pipeline:
1726 // (1) Raw data received from browser, and transformed into our own
1727 // internal row format (where properties are indexed by KEY_*
1729 // (2) We "augment" each row by adding some extra computed columns
1731 // (3) The rows are merged using current merge settings.
1732 // (4) The rows that don't match current search expression are
1734 // (5) The rows are organized into "groups" based on current settings,
1735 // and aggregate values are computed for each resulting group.
1736 // (6) The rows within each group are sorted using current settings.
1737 // (7) The grouped rows are drawn to the screen.
1738 this.mergedData_ = [];
1739 this.filteredData_ = [];
1740 this.groupedData_ = {};
1741 this.sortedGroupKeys_ = [];
1743 this.groupDisplaySettings_ = {};
1745 this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID));
1746 this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID));
1748 $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this);
1750 this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0);
1751 this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0);
1753 this.fillGroupingDropdowns_();
1754 this.fillSortingDropdowns_();
1756 $(EDIT_COLUMNS_LINK_ID).onclick =
1757 toggleNodeDisplay.bind(null, $(EDIT_COLUMNS_ROW));
1759 $(TOGGLE_SNAPSHOTS_LINK_ID).onclick =
1760 toggleNodeDisplay.bind(null, $(SNAPSHOTS_ROW));
1762 $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange =
1763 this.onMergeSimilarThreadsCheckboxChanged_.bind(this);
1765 $(RESET_DATA_LINK_ID).onclick =
1766 g_browserBridge.sendResetData.bind(g_browserBridge);
1768 $(TAKE_SNAPSHOT_BUTTON_ID).onclick = this.takeSnapshot_.bind(this);
1770 $(SAVE_SNAPSHOTS_BUTTON_ID).onclick = this.saveSnapshots_.bind(this);
1771 $(SNAPSHOT_FILE_LOADER_ID).onchange = this.loadFileChanged_.bind(this);
1774 takeSnapshot_: function() {
1775 // Start a new empty snapshot. Make note of the current time, so we know
1776 // when the snaphot was taken.
1777 this.snapshots_.push({flatData: [], origData: [], time: getTimeMillis()});
1779 // Update the UI to reflect the new snapshot.
1780 this.addSnapshotToList_(this.snapshots_.length - 1);
1782 // Ask the browser for the profiling data. We will receive the data
1783 // later through a callback to addDataToSnapshot_().
1784 g_browserBridge.sendGetData();
1787 saveSnapshots_: function() {
1789 for (var i = 0; i < this.snapshots_.length; ++i) {
1790 snapshots.push({ data: this.snapshots_[i].origData,
1791 timestamp: Math.floor(
1792 this.snapshots_[i].time / 1000) });
1796 'userAgent': navigator.userAgent,
1798 'snapshots': snapshots
1801 var dumpText = JSON.stringify(dump, null, ' ');
1802 var textBlob = new Blob([dumpText],
1803 { type: 'octet/stream', endings: 'native' });
1804 var blobUrl = window.URL.createObjectURL(textBlob);
1805 $(DOWNLOAD_ANCHOR_ID).href = blobUrl;
1806 $(DOWNLOAD_ANCHOR_ID).click();
1809 loadFileChanged_: function() {
1810 this.loadSnapshots_($(SNAPSHOT_FILE_LOADER_ID).files[0]);
1813 loadSnapshots_: function(file) {
1815 var fileReader = new FileReader();
1817 fileReader.onload = this.onLoadSnapshotsFile_.bind(this, file);
1818 fileReader.onerror = this.onLoadSnapshotsFileError_.bind(this, file);
1820 fileReader.readAsText(file);
1824 onLoadSnapshotsFile_: function(file, event) {
1827 parsed = JSON.parse(event.target.result);
1829 if (parsed.version != 1) {
1830 throw new Error('Unrecognized version: ' + parsed.version);
1833 if (parsed.snapshots.length < 1) {
1834 throw new Error('File contains no data');
1837 this.displayLoadedFile_(file, parsed);
1838 this.hideFileLoadError_();
1840 this.displayFileLoadError_('File load failure: ' + error.message);
1844 clearExistingSnapshots_: function() {
1845 var tbody = $('snapshots-tbody');
1846 this.snapshots_ = [];
1847 tbody.innerHTML = '';
1848 this.updateMergedDataSoon_();
1851 displayLoadedFile_: function(file, content) {
1852 this.clearExistingSnapshots_();
1853 $(TAKE_SNAPSHOT_BUTTON_ID).disabled = true;
1854 $(SAVE_SNAPSHOTS_BUTTON_ID).disabled = true;
1856 if (content.snapshots.length > 1) {
1857 setNodeDisplay($(SNAPSHOTS_ROW), true);
1860 for (var i = 0; i < content.snapshots.length; ++i) {
1861 var snapshot = content.snapshots[i];
1862 this.snapshots_.push({flatData: [], origData: [],
1863 time: snapshot.timestamp * 1000});
1864 this.addSnapshotToList_(this.snapshots_.length - 1);
1865 var snapshotData = snapshot.data;
1866 for (var j = 0; j < snapshotData.length; ++j) {
1867 this.addDataToSnapshot(snapshotData[j]);
1873 onLoadSnapshotsFileError_: function(file, filedata) {
1874 this.displayFileLoadError_('Error loading ' + file.name);
1877 displayFileLoadError_: function(message) {
1878 $(LOAD_ERROR_ID).textContent = message;
1879 $(LOAD_ERROR_ID).hidden = false;
1882 hideFileLoadError_: function() {
1883 $(LOAD_ERROR_ID).textContent = '';
1884 $(LOAD_ERROR_ID).hidden = true;
1887 getSnapshotCheckbox_: function(i) {
1888 return $(this.getSnapshotCheckboxId_(i));
1891 getSnapshotCheckboxId_: function(i) {
1892 return 'snapshotCheckbox-' + i;
1895 addSnapshotToList_: function(i) {
1896 var tbody = $('snapshots-tbody');
1898 var tr = addNode(tbody, 'tr');
1900 var id = this.getSnapshotCheckboxId_(i);
1902 var checkboxCell = addNode(tr, 'td');
1903 var checkbox = addNode(checkboxCell, 'input');
1904 checkbox.type = 'checkbox';
1906 checkbox.__index = i;
1907 checkbox.onclick = this.onSnapshotCheckboxChanged_.bind(this);
1909 addNode(tr, 'td', '#' + i);
1911 var labelCell = addNode(tr, 'td');
1912 var l = addNode(labelCell, 'label');
1914 var dateString = new Date(this.snapshots_[i].time).toLocaleString();
1915 addText(l, dateString);
1918 // If we are on snapshot 0, make it the default.
1920 checkbox.checked = true;
1921 checkbox.__time = getTimeMillis();
1922 this.updateSnapshotCheckboxStyling_();
1926 updateSnapshotCheckboxStyling_: function() {
1927 for (var i = 0; i < this.snapshots_.length; ++i) {
1928 var checkbox = this.getSnapshotCheckbox_(i);
1929 checkbox.parentNode.parentNode.className =
1930 checkbox.checked ? 'selected_snapshot' : '';
1934 onSnapshotCheckboxChanged_: function(event) {
1935 // Keep track of when we clicked this box (for when we need to uncheck
1937 event.target.__time = getTimeMillis();
1939 // Find all the checked boxes. Either 1 or 2 can be checked. If a third
1940 // was just checked, then uncheck one of the earlier ones so we only have
1942 var checked = this.getSelectedSnapshotBoxes_();
1943 checked.sort(function(a, b) { return b.__time - a.__time; });
1944 if (checked.length > 2) {
1945 for (var i = 2; i < checked.length; ++i)
1946 checked[i].checked = false;
1950 // We should always have at least 1 selection. Prevent the user from
1951 // unselecting the final box.
1952 if (checked.length == 0)
1953 event.target.checked = true;
1955 this.updateSnapshotCheckboxStyling_();
1956 this.updateSnapshotSelectionSummaryDiv_();
1958 // Recompute mergedData_ (since it is derived from selected snapshots).
1959 this.updateMergedData_();
1962 fillSelectionCheckboxes_: function(parent) {
1963 this.selectionCheckboxes_ = {};
1965 var onChangeFunc = this.onSelectCheckboxChanged_.bind(this);
1967 for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) {
1968 var key = ALL_TABLE_COLUMNS[i];
1969 var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1970 checkbox.checked = true;
1971 checkbox.onchange = onChangeFunc;
1972 addText(parent, ' ');
1973 this.selectionCheckboxes_[key] = checkbox;
1976 for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
1977 this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
1981 getSelectionColumns_: function() {
1982 return getKeysForCheckedBoxes(this.selectionCheckboxes_);
1985 getMergeColumns_: function() {
1986 return getKeysForCheckedBoxes(this.mergeCheckboxes_);
1989 shouldMergeSimilarThreads_: function() {
1990 return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
1993 fillMergeCheckboxes_: function(parent) {
1994 this.mergeCheckboxes_ = {};
1996 var onChangeFunc = this.onMergeCheckboxChanged_.bind(this);
1998 for (var i = 0; i < MERGEABLE_KEYS.length; ++i) {
1999 var key = MERGEABLE_KEYS[i];
2000 var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
2001 checkbox.onchange = onChangeFunc;
2002 addText(parent, ' ');
2003 this.mergeCheckboxes_[key] = checkbox;
2006 for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
2007 this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
2011 fillGroupingDropdowns_: function() {
2012 var parent = $(GROUP_BY_CONTAINER_ID);
2013 parent.innerHTML = '';
2015 for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) {
2017 var select = addNode(parent, 'select');
2018 select.onchange = this.onChangedGrouping_.bind(this, select, i);
2020 addOptionsForGroupingSelect(select);
2022 if (i < this.currentGroupingKeys_.length) {
2023 var key = this.currentGroupingKeys_[i];
2024 setSelectedOptionByValue(select, key);
2029 fillSortingDropdowns_: function() {
2030 var parent = $(SORT_BY_CONTAINER_ID);
2031 parent.innerHTML = '';
2033 for (var i = 0; i <= this.currentSortKeys_.length; ++i) {
2035 var select = addNode(parent, 'select');
2036 select.onchange = this.onChangedSorting_.bind(this, select, i);
2038 addOptionsForSortingSelect(select);
2040 if (i < this.currentSortKeys_.length) {
2041 var key = this.currentSortKeys_[i];
2042 setSelectedOptionByValue(select, key);
2047 onChangedGrouping_: function(select, i) {
2048 updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
2049 this.fillGroupingDropdowns_();
2050 this.updateGroupedData_();
2053 onChangedSorting_: function(select, i) {
2054 updateKeyListFromDropdown(this.currentSortKeys_, i, select);
2055 this.fillSortingDropdowns_();
2056 this.sortGroupedData_();
2059 onSelectCheckboxChanged_: function() {
2063 onMergeCheckboxChanged_: function() {
2064 this.updateMergedData_();
2067 onMergeSimilarThreadsCheckboxChanged_: function() {
2068 this.updateMergedData_();
2071 onChangedFilter_: function() {
2072 this.updateFilteredData_();
2076 * When left-clicking a column, change the primary sort order to that
2077 * column. If we were already sorted on that column then reverse the order.
2079 * When alt-clicking, add a secondary sort column. Similarly, if
2080 * alt-clicking a column which was already being sorted on, reverse its
2083 onClickColumn_: function(key, event) {
2084 // If this property wants to start off in descending order rather then
2085 // ascending, flip it.
2086 if (KEY_PROPERTIES[key].sortDescending)
2087 key = reverseSortKey(key);
2089 // Scan through our sort order and see if we are already sorted on this
2090 // key. If so, reverse that sort ordering.
2091 var foundIndex = -1;
2092 for (var i = 0; i < this.currentSortKeys_.length; ++i) {
2093 var curKey = this.currentSortKeys_[i];
2094 if (sortKeysMatch(curKey, key)) {
2095 this.currentSortKeys_[i] = reverseSortKey(curKey);
2102 if (foundIndex == -1) {
2103 // If we weren't already sorted on the column that was alt-clicked,
2104 // then add it to our sort.
2105 this.currentSortKeys_.push(key);
2108 if (foundIndex != 0 ||
2109 !sortKeysMatch(this.currentSortKeys_[foundIndex], key)) {
2110 // If the column we left-clicked wasn't already our primary column,
2112 this.currentSortKeys_ = [key];
2114 // If the column we left-clicked was already our primary column (and
2115 // we just reversed it), remove any secondary sorts.
2116 this.currentSortKeys_.length = 1;
2120 this.fillSortingDropdowns_();
2121 this.sortGroupedData_();
2124 getSortingFunction_: function() {
2125 var sortKeys = this.currentSortKeys_.slice(0);
2127 // Eliminate the empty string keys (which means they were unspecified).
2128 deleteValuesFromArray(sortKeys, ['']);
2130 // If no sort is specified, use our default sort.
2131 if (sortKeys.length == 0)
2132 sortKeys = [DEFAULT_SORT_KEYS];
2134 return function(a, b) {
2135 for (var i = 0; i < sortKeys.length; ++i) {
2136 var key = Math.abs(sortKeys[i]);
2137 var factor = sortKeys[i] < 0 ? -1 : 1;
2142 var comparison = compareValuesForKey(key, propA, propB);
2143 comparison *= factor; // Possibly reverse the ordering.
2145 if (comparison != 0)
2150 return simpleCompare(JSON.stringify(a), JSON.stringify(b));
2154 getGroupSortingFunction_: function() {
2155 return function(a, b) {
2156 var groupKey1 = JSON.parse(a);
2157 var groupKey2 = JSON.parse(b);
2159 for (var i = 0; i < groupKey1.length; ++i) {
2160 var comparison = compareValuesForKey(
2163 groupKey2[i].value);
2165 if (comparison != 0)
2170 return simpleCompare(a, b);
2174 getFilterFunction_: function() {
2175 var searchStr = $(FILTER_SEARCH_ID).value;
2177 // Normalize the search expression.
2178 searchStr = trimWhitespace(searchStr);
2179 searchStr = searchStr.toLowerCase();
2181 return function(x) {
2182 // Match everything when there was no filter.
2183 if (searchStr == '')
2186 // Treat the search text as a LOWERCASE substring search.
2187 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
2188 var propertyText = getTextValueForProperty(k, x[k]);
2189 if (propertyText.toLowerCase().indexOf(searchStr) != -1)
2197 getGroupingFunction_: function() {
2198 var groupings = this.currentGroupingKeys_.slice(0);
2200 // Eliminate the empty string groupings (which means they were
2202 deleteValuesFromArray(groupings, ['']);
2204 // Eliminate duplicate primary/secondary group by directives, since they
2206 deleteDuplicateStringsFromArray(groupings);
2208 return function(e) {
2211 for (var i = 0; i < groupings.length; ++i) {
2212 var entry = {key: groupings[i],
2213 value: e[groupings[i]]};
2214 groupKey.push(entry);
2217 return JSON.stringify(groupKey);