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 //--------------------------------------------------------------------------
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
);
60 * This class handles the presentation of our profiler view. Used as a
63 var MainView
= (function() {
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 // --------------------------------------------------------------------------
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 // --------------------------------------------------------------------------
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
155 * This aggregator counts the number of unique values that were fed to it.
157 var UniquifyAggregator
= (function() {
158 function Aggregator(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';
174 create: function(key
) { return new Aggregator(key
); }
179 * This aggregator sums a numeric field.
181 var SumAggregator
= (function() {
182 function Aggregator(key
) {
187 Aggregator
.prototype = {
188 consume: function(e
) {
189 this.sum_
+= e
[this.key_
];
192 getValue: function() {
196 getValueAsText: function() {
197 return formatNumberAsText(this.getValue());
202 create: function(key
) { return new Aggregator(key
); }
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());
235 create: function(numeratorKey
, divisorKey
) {
237 create: function(key
) {
238 return new Aggregator(numeratorKey
, divisorKey
);
246 * This aggregator finds the maximum for a numeric field.
248 var MaxAggregator
= (function() {
249 function Aggregator(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() {
263 getValueAsText: function() {
264 return formatNumberAsText(this.getValue());
269 create: function(key
) { return new Aggregator(key
); }
273 // --------------------------------------------------------------------------
275 // --------------------------------------------------------------------------
277 // Custom comparator for thread names (sorts main thread and IO thread
278 // higher than would happen lexicographically.)
279 var threadNameComparator
=
280 createLexicographicComparatorWithExceptions([
284 'Chrome_HistoryThread',
289 function diffFuncForCount(a
, b
) {
293 function diffFuncForMax(a
, 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
] = {
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
] = {
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
] = {
370 inputJsonKey
: 'birth_location.file_name',
371 aggregator
: UniquifyAggregator
,
374 KEY_PROPERTIES
[KEY_LINE_NUMBER
] = {
376 cellAlignment
: 'right',
377 inputJsonKey
: 'birth_location.line_number',
378 aggregator
: UniquifyAggregator
,
381 KEY_PROPERTIES
[KEY_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',
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
;
464 * Ordered list of all keys. This is the order we generally want
465 * to display the properties in. Default to declaration order.
468 for (var k
= BEGIN_KEY
; k
< END_KEY
; ++k
)
471 // --------------------------------------------------------------------------
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
= [
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
= [
502 * The ordered list of sorting choices to expose in the "Sort by"
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
= [
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
= [
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
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
) {
598 for (var key
in dict
) {
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);
612 for (var end
= orig
.length
; end
> 0; ) {
613 var chunk
= Math
.min(end
, 3);
614 parts
.push(orig
.substr(end
- chunk
, chunk
));
617 return parts
.reverse().join(',');
621 * Simple comparator function which works for both strings and numbers.
623 function simpleCompare(a
, b
) {
632 * Returns a comparator function that compares values lexicographically,
633 * but special-cases the values in |orderedList| to have a higher
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.
648 return function(a
, b
) {
649 var aRank
= getCustomRank(a
);
650 var bRank
= getCustomRank(b
);
652 // Not matched by any of our exceptions.
654 return simpleCompare(a
, b
);
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
) {
668 var parts
= key
.split('.');
669 for (var i
= 0; i
< parts
.length
; ++i
) {
670 if (cur
== undefined)
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
);
691 * Adds |text| to |parent|.
693 function addText(parent
, text
) {
694 var textNode
= parent
.ownerDocument
.createTextNode(text
);
695 parent
.appendChild(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
]]) {
714 * Deletes all the repeated ocurrences of strings in |array|.
716 function deleteDuplicateStringsFromArray(array
) {
717 // Build up set of each entry in array.
720 for (var i
= 0; i
< array
.length
; ) {
721 var value
= array
[i
];
722 if (seenSoFar
[value
]) {
725 seenSoFar
[value
] = true;
732 * Builds a map out of the array |list|.
734 function arrayToSet(list
) {
736 for (var i
= 0; i
< list
.length
; ++i
)
741 function trimWhitespace(text
) {
742 var m
= /^\s*(.*)\s*$/.exec(text
);
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;
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
);
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('\\'));
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';
798 n
.style
.display
= '';
803 * Set the visibility state of a node.
805 function setNodeDisplay(n
, visible
) {
807 n
.style
.display
= '';
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
) {
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
);
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
) {
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
];
865 identicalRows
[rowIdentity
] = l
;
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
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
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
);
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|.
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
];
931 if (outputAsDictionary
) {
932 mergedRows
[k
] = newRow
;
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();
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
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
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
);
986 for (var rowId
in data2
) {
987 var row1
= data1
[rowId
];
988 var row2
= data2
[rowId
];
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.
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
);
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.
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
);
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.
1042 var textPrinter
= KEY_PROPERTIES
[key
].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
;
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
1066 var m
= /^(.*) \[(\d+)\]$/.exec(text
);
1068 var filepath
= m
[1];
1069 var filename
= getFilenameFromPath(filepath
);
1070 var linenumber
= m
[2];
1072 var link
= addNode(td
, 'a', filename
+ ' [' + linenumber
+ ']');
1074 link
.href
= 'https://code.google.com/p/chromium/codesearch#search/&q=' +
1075 encodeURIComponent(filename
) + ':' + linenumber
+
1076 '&sq=package:chromium&type=cs';
1077 link
.target
= '_blank';
1082 // String values can get pretty long. If the string contains no spaces, then
1083 // CSS fails to wrap it, and it overflows the cell causing the table to get
1084 // really big. We solve this using a hack: insert a <wbr> element after
1085 // every single character. This will allow the rendering engine to wrap the
1086 // value, and hence avoid it overflowing!
1087 var kMinLengthBeforeWrap
= 20;
1089 addText(td
, text
.substr(0, kMinLengthBeforeWrap
));
1090 for (var i
= kMinLengthBeforeWrap
; i
< text
.length
; ++i
) {
1092 addText(td
, text
.substr(i
, 1));
1096 // --------------------------------------------------------------------------
1097 // Helper code for handling the sort and grouping dropdowns.
1098 // --------------------------------------------------------------------------
1100 function addOptionsForGroupingSelect(select
) {
1101 // Add "no group" choice.
1102 addNode(select
, 'option', '---').value
= '';
1104 for (var i
= 0; i
< GROUPING_DROPDOWN_CHOICES
.length
; ++i
) {
1105 var key
= GROUPING_DROPDOWN_CHOICES
[i
];
1106 var option
= addNode(select
, 'option', getNameForKey(key
));
1111 function addOptionsForSortingSelect(select
) {
1112 // Add "no sort" choice.
1113 addNode(select
, 'option', '---').value
= '';
1116 addNode(select
, 'optgroup').label
= '';
1118 for (var i
= 0; i
< SORT_DROPDOWN_CHOICES
.length
; ++i
) {
1119 var key
= SORT_DROPDOWN_CHOICES
[i
];
1120 addNode(select
, 'option', getNameForKey(key
)).value
= key
;
1124 addNode(select
, 'optgroup').label
= '';
1126 // Add the same options, but for descending.
1127 for (var i
= 0; i
< SORT_DROPDOWN_CHOICES
.length
; ++i
) {
1128 var key
= SORT_DROPDOWN_CHOICES
[i
];
1129 var n
= addNode(select
, 'option', getNameForKey(key
) + ' (DESC)');
1130 n
.value
= reverseSortKey(key
);
1135 * Helper function used to update the sorting and grouping lists after a
1138 function updateKeyListFromDropdown(list
, i
, select
) {
1140 if (i
< list
.length
) {
1141 list
[i
] = select
.value
;
1143 list
.push(select
.value
);
1146 // Normalize the list, so setting 'none' as primary zeros out everything
1148 for (var i
= 0; i
< list
.length
; ++i
) {
1149 if (list
[i
] == '') {
1150 list
.splice(i
, list
.length
- i
);
1157 * Comparator for property |key|, having values |value1| and |value2|.
1158 * If the key has defined a custom comparator use it. Otherwise use a
1159 * default "less than" comparison.
1161 function compareValuesForKey(key
, value1
, value2
) {
1162 var comparator
= KEY_PROPERTIES
[key
].comparator
;
1164 return comparator(value1
, value2
);
1165 return simpleCompare(value1
, value2
);
1168 function reverseSortKey(key
) {
1172 function sortKeyIsReversed(key
) {
1176 function sortKeysMatch(key1
, key2
) {
1177 return Math
.abs(key1
) == Math
.abs(key2
);
1180 function getKeysForCheckedBoxes(checkboxes
) {
1182 for (var k
in checkboxes
) {
1183 if (checkboxes
[k
].checked
)
1189 // --------------------------------------------------------------------------
1194 function MainView() {
1195 // Make sure we have a definition for each key.
1196 for (var k
= BEGIN_KEY
; k
< END_KEY
; ++k
) {
1197 if (!KEY_PROPERTIES
[k
])
1198 throw 'KEY_PROPERTIES[] not defined for key: ' + k
;
1204 MainView
.prototype = {
1205 addDataToSnapshot: function(data
) {
1206 // TODO(eroman): We need to know which snapshot this data belongs to!
1207 // For now we assume it is the most recent snapshot.
1208 var snapshotIndex
= this.snapshots_
.length
- 1;
1210 var snapshot
= this.snapshots_
[snapshotIndex
];
1212 var pid
= data
.process_id
;
1213 var ptype
= data
.process_type
;
1215 // Save the browser's representation of the data
1216 snapshot
.origData
.push(data
);
1218 // Augment each data row with the process information.
1219 var rows
= data
.list
;
1220 for (var i
= 0; i
< rows
.length
; ++i
) {
1221 // Transform the data from a dictionary to an array. This internal
1222 // representation is more compact and faster to access.
1223 var origRow
= rows
[i
];
1226 newRow
[KEY_PROCESS_ID
] = pid
;
1227 newRow
[KEY_PROCESS_TYPE
] = ptype
;
1229 // Copy over the known properties which have a 1:1 mapping with JSON.
1230 for (var k
= BEGIN_KEY
; k
< END_KEY
; ++k
) {
1231 var inputJsonKey
= KEY_PROPERTIES
[k
].inputJsonKey
;
1232 if (inputJsonKey
!= undefined) {
1233 newRow
[k
] = getPropertyByPath(origRow
, inputJsonKey
);
1237 if (newRow
[KEY_COUNT
] == 0) {
1238 // When resetting the data, it is possible for the backend to give us
1239 // counts of "0". There is no point adding these rows (in fact they
1240 // will cause us to do divide by zeros when calculating averages and
1241 // stuff), so we skip past them.
1245 // Add our computed properties.
1246 augmentDataRow(newRow
);
1248 snapshot
.flatData
.push(newRow
);
1251 if (!arrayToSet(this.getSelectedSnapshotIndexes_())[snapshotIndex
]) {
1252 // Optimization: If this snapshot is not a data dependency for the
1253 // current display, then don't bother updating anything.
1257 // We may end up calling addDataToSnapshot_() repeatedly (once for each
1258 // process). To avoid this from slowing us down we do bulk updates on a
1260 this.updateMergedDataSoon_();
1263 updateMergedDataSoon_: function() {
1264 if (this.updateMergedDataPending_
) {
1265 // If a delayed task has already been posted to re-merge the data,
1266 // then we don't need to do anything extra.
1270 // Otherwise schedule updateMergedData_() to be called later. We want it
1271 // to be called no more than once every PROCESS_DATA_DELAY_MS
1274 if (this.lastUpdateMergedDataTime_
== undefined)
1275 this.lastUpdateMergedDataTime_
= 0;
1277 var timeSinceLastMerge
= getTimeMillis() - this.lastUpdateMergedDataTime_
;
1278 var timeToWait
= Math
.max(0, PROCESS_DATA_DELAY_MS
- timeSinceLastMerge
);
1280 var functionToRun = function() {
1281 // Do the actual update.
1282 this.updateMergedData_();
1283 // Keep track of when we last ran.
1284 this.lastUpdateMergedDataTime_
= getTimeMillis();
1285 this.updateMergedDataPending_
= false;
1288 this.updateMergedDataPending_
= true;
1289 window
.setTimeout(functionToRun
, timeToWait
);
1293 * Returns a list of the currently selected snapshots. This list is
1294 * guaranteed to be of length 1 or 2.
1296 getSelectedSnapshotIndexes_: function() {
1297 var indexes
= this.getSelectedSnapshotBoxes_();
1298 for (var i
= 0; i
< indexes
.length
; ++i
)
1299 indexes
[i
] = indexes
[i
].__index
;
1304 * Same as getSelectedSnapshotIndexes_(), only it returns the actual
1305 * checkbox input DOM nodes rather than the snapshot ID.
1307 getSelectedSnapshotBoxes_: function() {
1308 // Figure out which snaphots to use for our data.
1310 for (var i
= 0; i
< this.snapshots_
.length
; ++i
) {
1311 var box
= this.getSnapshotCheckbox_(i
);
1319 * Re-draw the description that explains which snapshots are currently
1320 * selected (if two snapshots were selected we explain that the *difference*
1321 * between them is being displayed).
1323 updateSnapshotSelectionSummaryDiv_: function() {
1324 var summaryDiv
= $(SNAPSHOT_SELECTION_SUMMARY_ID
);
1326 var selectedSnapshots
= this.getSelectedSnapshotIndexes_();
1327 if (selectedSnapshots
.length
== 0) {
1328 // This can occur during an attempt to load a file or following file
1329 // load failure. We just ignore it and move on.
1330 } else if (selectedSnapshots
.length
== 1) {
1331 // If only one snapshot is chosen then we will display that snapshot's
1332 // data in its entirety.
1333 this.flatData_
= this.snapshots_
[selectedSnapshots
[0]].flatData
;
1335 // Don't bother displaying any text when just 1 snapshot is selected,
1336 // since it is obvious what this should do.
1337 summaryDiv
.innerText
= '';
1338 } else if (selectedSnapshots
.length
== 2) {
1339 // Otherwise if two snapshots were chosen, show the difference between
1341 var snapshot1
= this.snapshots_
[selectedSnapshots
[0]];
1342 var snapshot2
= this.snapshots_
[selectedSnapshots
[1]];
1344 var timeDeltaInSeconds
=
1345 ((snapshot2
.time
- snapshot1
.time
) / 1000).toFixed(0);
1347 // Explain that what is being shown is the difference between two
1349 summaryDiv
.innerText
=
1350 'Showing the difference between snapshots #' +
1351 selectedSnapshots
[0] + ' and #' +
1352 selectedSnapshots
[1] + ' (' + timeDeltaInSeconds
+
1353 ' seconds worth of data)';
1355 // This shouldn't be possible...
1356 throw 'Unexpected number of selected snapshots';
1360 updateMergedData_: function() {
1361 // Retrieve the merge options.
1362 var mergeColumns
= this.getMergeColumns_();
1363 var shouldMergeSimilarThreads
= this.shouldMergeSimilarThreads_();
1365 var selectedSnapshots
= this.getSelectedSnapshotIndexes_();
1367 // We do merges a bit differently depending if we are displaying the diffs
1368 // between two snapshots, or just displaying a single snapshot.
1369 if (selectedSnapshots
.length
== 1) {
1370 var snapshot
= this.snapshots_
[selectedSnapshots
[0]];
1371 this.mergedData_
= mergeRows(snapshot
.flatData
,
1373 shouldMergeSimilarThreads
,
1376 } else if (selectedSnapshots
.length
== 2) {
1377 var snapshot1
= this.snapshots_
[selectedSnapshots
[0]];
1378 var snapshot2
= this.snapshots_
[selectedSnapshots
[1]];
1380 // Merge the data for snapshot1.
1381 var mergedRows1
= mergeRows(snapshot1
.flatData
,
1383 shouldMergeSimilarThreads
,
1386 // Merge the data for snapshot2.
1387 var mergedRows2
= mergeRows(snapshot2
.flatData
,
1389 shouldMergeSimilarThreads
,
1392 // Do a diff between the two snapshots.
1393 this.mergedData_
= subtractSnapshots(mergedRows1
,
1397 throw 'Unexpected number of selected snapshots';
1400 // Recompute filteredData_ (since it is derived from mergedData_)
1401 this.updateFilteredData_();
1404 updateFilteredData_: function() {
1405 // Recompute filteredData_.
1406 this.filteredData_
= [];
1407 var filterFunc
= this.getFilterFunction_();
1408 for (var i
= 0; i
< this.mergedData_
.length
; ++i
) {
1409 var r
= this.mergedData_
[i
];
1410 if (!filterFunc(r
)) {
1411 // Not matched by our filter, discard.
1414 this.filteredData_
.push(r
);
1417 // Recompute groupedData_ (since it is derived from filteredData_)
1418 this.updateGroupedData_();
1421 updateGroupedData_: function() {
1422 // Recompute groupedData_.
1423 var groupKeyToData
= {};
1424 var entryToGroupKeyFunc
= this.getGroupingFunction_();
1425 for (var i
= 0; i
< this.filteredData_
.length
; ++i
) {
1426 var r
= this.filteredData_
[i
];
1428 var groupKey
= entryToGroupKeyFunc(r
);
1430 var groupData
= groupKeyToData
[groupKey
];
1433 key
: JSON
.parse(groupKey
),
1434 aggregates
: initializeAggregates(ALL_KEYS
),
1437 groupKeyToData
[groupKey
] = groupData
;
1440 // Add the row to our list.
1441 groupData
.rows
.push(r
);
1443 // Update aggregates for each column.
1444 consumeAggregates(groupData
.aggregates
, r
);
1446 this.groupedData_
= groupKeyToData
;
1448 // Figure out a display order for the groups themselves.
1449 this.sortedGroupKeys_
= getDictionaryKeys(groupKeyToData
);
1450 this.sortedGroupKeys_
.sort(this.getGroupSortingFunction_());
1452 // Sort the group data.
1453 this.sortGroupedData_();
1456 sortGroupedData_: function() {
1457 var sortingFunc
= this.getSortingFunction_();
1458 for (var k
in this.groupedData_
)
1459 this.groupedData_
[k
].rows
.sort(sortingFunc
);
1461 // Every cached data dependency is now up to date, all that is left is
1462 // to actually draw the result.
1466 getVisibleColumnKeys_: function() {
1467 // Figure out what columns to include, based on the selected checkboxes.
1468 var columns
= this.getSelectionColumns_();
1469 columns
= columns
.slice(0);
1471 // Eliminate columns which we are merging on.
1472 deleteValuesFromArray(columns
, this.getMergeColumns_());
1474 // Eliminate columns which we are grouped on.
1475 if (this.sortedGroupKeys_
.length
> 0) {
1476 // The grouping will be the the same for each so just pick the first.
1477 var randomGroupKey
= this.groupedData_
[this.sortedGroupKeys_
[0]].key
;
1479 // The grouped properties are going to be the same for each row in our,
1480 // table, so avoid drawing them in our table!
1481 var keysToExclude
= [];
1483 for (var i
= 0; i
< randomGroupKey
.length
; ++i
)
1484 keysToExclude
.push(randomGroupKey
[i
].key
);
1485 deleteValuesFromArray(columns
, keysToExclude
);
1488 // If we are currently showing a "diff", hide the max columns, since we
1489 // are not populating it correctly. See the TODO at the top of this file.
1490 if (this.getSelectedSnapshotIndexes_().length
> 1)
1491 deleteValuesFromArray(columns
, [KEY_MAX_RUN_TIME
, KEY_MAX_QUEUE_TIME
]);
1496 redrawData_: function() {
1497 // Clear the results div, sine we may be overwriting older data.
1498 var parent
= $(RESULTS_DIV_ID
);
1499 parent
.innerHTML
= '';
1501 var columns
= this.getVisibleColumnKeys_();
1504 for (var i
= 0; i
< this.sortedGroupKeys_
.length
; ++i
) {
1505 var k
= this.sortedGroupKeys_
[i
];
1506 this.drawGroup_(parent
, k
, columns
);
1511 * Renders the information for a particular group.
1513 drawGroup_: function(parent
, groupKey
, columns
) {
1514 var groupData
= this.groupedData_
[groupKey
];
1516 var div
= addNode(parent
, 'div');
1517 div
.className
= 'group-container';
1519 this.drawGroupTitle_(div
, groupData
.key
);
1521 var table
= addNode(div
, 'table');
1523 this.drawDataTable_(table
, groupData
, columns
, groupKey
);
1527 * Draws a title into |parent| that describes |groupKey|.
1529 drawGroupTitle_: function(parent
, groupKey
) {
1530 if (groupKey
.length
== 0) {
1531 // Empty group key means there was no grouping.
1535 var parent
= addNode(parent
, 'div');
1536 parent
.className
= 'group-title-container';
1538 // Each component of the group key represents the "key=value" constraint
1539 // for this group. Show these as an AND separated list.
1540 for (var i
= 0; i
< groupKey
.length
; ++i
) {
1542 addNode(parent
, 'i', ' and ');
1543 var e
= groupKey
[i
];
1544 addNode(parent
, 'b', getNameForKey(e
.key
) + ' = ');
1545 addNode(parent
, 'span', e
.value
);
1550 * Renders a table which summarizes all |column| fields for |data|.
1552 drawDataTable_: function(table
, data
, columns
, groupKey
) {
1553 table
.className
= 'results-table';
1554 var thead
= addNode(table
, 'thead');
1555 var tbody
= addNode(table
, 'tbody');
1557 var displaySettings
= this.getGroupDisplaySettings_(groupKey
);
1558 var limit
= displaySettings
.limit
;
1560 this.drawAggregateRow_(thead
, data
.aggregates
, columns
);
1561 this.drawTableHeader_(thead
, columns
);
1562 this.drawTableBody_(tbody
, data
.rows
, columns
, limit
);
1563 this.drawTruncationRow_(tbody
, data
.rows
.length
, limit
, columns
.length
,
1567 drawTableHeader_: function(thead
, columns
) {
1568 var tr
= addNode(thead
, 'tr');
1569 for (var i
= 0; i
< columns
.length
; ++i
) {
1570 var key
= columns
[i
];
1571 var th
= addNode(tr
, 'th', getNameForKey(key
));
1572 th
.onclick
= this.onClickColumn_
.bind(this, key
);
1574 // Draw an indicator if we are currently sorted on this column.
1575 // TODO(eroman): Should use an icon instead of asterisk!
1576 for (var j
= 0; j
< this.currentSortKeys_
.length
; ++j
) {
1577 if (sortKeysMatch(this.currentSortKeys_
[j
], key
)) {
1578 var sortIndicator
= addNode(th
, 'span', '*');
1579 sortIndicator
.style
.color
= 'red';
1580 if (sortKeyIsReversed(this.currentSortKeys_
[j
])) {
1581 // Use double-asterisk for descending columns.
1582 addText(sortIndicator
, '*');
1590 drawTableBody_: function(tbody
, rows
, columns
, limit
) {
1591 for (var i
= 0; i
< rows
.length
&& i
< limit
; ++i
) {
1594 var tr
= addNode(tbody
, 'tr');
1596 for (var c
= 0; c
< columns
.length
; ++c
) {
1597 var key
= columns
[c
];
1600 var td
= addNode(tr
, 'td');
1601 drawValueToCell(td
, key
, value
);
1607 * Renders a row that describes all the aggregate values for |columns|.
1609 drawAggregateRow_: function(tbody
, aggregates
, columns
) {
1610 var tr
= addNode(tbody
, 'tr');
1611 tr
.className
= 'aggregator-row';
1613 for (var i
= 0; i
< columns
.length
; ++i
) {
1614 var key
= columns
[i
];
1615 var td
= addNode(tr
, 'td');
1617 // Most of our outputs are numeric, so we want to align them to the
1618 // right. However for the unique counts we will center.
1619 if (KEY_PROPERTIES
[key
].aggregator
== UniquifyAggregator
) {
1620 td
.align
= 'center';
1625 var aggregator
= aggregates
[key
];
1627 td
.innerText
= aggregator
.getValueAsText();
1632 * Renders a row which describes how many rows the table has, how many are
1633 * currently hidden, and a set of buttons to show more.
1635 drawTruncationRow_: function(tbody
, numRows
, limit
, numColumns
, groupKey
) {
1636 var numHiddenRows
= Math
.max(numRows
- limit
, 0);
1637 var numVisibleRows
= numRows
- numHiddenRows
;
1639 var tr
= addNode(tbody
, 'tr');
1640 tr
.className
= 'truncation-row';
1641 var td
= addNode(tr
, 'td');
1642 td
.colSpan
= numColumns
;
1644 addText(td
, numRows
+ ' rows');
1645 if (numHiddenRows
> 0) {
1646 var s
= addNode(td
, 'span', ' (' + numHiddenRows
+ ' hidden) ');
1647 s
.style
.color
= 'red';
1650 if (numVisibleRows
> LIMIT_INCREMENT
) {
1651 addNode(td
, 'button', 'Show less').onclick
=
1652 this.changeGroupDisplayLimit_
.bind(
1653 this, groupKey
, -LIMIT_INCREMENT
);
1655 if (numVisibleRows
> 0) {
1656 addNode(td
, 'button', 'Show none').onclick
=
1657 this.changeGroupDisplayLimit_
.bind(this, groupKey
, -Infinity
);
1660 if (numHiddenRows
> 0) {
1661 addNode(td
, 'button', 'Show more').onclick
=
1662 this.changeGroupDisplayLimit_
.bind(this, groupKey
, LIMIT_INCREMENT
);
1663 addNode(td
, 'button', 'Show all').onclick
=
1664 this.changeGroupDisplayLimit_
.bind(this, groupKey
, Infinity
);
1669 * Adjusts the row limit for group |groupKey| by |delta|.
1671 changeGroupDisplayLimit_: function(groupKey
, delta
) {
1672 // Get the current settings for this group.
1673 var settings
= this.getGroupDisplaySettings_(groupKey
, true);
1675 // Compute the adjusted limit.
1676 var newLimit
= settings
.limit
;
1677 var totalNumRows
= this.groupedData_
[groupKey
].rows
.length
;
1678 newLimit
= Math
.min(totalNumRows
, newLimit
);
1680 newLimit
= Math
.max(0, newLimit
);
1682 // Update the settings with the new limit.
1683 settings
.limit
= newLimit
;
1685 // TODO(eroman): It isn't necessary to redraw *all* the data. Really we
1686 // just need to insert the missing rows (everything else stays the same)!
1691 * Returns the rendering settings for group |groupKey|. This includes things
1692 * like how many rows to display in the table.
1694 getGroupDisplaySettings_: function(groupKey
, opt_create
) {
1695 var settings
= this.groupDisplaySettings_
[groupKey
];
1697 // If we don't have any settings for this group yet, create some
1699 if (groupKey
== '[]') {
1700 // (groupKey of '[]' is what we use for ungrouped data).
1701 settings
= {limit
: INITIAL_UNGROUPED_ROW_LIMIT
};
1703 settings
= {limit
: INITIAL_GROUP_ROW_LIMIT
};
1706 this.groupDisplaySettings_
[groupKey
] = settings
;
1712 this.snapshots_
= [];
1714 // Start fetching the data from the browser; this will be our snapshot #0.
1715 this.takeSnapshot_();
1717 // Data goes through the following pipeline:
1718 // (1) Raw data received from browser, and transformed into our own
1719 // internal row format (where properties are indexed by KEY_*
1721 // (2) We "augment" each row by adding some extra computed columns
1723 // (3) The rows are merged using current merge settings.
1724 // (4) The rows that don't match current search expression are
1726 // (5) The rows are organized into "groups" based on current settings,
1727 // and aggregate values are computed for each resulting group.
1728 // (6) The rows within each group are sorted using current settings.
1729 // (7) The grouped rows are drawn to the screen.
1730 this.mergedData_
= [];
1731 this.filteredData_
= [];
1732 this.groupedData_
= {};
1733 this.sortedGroupKeys_
= [];
1735 this.groupDisplaySettings_
= {};
1737 this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID
));
1738 this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID
));
1740 $(FILTER_SEARCH_ID
).onsearch
= this.onChangedFilter_
.bind(this);
1742 this.currentSortKeys_
= INITIAL_SORT_KEYS
.slice(0);
1743 this.currentGroupingKeys_
= INITIAL_GROUP_KEYS
.slice(0);
1745 this.fillGroupingDropdowns_();
1746 this.fillSortingDropdowns_();
1748 $(EDIT_COLUMNS_LINK_ID
).onclick
=
1749 toggleNodeDisplay
.bind(null, $(EDIT_COLUMNS_ROW
));
1751 $(TOGGLE_SNAPSHOTS_LINK_ID
).onclick
=
1752 toggleNodeDisplay
.bind(null, $(SNAPSHOTS_ROW
));
1754 $(MERGE_SIMILAR_THREADS_CHECKBOX_ID
).onchange
=
1755 this.onMergeSimilarThreadsCheckboxChanged_
.bind(this);
1757 $(TAKE_SNAPSHOT_BUTTON_ID
).onclick
= this.takeSnapshot_
.bind(this);
1759 $(SAVE_SNAPSHOTS_BUTTON_ID
).onclick
= this.saveSnapshots_
.bind(this);
1760 $(SNAPSHOT_FILE_LOADER_ID
).onchange
= this.loadFileChanged_
.bind(this);
1763 takeSnapshot_: function() {
1764 // Start a new empty snapshot. Make note of the current time, so we know
1765 // when the snaphot was taken.
1766 this.snapshots_
.push({flatData
: [], origData
: [], time
: getTimeMillis()});
1768 // Update the UI to reflect the new snapshot.
1769 this.addSnapshotToList_(this.snapshots_
.length
- 1);
1771 // Ask the browser for the profiling data. We will receive the data
1772 // later through a callback to addDataToSnapshot_().
1773 g_browserBridge
.sendGetData();
1776 saveSnapshots_: function() {
1778 for (var i
= 0; i
< this.snapshots_
.length
; ++i
) {
1779 snapshots
.push({ data
: this.snapshots_
[i
].origData
,
1780 timestamp
: Math
.floor(
1781 this.snapshots_
[i
].time
/ 1000) });
1785 'userAgent': navigator
.userAgent
,
1787 'snapshots': snapshots
1790 var dumpText
= JSON
.stringify(dump
, null, ' ');
1791 var textBlob
= new Blob([dumpText
],
1792 { type
: 'octet/stream', endings
: 'native' });
1793 var blobUrl
= window
.URL
.createObjectURL(textBlob
);
1794 $(DOWNLOAD_ANCHOR_ID
).href
= blobUrl
;
1795 $(DOWNLOAD_ANCHOR_ID
).click();
1798 loadFileChanged_: function() {
1799 this.loadSnapshots_($(SNAPSHOT_FILE_LOADER_ID
).files
[0]);
1802 loadSnapshots_: function(file
) {
1804 var fileReader
= new FileReader();
1806 fileReader
.onload
= this.onLoadSnapshotsFile_
.bind(this, file
);
1807 fileReader
.onerror
= this.onLoadSnapshotsFileError_
.bind(this, file
);
1809 fileReader
.readAsText(file
);
1813 onLoadSnapshotsFile_: function(file
, event
) {
1816 parsed
= JSON
.parse(event
.target
.result
);
1818 if (parsed
.version
!= 1) {
1819 throw new Error('Unrecognized version: ' + parsed
.version
);
1822 if (parsed
.snapshots
.length
< 1) {
1823 throw new Error('File contains no data');
1826 this.displayLoadedFile_(file
, parsed
);
1827 this.hideFileLoadError_();
1829 this.displayFileLoadError_('File load failure: ' + error
.message
);
1833 clearExistingSnapshots_: function() {
1834 var tbody
= $('snapshots-tbody');
1835 this.snapshots_
= [];
1836 tbody
.innerHTML
= '';
1837 this.updateMergedDataSoon_();
1840 displayLoadedFile_: function(file
, content
) {
1841 this.clearExistingSnapshots_();
1842 $(TAKE_SNAPSHOT_BUTTON_ID
).disabled
= true;
1843 $(SAVE_SNAPSHOTS_BUTTON_ID
).disabled
= true;
1845 if (content
.snapshots
.length
> 1) {
1846 setNodeDisplay($(SNAPSHOTS_ROW
), true);
1849 for (var i
= 0; i
< content
.snapshots
.length
; ++i
) {
1850 var snapshot
= content
.snapshots
[i
];
1851 this.snapshots_
.push({flatData
: [], origData
: [],
1852 time
: snapshot
.timestamp
* 1000});
1853 this.addSnapshotToList_(this.snapshots_
.length
- 1);
1854 var snapshotData
= snapshot
.data
;
1855 for (var j
= 0; j
< snapshotData
.length
; ++j
) {
1856 this.addDataToSnapshot(snapshotData
[j
]);
1862 onLoadSnapshotsFileError_: function(file
, filedata
) {
1863 this.displayFileLoadError_('Error loading ' + file
.name
);
1866 displayFileLoadError_: function(message
) {
1867 $(LOAD_ERROR_ID
).textContent
= message
;
1868 $(LOAD_ERROR_ID
).hidden
= false;
1871 hideFileLoadError_: function() {
1872 $(LOAD_ERROR_ID
).textContent
= '';
1873 $(LOAD_ERROR_ID
).hidden
= true;
1876 getSnapshotCheckbox_: function(i
) {
1877 return $(this.getSnapshotCheckboxId_(i
));
1880 getSnapshotCheckboxId_: function(i
) {
1881 return 'snapshotCheckbox-' + i
;
1884 addSnapshotToList_: function(i
) {
1885 var tbody
= $('snapshots-tbody');
1887 var tr
= addNode(tbody
, 'tr');
1889 var id
= this.getSnapshotCheckboxId_(i
);
1891 var checkboxCell
= addNode(tr
, 'td');
1892 var checkbox
= addNode(checkboxCell
, 'input');
1893 checkbox
.type
= 'checkbox';
1895 checkbox
.__index
= i
;
1896 checkbox
.onclick
= this.onSnapshotCheckboxChanged_
.bind(this);
1898 addNode(tr
, 'td', '#' + i
);
1900 var labelCell
= addNode(tr
, 'td');
1901 var l
= addNode(labelCell
, 'label');
1903 var dateString
= new Date(this.snapshots_
[i
].time
).toLocaleString();
1904 addText(l
, dateString
);
1907 // If we are on snapshot 0, make it the default.
1909 checkbox
.checked
= true;
1910 checkbox
.__time
= getTimeMillis();
1911 this.updateSnapshotCheckboxStyling_();
1915 updateSnapshotCheckboxStyling_: function() {
1916 for (var i
= 0; i
< this.snapshots_
.length
; ++i
) {
1917 var checkbox
= this.getSnapshotCheckbox_(i
);
1918 checkbox
.parentNode
.parentNode
.className
=
1919 checkbox
.checked
? 'selected_snapshot' : '';
1923 onSnapshotCheckboxChanged_: function(event
) {
1924 // Keep track of when we clicked this box (for when we need to uncheck
1926 event
.target
.__time
= getTimeMillis();
1928 // Find all the checked boxes. Either 1 or 2 can be checked. If a third
1929 // was just checked, then uncheck one of the earlier ones so we only have
1931 var checked
= this.getSelectedSnapshotBoxes_();
1932 checked
.sort(function(a
, b
) { return b
.__time
- a
.__time
; });
1933 if (checked
.length
> 2) {
1934 for (var i
= 2; i
< checked
.length
; ++i
)
1935 checked
[i
].checked
= false;
1939 // We should always have at least 1 selection. Prevent the user from
1940 // unselecting the final box.
1941 if (checked
.length
== 0)
1942 event
.target
.checked
= true;
1944 this.updateSnapshotCheckboxStyling_();
1945 this.updateSnapshotSelectionSummaryDiv_();
1947 // Recompute mergedData_ (since it is derived from selected snapshots).
1948 this.updateMergedData_();
1951 fillSelectionCheckboxes_: function(parent
) {
1952 this.selectionCheckboxes_
= {};
1954 var onChangeFunc
= this.onSelectCheckboxChanged_
.bind(this);
1956 for (var i
= 0; i
< ALL_TABLE_COLUMNS
.length
; ++i
) {
1957 var key
= ALL_TABLE_COLUMNS
[i
];
1958 var checkbox
= addLabeledCheckbox(parent
, getNameForKey(key
));
1959 checkbox
.checked
= true;
1960 checkbox
.onchange
= onChangeFunc
;
1961 addText(parent
, ' ');
1962 this.selectionCheckboxes_
[key
] = checkbox
;
1965 for (var i
= 0; i
< INITIALLY_HIDDEN_KEYS
.length
; ++i
) {
1966 this.selectionCheckboxes_
[INITIALLY_HIDDEN_KEYS
[i
]].checked
= false;
1970 getSelectionColumns_: function() {
1971 return getKeysForCheckedBoxes(this.selectionCheckboxes_
);
1974 getMergeColumns_: function() {
1975 return getKeysForCheckedBoxes(this.mergeCheckboxes_
);
1978 shouldMergeSimilarThreads_: function() {
1979 return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID
).checked
;
1982 fillMergeCheckboxes_: function(parent
) {
1983 this.mergeCheckboxes_
= {};
1985 var onChangeFunc
= this.onMergeCheckboxChanged_
.bind(this);
1987 for (var i
= 0; i
< MERGEABLE_KEYS
.length
; ++i
) {
1988 var key
= MERGEABLE_KEYS
[i
];
1989 var checkbox
= addLabeledCheckbox(parent
, getNameForKey(key
));
1990 checkbox
.onchange
= onChangeFunc
;
1991 addText(parent
, ' ');
1992 this.mergeCheckboxes_
[key
] = checkbox
;
1995 for (var i
= 0; i
< INITIALLY_MERGED_KEYS
.length
; ++i
) {
1996 this.mergeCheckboxes_
[INITIALLY_MERGED_KEYS
[i
]].checked
= true;
2000 fillGroupingDropdowns_: function() {
2001 var parent
= $(GROUP_BY_CONTAINER_ID
);
2002 parent
.innerHTML
= '';
2004 for (var i
= 0; i
<= this.currentGroupingKeys_
.length
; ++i
) {
2006 var select
= addNode(parent
, 'select');
2007 select
.onchange
= this.onChangedGrouping_
.bind(this, select
, i
);
2009 addOptionsForGroupingSelect(select
);
2011 if (i
< this.currentGroupingKeys_
.length
) {
2012 var key
= this.currentGroupingKeys_
[i
];
2013 setSelectedOptionByValue(select
, key
);
2018 fillSortingDropdowns_: function() {
2019 var parent
= $(SORT_BY_CONTAINER_ID
);
2020 parent
.innerHTML
= '';
2022 for (var i
= 0; i
<= this.currentSortKeys_
.length
; ++i
) {
2024 var select
= addNode(parent
, 'select');
2025 select
.onchange
= this.onChangedSorting_
.bind(this, select
, i
);
2027 addOptionsForSortingSelect(select
);
2029 if (i
< this.currentSortKeys_
.length
) {
2030 var key
= this.currentSortKeys_
[i
];
2031 setSelectedOptionByValue(select
, key
);
2036 onChangedGrouping_: function(select
, i
) {
2037 updateKeyListFromDropdown(this.currentGroupingKeys_
, i
, select
);
2038 this.fillGroupingDropdowns_();
2039 this.updateGroupedData_();
2042 onChangedSorting_: function(select
, i
) {
2043 updateKeyListFromDropdown(this.currentSortKeys_
, i
, select
);
2044 this.fillSortingDropdowns_();
2045 this.sortGroupedData_();
2048 onSelectCheckboxChanged_: function() {
2052 onMergeCheckboxChanged_: function() {
2053 this.updateMergedData_();
2056 onMergeSimilarThreadsCheckboxChanged_: function() {
2057 this.updateMergedData_();
2060 onChangedFilter_: function() {
2061 this.updateFilteredData_();
2065 * When left-clicking a column, change the primary sort order to that
2066 * column. If we were already sorted on that column then reverse the order.
2068 * When alt-clicking, add a secondary sort column. Similarly, if
2069 * alt-clicking a column which was already being sorted on, reverse its
2072 onClickColumn_: function(key
, event
) {
2073 // If this property wants to start off in descending order rather then
2074 // ascending, flip it.
2075 if (KEY_PROPERTIES
[key
].sortDescending
)
2076 key
= reverseSortKey(key
);
2078 // Scan through our sort order and see if we are already sorted on this
2079 // key. If so, reverse that sort ordering.
2080 var foundIndex
= -1;
2081 for (var i
= 0; i
< this.currentSortKeys_
.length
; ++i
) {
2082 var curKey
= this.currentSortKeys_
[i
];
2083 if (sortKeysMatch(curKey
, key
)) {
2084 this.currentSortKeys_
[i
] = reverseSortKey(curKey
);
2091 if (foundIndex
== -1) {
2092 // If we weren't already sorted on the column that was alt-clicked,
2093 // then add it to our sort.
2094 this.currentSortKeys_
.push(key
);
2097 if (foundIndex
!= 0 ||
2098 !sortKeysMatch(this.currentSortKeys_
[foundIndex
], key
)) {
2099 // If the column we left-clicked wasn't already our primary column,
2101 this.currentSortKeys_
= [key
];
2103 // If the column we left-clicked was already our primary column (and
2104 // we just reversed it), remove any secondary sorts.
2105 this.currentSortKeys_
.length
= 1;
2109 this.fillSortingDropdowns_();
2110 this.sortGroupedData_();
2113 getSortingFunction_: function() {
2114 var sortKeys
= this.currentSortKeys_
.slice(0);
2116 // Eliminate the empty string keys (which means they were unspecified).
2117 deleteValuesFromArray(sortKeys
, ['']);
2119 // If no sort is specified, use our default sort.
2120 if (sortKeys
.length
== 0)
2121 sortKeys
= [DEFAULT_SORT_KEYS
];
2123 return function(a
, b
) {
2124 for (var i
= 0; i
< sortKeys
.length
; ++i
) {
2125 var key
= Math
.abs(sortKeys
[i
]);
2126 var factor
= sortKeys
[i
] < 0 ? -1 : 1;
2131 var comparison
= compareValuesForKey(key
, propA
, propB
);
2132 comparison
*= factor
; // Possibly reverse the ordering.
2134 if (comparison
!= 0)
2139 return simpleCompare(JSON
.stringify(a
), JSON
.stringify(b
));
2143 getGroupSortingFunction_: function() {
2144 return function(a
, b
) {
2145 var groupKey1
= JSON
.parse(a
);
2146 var groupKey2
= JSON
.parse(b
);
2148 for (var i
= 0; i
< groupKey1
.length
; ++i
) {
2149 var comparison
= compareValuesForKey(
2152 groupKey2
[i
].value
);
2154 if (comparison
!= 0)
2159 return simpleCompare(a
, b
);
2163 getFilterFunction_: function() {
2164 var searchStr
= $(FILTER_SEARCH_ID
).value
;
2166 // Normalize the search expression.
2167 searchStr
= trimWhitespace(searchStr
);
2168 searchStr
= searchStr
.toLowerCase();
2170 return function(x
) {
2171 // Match everything when there was no filter.
2172 if (searchStr
== '')
2175 // Treat the search text as a LOWERCASE substring search.
2176 for (var k
= BEGIN_KEY
; k
< END_KEY
; ++k
) {
2177 var propertyText
= getTextValueForProperty(k
, x
[k
]);
2178 if (propertyText
.toLowerCase().indexOf(searchStr
) != -1)
2186 getGroupingFunction_: function() {
2187 var groupings
= this.currentGroupingKeys_
.slice(0);
2189 // Eliminate the empty string groupings (which means they were
2191 deleteValuesFromArray(groupings
, ['']);
2193 // Eliminate duplicate primary/secondary group by directives, since they
2195 deleteDuplicateStringsFromArray(groupings
);
2197 return function(e
) {
2200 for (var i
= 0; i
< groupings
.length
; ++i
) {
2201 var entry
= {key
: groupings
[i
],
2202 value
: e
[groupings
[i
]]};
2203 groupKey
.push(entry
);
2206 return JSON
.stringify(groupKey
);