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
);