Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(582)

Side by Side Diff: chrome/browser/resources/profiler.js

Issue 8832001: Add an OWNERS file for the about:profiler javascript code. (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: '' Created 9 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chrome/browser/resources/profiler.html ('k') | chrome/browser/resources/profiler/OWNERS » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2011 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.
4
5 var g_browserBridge;
6 var g_mainView;
7
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.
12
13 /**
14 * Main entry point called once the page has loaded.
15 */
16 function onLoad() {
17 g_browserBridge = new BrowserBridge();
18 g_mainView = new MainView();
19 }
20
21 document.addEventListener('DOMContentLoaded', onLoad);
22
23 /**
24 * This class provides a "bridge" for communicating between the javascript and
25 * the browser. Used as a singleton.
26 */
27 var BrowserBridge = (function() {
28 'use strict';
29
30 /**
31 * @constructor
32 */
33 function BrowserBridge() {
34 }
35
36 BrowserBridge.prototype = {
37 //--------------------------------------------------------------------------
38 // Messages sent to the browser
39 //--------------------------------------------------------------------------
40
41 sendGetData: function() {
42 chrome.send('getData');
43 },
44
45 sendResetData: function() {
46 chrome.send('resetData');
47 },
48
49 //--------------------------------------------------------------------------
50 // Messages received from the browser.
51 //--------------------------------------------------------------------------
52
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);
57 },
58 };
59
60 return BrowserBridge;
61 })();
62
63 /**
64 * This class handles the presentation of our profiler view. Used as a
65 * singleton.
66 */
67 var MainView = (function() {
68 'use strict';
69
70 // --------------------------------------------------------------------------
71 // Important IDs in the HTML document
72 // --------------------------------------------------------------------------
73
74 // The search box to filter results.
75 var FILTER_SEARCH_ID = 'filter-search';
76
77 // The container node to put all the "Group by" dropdowns into.
78 var GROUP_BY_CONTAINER_ID = 'group-by-container';
79
80 // The container node to put all the "Sort by" dropdowns into.
81 var SORT_BY_CONTAINER_ID = 'sort-by-container';
82
83 // The DIV to put all the tables into.
84 var RESULTS_DIV_ID = 'results-div';
85
86 // The container node to put all the column (visibility) checkboxes into.
87 var COLUMN_TOGGLES_CONTAINER_ID = 'column-toggles-container';
88
89 // The container node to put all the column (merge) checkboxes into.
90 var COLUMN_MERGE_TOGGLES_CONTAINER_ID = 'column-merge-toggles-container';
91
92 // The anchor which toggles visibility of column checkboxes.
93 var EDIT_COLUMNS_LINK_ID = 'edit-columns-link';
94
95 // The container node to show/hide when toggling the column checkboxes.
96 var EDIT_COLUMNS_ROW = 'edit-columns-row';
97
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';
101
102 var RESET_DATA_LINK_ID = 'reset-data-link';
103
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 = 'snapshot-button';
108
109 // --------------------------------------------------------------------------
110 // Row keys
111 // --------------------------------------------------------------------------
112
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.
117 //
118 // Note that the declaration order will also define the default display order.
119
120 var BEGIN_KEY = 1; // Start at 1 rather than 0 to simplify sorting code.
121 var END_KEY = BEGIN_KEY;
122
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++;
138
139 var NUM_KEYS = END_KEY - BEGIN_KEY;
140
141 // --------------------------------------------------------------------------
142 // Aggregators
143 // --------------------------------------------------------------------------
144
145 // To generalize computing/displaying the aggregate "counts" for each column,
146 // we specify an optional "Aggregator" class to use with each property.
147
148 // The following are actually "Aggregator factories". They create an
149 // aggregator instance by calling 'create()'. The instance is then fed
150 // each row one at a time via the 'consume()' method. After all rows have
151 // been consumed, the 'getValueAsText()' method will return the aggregated
152 // value.
153
154 /**
155 * This aggregator counts the number of unique values that were fed to it.
156 */
157 var UniquifyAggregator = (function() {
158 function Aggregator(key) {
159 this.key_ = key;
160 this.valuesSet_ = {};
161 }
162
163 Aggregator.prototype = {
164 consume: function(e) {
165 this.valuesSet_[e[this.key_]] = true;
166 },
167
168 getValueAsText: function() {
169 return getDictionaryKeys(this.valuesSet_).length + ' unique'
170 },
171 };
172
173 return {
174 create: function(key) { return new Aggregator(key); }
175 };
176 })();
177
178 /**
179 * This aggregator sums a numeric field.
180 */
181 var SumAggregator = (function() {
182 function Aggregator(key) {
183 this.key_ = key;
184 this.sum_ = 0;
185 }
186
187 Aggregator.prototype = {
188 consume: function(e) {
189 this.sum_ += e[this.key_];
190 },
191
192 getValue: function() {
193 return this.sum_;
194 },
195
196 getValueAsText: function() {
197 return formatNumberAsText(this.getValue());
198 },
199 };
200
201 return {
202 create: function(key) { return new Aggregator(key); }
203 };
204 })();
205
206 /**
207 * This aggregator computes an average by summing two
208 * numeric fields, and then dividing the totals.
209 */
210 var AvgAggregator = (function() {
211 function Aggregator(numeratorKey, divisorKey) {
212 this.numeratorKey_ = numeratorKey;
213 this.divisorKey_ = divisorKey;
214
215 this.numeratorSum_ = 0;
216 this.divisorSum_ = 0;
217 }
218
219 Aggregator.prototype = {
220 consume: function(e) {
221 this.numeratorSum_ += e[this.numeratorKey_];
222 this.divisorSum_ += e[this.divisorKey_];
223 },
224
225 getValue: function() {
226 return this.numeratorSum_ / this.divisorSum_;
227 },
228
229 getValueAsText: function() {
230 return formatNumberAsText(this.getValue());
231 },
232 };
233
234 return {
235 create: function(numeratorKey, divisorKey) {
236 return {
237 create: function(key) {
238 return new Aggregator(numeratorKey, divisorKey);
239 },
240 }
241 }
242 };
243 })();
244
245 /**
246 * This aggregator finds the maximum for a numeric field.
247 */
248 var MaxAggregator = (function() {
249 function Aggregator(key) {
250 this.key_ = key;
251 this.max_ = -Infinity;
252 }
253
254 Aggregator.prototype = {
255 consume: function(e) {
256 this.max_ = Math.max(this.max_, e[this.key_]);
257 },
258
259 getValue: function() {
260 return this.max_;
261 },
262
263 getValueAsText: function() {
264 return formatNumberAsText(this.getValue());
265 },
266 };
267
268 return {
269 create: function(key) { return new Aggregator(key); }
270 };
271 })();
272
273 // --------------------------------------------------------------------------
274 // Key properties
275 // --------------------------------------------------------------------------
276
277 // Custom comparator for thread names (sorts main thread and IO thread
278 // higher than would happen lexicographically.)
279 var threadNameComparator =
280 createLexicographicComparatorWithExceptions([
281 'CrBrowserMain',
282 'Chrome_IOThread',
283 'Chrome_FileThread',
284 'Chrome_HistoryThread',
285 'Chrome_DBThread',
286 'Still_Alive',
287 ]);
288
289 function diffFuncForCount(a, b) {
290 return b - a;
291 }
292
293 function diffFuncForMax(a, b) {
294 return b;
295 }
296
297 /**
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.
302 *
303 * --------------------------------------
304 * The following properties are required:
305 * --------------------------------------
306 *
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.
310 *
311 * --------------------------------------
312 * The following properties are optional:
313 * --------------------------------------
314 *
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.
334 */
335 var KEY_PROPERTIES = [];
336
337 KEY_PROPERTIES[KEY_PROCESS_ID] = {
338 name: 'PID',
339 cellAlignment: 'right',
340 aggregator: UniquifyAggregator,
341 };
342
343 KEY_PROPERTIES[KEY_PROCESS_TYPE] = {
344 name: 'Process type',
345 aggregator: UniquifyAggregator,
346 };
347
348 KEY_PROPERTIES[KEY_BIRTH_THREAD] = {
349 name: 'Birth thread',
350 inputJsonKey: 'birth_thread',
351 aggregator: UniquifyAggregator,
352 comparator: threadNameComparator,
353 };
354
355 KEY_PROPERTIES[KEY_DEATH_THREAD] = {
356 name: 'Exec thread',
357 inputJsonKey: 'death_thread',
358 aggregator: UniquifyAggregator,
359 comparator: threadNameComparator,
360 };
361
362 KEY_PROPERTIES[KEY_FUNCTION_NAME] = {
363 name: 'Function name',
364 inputJsonKey: 'location.function_name',
365 aggregator: UniquifyAggregator,
366 };
367
368 KEY_PROPERTIES[KEY_FILE_NAME] = {
369 name: 'File name',
370 inputJsonKey: 'location.file_name',
371 aggregator: UniquifyAggregator,
372 };
373
374 KEY_PROPERTIES[KEY_LINE_NUMBER] = {
375 name: 'Line number',
376 cellAlignment: 'right',
377 inputJsonKey: 'location.line_number',
378 aggregator: UniquifyAggregator,
379 };
380
381 KEY_PROPERTIES[KEY_COUNT] = {
382 name: 'Count',
383 cellAlignment: 'right',
384 sortDescending: true,
385 textPrinter: formatNumberAsText,
386 inputJsonKey: 'death_data.count',
387 aggregator: SumAggregator,
388 diff: diffFuncForCount,
389 };
390
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,
399 };
400
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,
409 };
410
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,
419 };
420
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),
427 };
428
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,
437 };
438
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),
445 };
446
447 KEY_PROPERTIES[KEY_SOURCE_LOCATION] = {
448 name: 'Source location',
449 type: 'string',
450 aggregator: UniquifyAggregator,
451 };
452
453 /**
454 * Returns the string name for |key|.
455 */
456 function getNameForKey(key) {
457 var props = KEY_PROPERTIES[key];
458 if (props == undefined)
459 throw 'Did not define properties for key: ' + key;
460 return props.name;
461 }
462
463 /**
464 * Ordered list of all keys. This is the order we generally want
465 * to display the properties in. Default to declaration order.
466 */
467 var ALL_KEYS = [];
468 for (var k = BEGIN_KEY; k < END_KEY; ++k)
469 ALL_KEYS.push(k);
470
471 // --------------------------------------------------------------------------
472 // Default settings
473 // --------------------------------------------------------------------------
474
475 /**
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]).
478 */
479 var INITIALLY_HIDDEN_KEYS = [
480 KEY_FILE_NAME,
481 KEY_LINE_NUMBER,
482 KEY_QUEUE_TIME,
483 ];
484
485 /**
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.
489 */
490 var GROUPING_DROPDOWN_CHOICES = [
491 KEY_PROCESS_TYPE,
492 KEY_PROCESS_ID,
493 KEY_BIRTH_THREAD,
494 KEY_DEATH_THREAD,
495 KEY_FUNCTION_NAME,
496 KEY_SOURCE_LOCATION,
497 KEY_FILE_NAME,
498 KEY_LINE_NUMBER,
499 ];
500
501 /**
502 * The ordered list of sorting choices to expose in the "Sort by"
503 * dropdowns.
504 */
505 var SORT_DROPDOWN_CHOICES = ALL_KEYS;
506
507 /**
508 * The ordered list of all columns that can be displayed in the tables (not
509 * including whatever has been hidden via [Edit Columns]).
510 */
511 var ALL_TABLE_COLUMNS = ALL_KEYS;
512
513 /**
514 * The initial keys to sort by when loading the page (can be changed later).
515 */
516 var INITIAL_SORT_KEYS = [-KEY_COUNT];
517
518 /**
519 * The default sort keys to use when nothing has been specified.
520 */
521 var DEFAULT_SORT_KEYS = [-KEY_COUNT];
522
523 /**
524 * The initial keys to group by when loading the page (can be changed later).
525 */
526 var INITIAL_GROUP_KEYS = [];
527
528 /**
529 * The columns to give the option to merge on.
530 */
531 var MERGEABLE_KEYS = [
532 KEY_PROCESS_ID,
533 KEY_PROCESS_TYPE,
534 KEY_BIRTH_THREAD,
535 KEY_DEATH_THREAD,
536 ];
537
538 /**
539 * The columns to merge by default.
540 */
541 var INITIALLY_MERGED_KEYS = [];
542
543 /**
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.
549 */
550 var IDENTITY_KEYS = [
551 KEY_BIRTH_THREAD,
552 KEY_DEATH_THREAD,
553 KEY_PROCESS_TYPE,
554 KEY_PROCESS_ID,
555 KEY_FUNCTION_NAME,
556 KEY_SOURCE_LOCATION,
557 KEY_FILE_NAME,
558 KEY_LINE_NUMBER,
559 ];
560
561 /**
562 * The time (in milliseconds) to wait after receiving new data before
563 * re-drawing it to the screen. The reason we wait a bit is to avoid
564 * repainting repeatedly during the loading phase (which can slow things
565 * down). Note that this only slows down the addition of new data. It does
566 * not impact the latency of user-initiated operations like sorting or
567 * merging.
568 */
569 var PROCESS_DATA_DELAY_MS = 500;
570
571 /**
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.
575 */
576 var INITIAL_UNGROUPED_ROW_LIMIT = 30;
577
578 /**
579 * The initial number of rows to display (rest are hidden) for each group.
580 */
581 var INITIAL_GROUP_ROW_LIMIT = 10;
582
583 /**
584 * The number of extra rows to show/hide when clicking the "Show more" or
585 * "Show less" buttons.
586 */
587 var LIMIT_INCREMENT = 10;
588
589 // --------------------------------------------------------------------------
590 // General utility functions
591 // --------------------------------------------------------------------------
592
593 /**
594 * Returns a list of all the keys in |dict|.
595 */
596 function getDictionaryKeys(dict) {
597 var keys = [];
598 for (var key in dict) {
599 keys.push(key);
600 }
601 return keys;
602 }
603
604 /**
605 * Formats the number |x| as a decimal integer. Strips off any decimal parts,
606 * and comma separates the number every 3 characters.
607 */
608 function formatNumberAsText(x) {
609 var orig = x.toFixed(0);
610
611 var parts = [];
612 for (var end = orig.length; end > 0; ) {
613 var chunk = Math.min(end, 3);
614 parts.push(orig.substr(end-chunk, chunk));
615 end -= chunk;
616 }
617 return parts.reverse().join(',');
618 }
619
620 /**
621 * Simple comparator function which works for both strings and numbers.
622 */
623 function simpleCompare(a, b) {
624 if (a == b)
625 return 0;
626 if (a < b)
627 return -1;
628 return 1;
629 }
630
631 /**
632 * Returns a comparator function that compares values lexicographically,
633 * but special-cases the values in |orderedList| to have a higher
634 * rank.
635 */
636 function createLexicographicComparatorWithExceptions(orderedList) {
637 var valueToRankMap = {};
638 for (var i = 0; i < orderedList.length; ++i)
639 valueToRankMap[orderedList[i]] = i;
640
641 function getCustomRank(x) {
642 var rank = valueToRankMap[x];
643 if (rank == undefined)
644 rank = Infinity; // Unmatched.
645 return rank;
646 }
647
648 return function(a, b) {
649 var aRank = getCustomRank(a);
650 var bRank = getCustomRank(b);
651
652 // Not matched by any of our exceptions.
653 if (aRank == bRank)
654 return simpleCompare(a, b);
655
656 if (aRank < bRank)
657 return -1;
658 return 1;
659 };
660 }
661
662 /**
663 * Returns dict[key]. Note that if |key| contains periods (.), they will be
664 * interpreted as meaning a sub-property.
665 */
666 function getPropertyByPath(dict, key) {
667 var cur = dict;
668 var parts = key.split('.');
669 for (var i = 0; i < parts.length; ++i) {
670 if (cur == undefined)
671 return undefined;
672 cur = cur[parts[i]];
673 }
674 return cur;
675 }
676
677 /**
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.
680 */
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);
686 }
687 return n;
688 }
689
690 /**
691 * Adds |text| to |parent|.
692 */
693 function addText(parent, text) {
694 var textNode = parent.ownerDocument.createTextNode(text);
695 parent.appendChild(textNode);
696 return textNode;
697 }
698
699 /**
700 * Deletes all the strings in |array| which appear in |valuesToDelete|.
701 */
702 function deleteValuesFromArray(array, valuesToDelete) {
703 var valueSet = arrayToSet(valuesToDelete);
704 for (var i = 0; i < array.length; ) {
705 if (valueSet[array[i]]) {
706 array.splice(i, 1);
707 } else {
708 i++;
709 }
710 }
711 }
712
713 /**
714 * Deletes all the repeated ocurrences of strings in |array|.
715 */
716 function deleteDuplicateStringsFromArray(array) {
717 // Build up set of each entry in array.
718 var seenSoFar = {};
719
720 for (var i = 0; i < array.length; ) {
721 var value = array[i];
722 if (seenSoFar[value]) {
723 array.splice(i, 1);
724 } else {
725 seenSoFar[value] = true;
726 i++;
727 }
728 }
729 }
730
731 /**
732 * Builds a map out of the array |list|.
733 */
734 function arrayToSet(list) {
735 var set = {};
736 for (var i = 0; i < list.length; ++i)
737 set[list[i]] = true;
738 return set;
739 }
740
741 function trimWhitespace(text) {
742 var m = /^\s*(.*)\s*$/.exec(text);
743 return m[1];
744 }
745
746 /**
747 * Selects the option in |select| which has a value of |value|.
748 */
749 function setSelectedOptionByValue(select, value) {
750 for (var i = 0; i < select.options.length; ++i) {
751 if (select.options[i].value == value) {
752 select.options[i].selected = true;
753 return true;
754 }
755 }
756 return false;
757 }
758
759 /**
760 * Adds a checkbox to |parent|. The checkbox will have a label on its right
761 * with text |label|. Returns the checkbox input node.
762 */
763 function addLabeledCheckbox(parent, label) {
764 var labelNode = addNode(parent, 'label');
765 var checkbox = addNode(labelNode, 'input');
766 checkbox.type = 'checkbox';
767 addText(labelNode, label);
768 return checkbox;
769 }
770
771 /**
772 * Return the last component in a path which is separated by either forward
773 * slashes or backslashes.
774 */
775 function getFilenameFromPath(path) {
776 var lastSlash = Math.max(path.lastIndexOf('/'),
777 path.lastIndexOf('\\'));
778 if (lastSlash == -1)
779 return path;
780
781 return path.substr(lastSlash + 1);
782 }
783
784 /**
785 * Returns the current time in milliseconds since unix epoch.
786 */
787 function getTimeMillis() {
788 return (new Date()).getTime();
789 }
790
791 /**
792 * Toggle a node between hidden/invisible.
793 */
794 function toggleNodeDisplay(n) {
795 if (n.style.display == '') {
796 n.style.display = 'none';
797 } else {
798 n.style.display = '';
799 }
800 }
801
802 // --------------------------------------------------------------------------
803 // Functions that augment, bucket, and compute aggregates for the input data.
804 // --------------------------------------------------------------------------
805
806 /**
807 * Adds new derived properties to row. Mutates the provided dictionary |e|.
808 */
809 function augmentDataRow(e) {
810 computeDataRowAverages(e);
811 e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']';
812 }
813
814 function computeDataRowAverages(e) {
815 e[KEY_AVG_QUEUE_TIME] = e[KEY_QUEUE_TIME] / e[KEY_COUNT];
816 e[KEY_AVG_RUN_TIME] = e[KEY_RUN_TIME] / e[KEY_COUNT];
817 }
818
819 /**
820 * Creates and initializes an aggregator object for each key in |columns|.
821 * Returns an array whose keys are values from |columns|, and whose
822 * values are Aggregator instances.
823 */
824 function initializeAggregates(columns) {
825 var aggregates = [];
826
827 for (var i = 0; i < columns.length; ++i) {
828 var key = columns[i];
829 var aggregatorFactory = KEY_PROPERTIES[key].aggregator;
830 aggregates[key] = aggregatorFactory.create(key);
831 }
832
833 return aggregates;
834 }
835
836 function consumeAggregates(aggregates, row) {
837 for (var key in aggregates)
838 aggregates[key].consume(row);
839 }
840
841 function bucketIdenticalRows(rows, identityKeys, propertyGetterFunc) {
842 var identicalRows = {};
843 for (var i = 0; i < rows.length; ++i) {
844 var r = rows[i];
845
846 var rowIdentity = [];
847 for (var j = 0; j < identityKeys.length; ++j)
848 rowIdentity.push(propertyGetterFunc(r, identityKeys[j]));
849 rowIdentity = rowIdentity.join('\n');
850
851 var l = identicalRows[rowIdentity];
852 if (!l) {
853 l = [];
854 identicalRows[rowIdentity] = l;
855 }
856 l.push(r);
857 }
858 return identicalRows;
859 }
860
861 /**
862 * Merges the rows in |origRows|, by collapsing the columns listed in
863 * |mergeKeys|. Returns an array with the merged rows (in no particular
864 * order).
865 *
866 * If |mergeSimilarThreads| is true, then threads with a similar name will be
867 * considered equivalent. For instance, "WorkerThread-1" and "WorkerThread-2"
868 * will be remapped to "WorkerThread-*".
869 */
870 function mergeRows(origRows, mergeKeys, mergeSimilarThreads) {
871 // Define a translation function for each property. Normally we copy over
872 // properties as-is, but if we have been asked to "merge similar threads" we
873 // we will remap the thread names that end in a numeric suffix.
874 var propertyGetterFunc;
875
876 if (mergeSimilarThreads) {
877 propertyGetterFunc = function(row, key) {
878 var value = row[key];
879 // If the property is a thread name, try to remap it.
880 if (key == KEY_BIRTH_THREAD || key == KEY_DEATH_THREAD) {
881 var m = /^(.*[^\d])(\d+)$/.exec(value);
882 if (m)
883 value = m[1] + '*';
884 }
885 return value;
886 }
887 } else {
888 propertyGetterFunc = function(row, key) { return row[key]; };
889 }
890
891 // Determine which sets of properties a row needs to match on to be
892 // considered identical to another row.
893 var identityKeys = IDENTITY_KEYS.slice(0);
894 deleteValuesFromArray(identityKeys, mergeKeys);
895
896 // Set |aggregateKeys| to everything else, since we will be aggregating
897 // their value as part of the merge.
898 var aggregateKeys = ALL_KEYS.slice(0);
899 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
900
901 // Group all the identical rows together, bucketed into |identicalRows|.
902 var identicalRows =
903 bucketIdenticalRows(origRows, identityKeys, propertyGetterFunc);
904
905 var mergedRows = [];
906
907 // Merge the rows and save the results to |mergedRows|.
908 for (var k in identicalRows) {
909 // We need to smash the list |l| down to a single row...
910 var l = identicalRows[k];
911
912 var newRow = [];
913 mergedRows.push(newRow);
914
915 // Copy over all the identity columns to the new row (since they
916 // were the same for each row matched).
917 for (var i = 0; i < identityKeys.length; ++i)
918 newRow[identityKeys[i]] = propertyGetterFunc(l[0], identityKeys[i]);
919
920 // Compute aggregates for the other columns.
921 var aggregates = initializeAggregates(aggregateKeys);
922
923 // Feed the rows to the aggregators.
924 for (var i = 0; i < l.length; ++i)
925 consumeAggregates(aggregates, l[i]);
926
927 // Suck out the data generated by the aggregators.
928 for (var aggregateKey in aggregates)
929 newRow[aggregateKey] = aggregates[aggregateKey].getValue();
930 }
931
932 return mergedRows;
933 }
934
935 /**
936 * Takes two flat lists data1 and data2, and returns a new flat list which
937 * represents the difference between them. The exact meaning of "difference"
938 * is column specific, but for most numeric fields (like the count, or total
939 * time), it is found by subtracting.
940 *
941 * TODO(eroman): Some of this code is duplicated from mergeRows().
942 */
943 function subtractSnapshots(data1, data2) {
944 // These columns are computed from the other columns. We won't bother
945 // diffing/aggregating these, but rather will derive them again from the
946 // final row.
947 var COMPUTED_AGGREGATE_KEYS = [KEY_AVG_QUEUE_TIME, KEY_AVG_RUN_TIME];
948
949 // These are the keys which determine row equality. Since we are not doing
950 // any merging yet at this point, it is simply the list of all identity
951 // columns.
952 var identityKeys = IDENTITY_KEYS;
953
954 // The columns to compute via aggregation is everything else.
955 var aggregateKeys = ALL_KEYS.slice(0);
956 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
957 deleteValuesFromArray(aggregateKeys, COMPUTED_AGGREGATE_KEYS);
958
959 // Group all the identical rows for each list together.
960 var propertyGetterFunc = function(row, key) { return row[key]; };
961 var identicalRows1 =
962 bucketIdenticalRows(data1, identityKeys, propertyGetterFunc);
963 var identicalRows2 =
964 bucketIdenticalRows(data2, identityKeys, propertyGetterFunc);
965
966 var diffedRows = [];
967
968 for (var k in identicalRows2) {
969 var rows2 = identicalRows2[k];
970 var rows1 = identicalRows1[k];
971 if (rows1 == undefined)
972 rows1 = [];
973
974 var newRow = [];
975
976 // Copy over all the identity columns to the new row (since they
977 // were the same for each row matched).
978 for (var i = 0; i < identityKeys.length; ++i)
979 newRow[identityKeys[i]] = propertyGetterFunc(rows2[0], identityKeys[i]);
980
981 // The raw data for each snapshot *may* have contained duplicate rows, so
982 // smash them down into a single row using our aggregation functions.
983 var aggregates1 = initializeAggregates(aggregateKeys);
984 var aggregates2 = initializeAggregates(aggregateKeys);
985 for (var i = 0; i < rows1.length; ++i)
986 consumeAggregates(aggregates1, rows1[i]);
987 for (var i = 0; i < rows2.length; ++i)
988 consumeAggregates(aggregates2, rows2[i]);
989
990 // Finally, diff the two merged rows.
991 for (var aggregateKey in aggregates2) {
992 var a = aggregates1[aggregateKey].getValue();
993 var b = aggregates2[aggregateKey].getValue();
994
995 var diffFunc = KEY_PROPERTIES[aggregateKey].diff;
996 newRow[aggregateKey] = diffFunc(a, b);
997 }
998
999 if (newRow[KEY_COUNT] == 0) {
1000 // If a row's count has gone to zero, it means there were no new
1001 // occurrences of it in the second snapshot, so remove it.
1002 continue;
1003 }
1004
1005 // Since we excluded the averages during diffing phase, re-compute them
1006 // using the diffed totals.
1007 computeDataRowAverages(newRow);
1008 diffedRows.push(newRow);
1009 }
1010
1011 return diffedRows;
1012 }
1013
1014 // --------------------------------------------------------------------------
1015 // HTML drawing code
1016 // --------------------------------------------------------------------------
1017
1018 function getTextValueForProperty(key, value) {
1019 if (value == undefined) {
1020 // A value may be undefined as a result of having merging rows. We
1021 // won't actually draw it, but this might be called by the filter.
1022 return '';
1023 }
1024
1025 var textPrinter = KEY_PROPERTIES[key].textPrinter;
1026 if (textPrinter)
1027 return textPrinter(value);
1028 return value.toString();
1029 }
1030
1031 /**
1032 * Renders the property value |value| into cell |td|. The name of this
1033 * property is |key|.
1034 */
1035 function drawValueToCell(td, key, value) {
1036 // Get a text representation of the value.
1037 var text = getTextValueForProperty(key, value);
1038
1039 // Apply the desired cell alignment.
1040 var cellAlignment = KEY_PROPERTIES[key].cellAlignment;
1041 if (cellAlignment)
1042 td.align = cellAlignment;
1043
1044 if (key == KEY_SOURCE_LOCATION) {
1045 // Linkify the source column so it jumps to the source code. This doesn't
1046 // take into account the particular code this build was compiled from, or
1047 // local edits to source. It should however work correctly for top of tree
1048 // builds.
1049 var m = /^(.*) \[(\d+)\]$/.exec(text);
1050 if (m) {
1051 var filepath = m[1];
1052 var filename = getFilenameFromPath(filepath);
1053 var linenumber = m[2];
1054
1055 var link = addNode(td, 'a', filename + ' [' + linenumber + ']');
1056 // http://chromesrc.appspot.com is a server I wrote specifically for
1057 // this task. It redirects to the appropriate source file; the file
1058 // paths given by the compiler can be pretty crazy and different
1059 // between platforms.
1060 link.href = 'http://chromesrc.appspot.com/?path=' +
1061 encodeURIComponent(filepath) + '&line=' + linenumber;
1062 link.target = '_blank';
1063 return;
1064 }
1065 }
1066
1067 // String values can get pretty long. If the string contains no spaces, then
1068 // CSS fails to wrap it, and it overflows the cell causing the table to get
1069 // really big. We solve this using a hack: insert a <wbr> element after
1070 // every single character. This will allow the rendering engine to wrap the
1071 // value, and hence avoid it overflowing!
1072 var kMinLengthBeforeWrap = 20;
1073
1074 addText(td, text.substr(0, kMinLengthBeforeWrap));
1075 for (var i = kMinLengthBeforeWrap; i < text.length; ++i) {
1076 addNode(td, 'wbr');
1077 addText(td, text.substr(i, 1));
1078 }
1079 }
1080
1081 // --------------------------------------------------------------------------
1082 // Helper code for handling the sort and grouping dropdowns.
1083 // --------------------------------------------------------------------------
1084
1085 function addOptionsForGroupingSelect(select) {
1086 // Add "no group" choice.
1087 addNode(select, 'option', '---').value = '';
1088
1089 for (var i = 0; i < GROUPING_DROPDOWN_CHOICES.length; ++i) {
1090 var key = GROUPING_DROPDOWN_CHOICES[i];
1091 var option = addNode(select, 'option', getNameForKey(key));
1092 option.value = key;
1093 }
1094 }
1095
1096 function addOptionsForSortingSelect(select) {
1097 // Add "no sort" choice.
1098 addNode(select, 'option', '---').value = '';
1099
1100 // Add a divider.
1101 addNode(select, 'optgroup').label = '';
1102
1103 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1104 var key = SORT_DROPDOWN_CHOICES[i];
1105 addNode(select, 'option', getNameForKey(key)).value = key;
1106 }
1107
1108 // Add a divider.
1109 addNode(select, 'optgroup').label = '';
1110
1111 // Add the same options, but for descending.
1112 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1113 var key = SORT_DROPDOWN_CHOICES[i];
1114 var n = addNode(select, 'option', getNameForKey(key) + ' (DESC)');
1115 n.value = reverseSortKey(key);
1116 }
1117 }
1118
1119 /**
1120 * Helper function used to update the sorting and grouping lists after a
1121 * dropdown changes.
1122 */
1123 function updateKeyListFromDropdown(list, i, select) {
1124 // Update the list.
1125 if (i < list.length) {
1126 list[i] = select.value;
1127 } else {
1128 list.push(select.value);
1129 }
1130
1131 // Normalize the list, so setting 'none' as primary zeros out everything
1132 // else.
1133 for (var i = 0; i < list.length; ++i) {
1134 if (list[i] == '') {
1135 list.splice(i, list.length - i);
1136 break;
1137 }
1138 }
1139 }
1140
1141 /**
1142 * Comparator for property |key|, having values |value1| and |value2|.
1143 * If the key has defined a custom comparator use it. Otherwise use a
1144 * default "less than" comparison.
1145 */
1146 function compareValuesForKey(key, value1, value2) {
1147 var comparator = KEY_PROPERTIES[key].comparator;
1148 if (comparator)
1149 return comparator(value1, value2);
1150 return simpleCompare(value1, value2);
1151 }
1152
1153 function reverseSortKey(key) {
1154 return -key;
1155 }
1156
1157 function sortKeyIsReversed(key) {
1158 return key < 0;
1159 }
1160
1161 function sortKeysMatch(key1, key2) {
1162 return Math.abs(key1) == Math.abs(key2);
1163 }
1164
1165 function getKeysForCheckedBoxes(checkboxes) {
1166 var keys = [];
1167 for (var k in checkboxes) {
1168 if (checkboxes[k].checked)
1169 keys.push(k);
1170 }
1171 return keys;
1172 }
1173
1174 // --------------------------------------------------------------------------
1175
1176 /**
1177 * @constructor
1178 */
1179 function MainView() {
1180 // Make sure we have a definition for each key.
1181 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1182 if (!KEY_PROPERTIES[k])
1183 throw 'KEY_PROPERTIES[] not defined for key: ' + k;
1184 }
1185
1186 this.init_();
1187 }
1188
1189 MainView.prototype = {
1190 addDataToSnapshot: function(data) {
1191 // TODO(eroman): We need to know which snapshot this data belongs to!
1192 // For now we assume it is the most recent snapshot.
1193 var snapshotIndex = this.snapshots_.length - 1;
1194
1195 var snapshot = this.snapshots_[snapshotIndex];
1196
1197 var pid = data.process_id;
1198 var ptype = data.process_type;
1199
1200 // Augment each data row with the process information.
1201 var rows = data.list;
1202 for (var i = 0; i < rows.length; ++i) {
1203 // Transform the data from a dictionary to an array. This internal
1204 // representation is more compact and faster to access.
1205 var origRow = rows[i];
1206 var newRow = [];
1207
1208 newRow[KEY_PROCESS_ID] = pid;
1209 newRow[KEY_PROCESS_TYPE] = ptype;
1210
1211 // Copy over the known properties which have a 1:1 mapping with JSON.
1212 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1213 var inputJsonKey = KEY_PROPERTIES[k].inputJsonKey;
1214 if (inputJsonKey != undefined) {
1215 newRow[k] = getPropertyByPath(origRow, inputJsonKey);
1216 }
1217 }
1218
1219 if (newRow[KEY_COUNT] == 0) {
1220 // When resetting the data, it is possible for the backend to give us
1221 // counts of "0". There is no point adding these rows (in fact they
1222 // will cause us to do divide by zeros when calculating averages and
1223 // stuff), so we skip past them.
1224 continue;
1225 }
1226
1227 // Add our computed properties.
1228 augmentDataRow(newRow);
1229
1230 snapshot.flatData.push(newRow);
1231 }
1232
1233 if (!arrayToSet(this.getSelectedSnapshotIndexes_())[snapshotIndex]) {
1234 // Optimization: If this snapshot is not a data dependency for the
1235 // current display, then don't bother updating anything.
1236 return;
1237 }
1238
1239 // We may end up calling addDataToSnapshot_() repeatedly (once for each
1240 // process). To avoid this from slowing us down we do bulk updates on a
1241 // timer.
1242 this.updateFlatDataSoon_();
1243 },
1244
1245 updateFlatDataSoon_: function() {
1246 if (this.updateFlatDataPending_) {
1247 // If a delayed task has already been posted to re-merge the data,
1248 // then we don't need to do anything extra.
1249 return;
1250 }
1251
1252 // Otherwise schedule updateFlatData_() to be called later. We want it to
1253 // be called no more than once every PROCESS_DATA_DELAY_MS milliseconds.
1254
1255 if (this.lastUpdateFlatDataTime_ == undefined)
1256 this.lastUpdateFlatDataTime_ = 0;
1257
1258 var timeSinceLastMerge = getTimeMillis() - this.lastUpdateFlatDataTime_;
1259 var timeToWait = Math.max(0, PROCESS_DATA_DELAY_MS - timeSinceLastMerge);
1260
1261 var functionToRun = function() {
1262 // Do the actual update.
1263 this.updateFlatData_();
1264 // Keep track of when we last ran.
1265 this.lastUpdateFlatDataTime_ = getTimeMillis();
1266 this.updateFlatDataPending_ = false;
1267 }.bind(this);
1268
1269 this.updateFlatDataPending_ = true;
1270 window.setTimeout(functionToRun, timeToWait);
1271 },
1272
1273 /**
1274 * Returns a list of the currently selected snapshots. This list is
1275 * guaranteed to be of length 1 or 2.
1276 */
1277 getSelectedSnapshotIndexes_: function() {
1278 var indexes = this.getSelectedSnapshotBoxes_();
1279 for (var i = 0; i < indexes.length; ++i)
1280 indexes[i] = indexes[i].__index;
1281 return indexes;
1282 },
1283
1284 /**
1285 * Same as getSelectedSnapshotIndexes_(), only it returns the actual
1286 * checkbox input DOM nodes rather than the snapshot ID.
1287 */
1288 getSelectedSnapshotBoxes_: function() {
1289 // Figure out which snaphots to use for our data.
1290 var boxes = [];
1291 for (var i = 0; i < this.snapshots_.length; ++i) {
1292 var box = this.getSnapshotCheckbox_(i);
1293 if (box.checked)
1294 boxes.push(box);
1295 }
1296 return boxes;
1297 },
1298
1299 /**
1300 * This function should be called any time a snapshot dependency for what is
1301 * being displayed on the screen has changed. It will re-calculate the
1302 * difference between the two snapshots and update flatData_.
1303 */
1304 updateFlatData_: function() {
1305 var summaryDiv = $(SNAPSHOT_SELECTION_SUMMARY_ID);
1306
1307 var selectedSnapshots = this.getSelectedSnapshotIndexes_();
1308 if (selectedSnapshots.length == 1) {
1309 // If only one snapshot is chosen then we will display that snapshot's
1310 // data in its entirety.
1311 this.flatData_ = this.snapshots_[selectedSnapshots[0]].flatData;
1312
1313 // Don't bother displaying any text when just 1 snapshot is selected,
1314 // since it is obvious what this should do.
1315 summaryDiv.innerText = '';
1316 } else if (selectedSnapshots.length == 2) {
1317 // Otherwise if two snapshots were chosen, show the difference between
1318 // them.
1319 var snapshot1 = this.snapshots_[selectedSnapshots[0]];
1320 var snapshot2 = this.snapshots_[selectedSnapshots[1]];
1321
1322 this.flatData_ =
1323 subtractSnapshots(snapshot1.flatData, snapshot2.flatData);
1324
1325 var timeDeltaInSeconds =
1326 ((snapshot2.time - snapshot1.time) / 1000).toFixed(0);
1327
1328 // Explain that what is being shown is the difference between two
1329 // snapshots.
1330 summaryDiv.innerText =
1331 'Showing the difference between snapshots #' +
1332 selectedSnapshots[0] + ' and #' +
1333 selectedSnapshots[1] + ' (' + timeDeltaInSeconds +
1334 ' seconds worth of data)';
1335 } else {
1336 // This shouldn't be possible...
1337 throw 'Unexpected number of selected snapshots';
1338 }
1339
1340 // Recompute mergedData_ (since it is derived from flatData_)
1341 this.updateMergedData_();
1342 },
1343
1344 updateMergedData_: function() {
1345 // Recompute mergedData_.
1346 this.mergedData_ = mergeRows(this.flatData_,
1347 this.getMergeColumns_(),
1348 this.shouldMergeSimilarThreads_());
1349
1350 // Recompute filteredData_ (since it is derived from mergedData_)
1351 this.updateFilteredData_();
1352 },
1353
1354 updateFilteredData_: function() {
1355 // Recompute filteredData_.
1356 this.filteredData_ = [];
1357 var filterFunc = this.getFilterFunction_();
1358 for (var i = 0; i < this.mergedData_.length; ++i) {
1359 var r = this.mergedData_[i];
1360 if (!filterFunc(r)) {
1361 // Not matched by our filter, discard.
1362 continue;
1363 }
1364 this.filteredData_.push(r);
1365 }
1366
1367 // Recompute groupedData_ (since it is derived from filteredData_)
1368 this.updateGroupedData_();
1369 },
1370
1371 updateGroupedData_: function() {
1372 // Recompute groupedData_.
1373 var groupKeyToData = {};
1374 var entryToGroupKeyFunc = this.getGroupingFunction_();
1375 for (var i = 0; i < this.filteredData_.length; ++i) {
1376 var r = this.filteredData_[i];
1377
1378 var groupKey = entryToGroupKeyFunc(r);
1379
1380 var groupData = groupKeyToData[groupKey];
1381 if (!groupData) {
1382 groupData = {
1383 key: JSON.parse(groupKey),
1384 aggregates: initializeAggregates(ALL_KEYS),
1385 rows: [],
1386 };
1387 groupKeyToData[groupKey] = groupData;
1388 }
1389
1390 // Add the row to our list.
1391 groupData.rows.push(r);
1392
1393 // Update aggregates for each column.
1394 consumeAggregates(groupData.aggregates, r);
1395 }
1396 this.groupedData_ = groupKeyToData;
1397
1398 // Figure out a display order for the groups themselves.
1399 this.sortedGroupKeys_ = getDictionaryKeys(groupKeyToData);
1400 this.sortedGroupKeys_.sort(this.getGroupSortingFunction_());
1401
1402 // Sort the group data.
1403 this.sortGroupedData_();
1404 },
1405
1406 sortGroupedData_: function() {
1407 var sortingFunc = this.getSortingFunction_();
1408 for (var k in this.groupedData_)
1409 this.groupedData_[k].rows.sort(sortingFunc);
1410
1411 // Every cached data dependency is now up to date, all that is left is
1412 // to actually draw the result.
1413 this.redrawData_();
1414 },
1415
1416 getVisibleColumnKeys_: function() {
1417 // Figure out what columns to include, based on the selected checkboxes.
1418 var columns = this.getSelectionColumns_();
1419 columns = columns.slice(0);
1420
1421 // Eliminate columns which we are merging on.
1422 deleteValuesFromArray(columns, this.getMergeColumns_());
1423
1424 // Eliminate columns which we are grouped on.
1425 if (this.sortedGroupKeys_.length > 0) {
1426 // The grouping will be the the same for each so just pick the first.
1427 var randomGroupKey = this.groupedData_[this.sortedGroupKeys_[0]].key;
1428
1429 // The grouped properties are going to be the same for each row in our,
1430 // table, so avoid drawing them in our table!
1431 var keysToExclude = []
1432
1433 for (var i = 0; i < randomGroupKey.length; ++i)
1434 keysToExclude.push(randomGroupKey[i].key);
1435 deleteValuesFromArray(columns, keysToExclude);
1436 }
1437
1438 // If we are currently showing a "diff", hide the max columns, since we
1439 // are not populating it correctly. See the TODO at the top of this file.
1440 if (this.getSelectedSnapshotIndexes_().length > 1)
1441 deleteValuesFromArray(columns, [KEY_MAX_RUN_TIME, KEY_MAX_QUEUE_TIME]);
1442
1443 return columns;
1444 },
1445
1446 redrawData_: function() {
1447 // Clear the results div, sine we may be overwriting older data.
1448 var parent = $(RESULTS_DIV_ID);
1449 parent.innerHTML = '';
1450
1451 var columns = this.getVisibleColumnKeys_();
1452
1453 // Draw each group.
1454 for (var i = 0; i < this.sortedGroupKeys_.length; ++i) {
1455 var k = this.sortedGroupKeys_[i];
1456 this.drawGroup_(parent, k, columns);
1457 }
1458 },
1459
1460 /**
1461 * Renders the information for a particular group.
1462 */
1463 drawGroup_: function(parent, groupKey, columns) {
1464 var groupData = this.groupedData_[groupKey];
1465
1466 var div = addNode(parent, 'div');
1467 div.className = 'group-container';
1468
1469 this.drawGroupTitle_(div, groupData.key);
1470
1471 var table = addNode(div, 'table');
1472
1473 this.drawDataTable_(table, groupData, columns, groupKey);
1474 },
1475
1476 /**
1477 * Draws a title into |parent| that describes |groupKey|.
1478 */
1479 drawGroupTitle_: function(parent, groupKey) {
1480 if (groupKey.length == 0) {
1481 // Empty group key means there was no grouping.
1482 return;
1483 }
1484
1485 var parent = addNode(parent, 'div');
1486 parent.className = 'group-title-container';
1487
1488 // Each component of the group key represents the "key=value" constraint
1489 // for this group. Show these as an AND separated list.
1490 for (var i = 0; i < groupKey.length; ++i) {
1491 if (i > 0)
1492 addNode(parent, 'i', ' and ');
1493 var e = groupKey[i];
1494 addNode(parent, 'b', getNameForKey(e.key) + ' = ');
1495 addNode(parent, 'span', e.value);
1496 }
1497 },
1498
1499 /**
1500 * Renders a table which summarizes all |column| fields for |data|.
1501 */
1502 drawDataTable_: function(table, data, columns, groupKey) {
1503 table.className = 'results-table';
1504 var thead = addNode(table, 'thead');
1505 var tbody = addNode(table, 'tbody');
1506
1507 var displaySettings = this.getGroupDisplaySettings_(groupKey);
1508 var limit = displaySettings.limit;
1509
1510 this.drawAggregateRow_(thead, data.aggregates, columns);
1511 this.drawTableHeader_(thead, columns);
1512 this.drawTableBody_(tbody, data.rows, columns, limit);
1513 this.drawTruncationRow_(tbody, data.rows.length, limit, columns.length,
1514 groupKey);
1515 },
1516
1517 drawTableHeader_: function(thead, columns) {
1518 var tr = addNode(thead, 'tr');
1519 for (var i = 0; i < columns.length; ++i) {
1520 var key = columns[i];
1521 var th = addNode(tr, 'th', getNameForKey(key));
1522 th.onclick = this.onClickColumn_.bind(this, key);
1523
1524 // Draw an indicator if we are currently sorted on this column.
1525 // TODO(eroman): Should use an icon instead of asterisk!
1526 for (var j = 0; j < this.currentSortKeys_.length; ++j) {
1527 if (sortKeysMatch(this.currentSortKeys_[j], key)) {
1528 var sortIndicator = addNode(th, 'span', '*');
1529 sortIndicator.style.color = 'red';
1530 if (sortKeyIsReversed(this.currentSortKeys_[j])) {
1531 // Use double-asterisk for descending columns.
1532 addText(sortIndicator, '*');
1533 }
1534 break;
1535 }
1536 }
1537 }
1538 },
1539
1540 drawTableBody_: function(tbody, rows, columns, limit) {
1541 for (var i = 0; i < rows.length && i < limit; ++i) {
1542 var e = rows[i];
1543
1544 var tr = addNode(tbody, 'tr');
1545
1546 for (var c = 0; c < columns.length; ++c) {
1547 var key = columns[c];
1548 var value = e[key];
1549
1550 var td = addNode(tr, 'td');
1551 drawValueToCell(td, key, value);
1552 }
1553 }
1554 },
1555
1556 /**
1557 * Renders a row that describes all the aggregate values for |columns|.
1558 */
1559 drawAggregateRow_: function(tbody, aggregates, columns) {
1560 var tr = addNode(tbody, 'tr');
1561 tr.className = 'aggregator-row';
1562
1563 for (var i = 0; i < columns.length; ++i) {
1564 var key = columns[i];
1565 var td = addNode(tr, 'td');
1566
1567 // Most of our outputs are numeric, so we want to align them to the
1568 // right. However for the unique counts we will center.
1569 if (KEY_PROPERTIES[key].aggregator == UniquifyAggregator) {
1570 td.align = 'center';
1571 } else {
1572 td.align = 'right';
1573 }
1574
1575 var aggregator = aggregates[key];
1576 if (aggregator)
1577 td.innerText = aggregator.getValueAsText();
1578 }
1579 },
1580
1581 /**
1582 * Renders a row which describes how many rows the table has, how many are
1583 * currently hidden, and a set of buttons to show more.
1584 */
1585 drawTruncationRow_: function(tbody, numRows, limit, numColumns, groupKey) {
1586 var numHiddenRows = Math.max(numRows - limit, 0);
1587 var numVisibleRows = numRows - numHiddenRows;
1588
1589 var tr = addNode(tbody, 'tr');
1590 tr.className = 'truncation-row';
1591 var td = addNode(tr, 'td');
1592 td.colSpan = numColumns;
1593
1594 addText(td, numRows + ' rows');
1595 if (numHiddenRows > 0) {
1596 var s = addNode(td, 'span', ' (' + numHiddenRows + ' hidden) ');
1597 s.style.color = 'red';
1598 }
1599
1600 if (numVisibleRows > LIMIT_INCREMENT) {
1601 addNode(td, 'button', 'Show less').onclick =
1602 this.changeGroupDisplayLimit_.bind(
1603 this, groupKey, -LIMIT_INCREMENT);
1604 }
1605 if (numVisibleRows > 0) {
1606 addNode(td, 'button', 'Show none').onclick =
1607 this.changeGroupDisplayLimit_.bind(this, groupKey, -Infinity);
1608 }
1609
1610 if (numHiddenRows > 0) {
1611 addNode(td, 'button', 'Show more').onclick =
1612 this.changeGroupDisplayLimit_.bind(this, groupKey, LIMIT_INCREMENT);
1613 addNode(td, 'button', 'Show all').onclick =
1614 this.changeGroupDisplayLimit_.bind(this, groupKey, Infinity);
1615 }
1616 },
1617
1618 /**
1619 * Adjusts the row limit for group |groupKey| by |delta|.
1620 */
1621 changeGroupDisplayLimit_: function(groupKey, delta) {
1622 // Get the current settings for this group.
1623 var settings = this.getGroupDisplaySettings_(groupKey, true);
1624
1625 // Compute the adjusted limit.
1626 var newLimit = settings.limit;
1627 var totalNumRows = this.groupedData_[groupKey].rows.length;
1628 newLimit = Math.min(totalNumRows, newLimit);
1629 newLimit += delta;
1630 newLimit = Math.max(0, newLimit);
1631
1632 // Update the settings with the new limit.
1633 settings.limit = newLimit;
1634
1635 // TODO(eroman): It isn't necessary to redraw *all* the data. Really we
1636 // just need to insert the missing rows (everything else stays the same)!
1637 this.redrawData_();
1638 },
1639
1640 /**
1641 * Returns the rendering settings for group |groupKey|. This includes things
1642 * like how many rows to display in the table.
1643 */
1644 getGroupDisplaySettings_: function(groupKey, opt_create) {
1645 var settings = this.groupDisplaySettings_[groupKey];
1646 if (!settings) {
1647 // If we don't have any settings for this group yet, create some
1648 // default ones.
1649 if (groupKey == '[]') {
1650 // (groupKey of '[]' is what we use for ungrouped data).
1651 settings = {limit: INITIAL_UNGROUPED_ROW_LIMIT};
1652 } else {
1653 settings = {limit: INITIAL_GROUP_ROW_LIMIT};
1654 }
1655 if (opt_create)
1656 this.groupDisplaySettings_[groupKey] = settings;
1657 }
1658 return settings;
1659 },
1660
1661 init_: function() {
1662 this.snapshots_ = [];
1663
1664 // Start fetching the data from the browser; this will be our snapshot #0.
1665 this.takeSnapshot_();
1666
1667 // Data goes through the following pipeline:
1668 // (1) Raw data received from browser, and transformed into our own
1669 // internal row format (where properties are indexed by KEY_*
1670 // constants.)
1671 // (2) We "augment" each row by adding some extra computed columns
1672 // (like averages).
1673 // (3) The rows are merged using current merge settings.
1674 // (4) The rows that don't match current search expression are
1675 // tossed out.
1676 // (5) The rows are organized into "groups" based on current settings,
1677 // and aggregate values are computed for each resulting group.
1678 // (6) The rows within each group are sorted using current settings.
1679 // (7) The grouped rows are drawn to the screen.
1680 this.flatData_ = [];
1681 this.mergedData_ = [];
1682 this.filteredData_ = [];
1683 this.groupedData_ = {};
1684 this.sortedGroupKeys_ = [];
1685
1686 this.groupDisplaySettings_ = {};
1687
1688 this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID));
1689 this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID));
1690
1691 $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this);
1692
1693 this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0);
1694 this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0);
1695
1696 this.fillGroupingDropdowns_();
1697 this.fillSortingDropdowns_();
1698
1699 $(EDIT_COLUMNS_LINK_ID).onclick =
1700 toggleNodeDisplay.bind(null, $(EDIT_COLUMNS_ROW));
1701
1702 $(TOGGLE_SNAPSHOTS_LINK_ID).onclick =
1703 toggleNodeDisplay.bind(null, $(SNAPSHOTS_ROW));
1704
1705 $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange =
1706 this.onMergeSimilarThreadsCheckboxChanged_.bind(this);
1707
1708 $(RESET_DATA_LINK_ID).onclick =
1709 g_browserBridge.sendResetData.bind(g_browserBridge);
1710
1711 $(TAKE_SNAPSHOT_BUTTON_ID).onclick = this.takeSnapshot_.bind(this);
1712 },
1713
1714 takeSnapshot_: function() {
1715 // Start a new empty snapshot. Make note of the current time, so we know
1716 // when the snaphot was taken.
1717 this.snapshots_.push({flatData: [], time: getTimeMillis()});
1718
1719 // Update the UI to reflect the new snapshot.
1720 this.addSnapshotToList_(this.snapshots_.length - 1);
1721
1722 // Ask the browser for the profiling data. We will receive the data
1723 // later through a callback to addDataToSnapshot_().
1724 g_browserBridge.sendGetData();
1725 },
1726
1727 getSnapshotCheckbox_: function(i) {
1728 return $(this.getSnapshotCheckboxId_(i));
1729 },
1730
1731 getSnapshotCheckboxId_: function(i) {
1732 return 'snapshotCheckbox-' + i;
1733 },
1734
1735 addSnapshotToList_: function(i) {
1736 var tbody = $('snapshots-tbody');
1737
1738 var tr = addNode(tbody, 'tr');
1739
1740 var id = this.getSnapshotCheckboxId_(i);
1741
1742 var checkboxCell = addNode(tr, 'td');
1743 var checkbox = addNode(checkboxCell, 'input');
1744 checkbox.type = 'checkbox';
1745 checkbox.id = id;
1746 checkbox.__index = i;
1747 checkbox.onclick = this.onSnapshotCheckboxChanged_.bind(this);
1748
1749 addNode(tr, 'td', '#' + i);
1750
1751 var labelCell = addNode(tr, 'td');
1752 var l = addNode(labelCell, 'label');
1753
1754 var dateString = new Date(this.snapshots_[i].time).toLocaleString();
1755 addText(l, dateString);
1756 l.htmlFor = id;
1757
1758 // If we are on snapshot 0, make it the default.
1759 if (i == 0) {
1760 checkbox.checked = true;
1761 checkbox.__time = getTimeMillis();
1762 this.updateSnapshotCheckboxStyling_();
1763 }
1764 },
1765
1766 updateSnapshotCheckboxStyling_: function() {
1767 for (var i = 0; i < this.snapshots_.length; ++i) {
1768 var checkbox = this.getSnapshotCheckbox_(i);
1769 checkbox.parentNode.parentNode.className =
1770 checkbox.checked ? 'selected_snapshot' : '';
1771 }
1772 },
1773
1774 onSnapshotCheckboxChanged_: function(event) {
1775 // Keep track of when we clicked this box (for when we need to uncheck
1776 // older boxes).
1777 event.target.__time = getTimeMillis();
1778
1779 // Find all the checked boxes. Either 1 or 2 can be checked. If a third
1780 // was just checked, then uncheck one of the earlier ones so we only have
1781 // 2.
1782 var checked = this.getSelectedSnapshotBoxes_();
1783 checked.sort(function(a, b) { return b.__time - a.__time; });
1784 if (checked.length > 2) {
1785 for (var i = 2; i < checked.length; ++i)
1786 checked[i].checked = false;
1787 checked.length = 2;
1788 }
1789
1790 // We should always have at least 1 selection. Prevent the user from
1791 // unselecting the final box.
1792 if (checked.length == 0)
1793 event.target.checked = true;
1794
1795 this.updateSnapshotCheckboxStyling_();
1796
1797 // Recompute flatData_ (since it is derived from selected snapshots).
1798 this.updateFlatData_();
1799 },
1800
1801 fillSelectionCheckboxes_: function(parent) {
1802 this.selectionCheckboxes_ = {};
1803
1804 var onChangeFunc = this.onSelectCheckboxChanged_.bind(this);
1805
1806 for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) {
1807 var key = ALL_TABLE_COLUMNS[i];
1808 var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1809 checkbox.checked = true;
1810 checkbox.onchange = onChangeFunc;
1811 addText(parent, ' ');
1812 this.selectionCheckboxes_[key] = checkbox;
1813 }
1814
1815 for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
1816 this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
1817 }
1818 },
1819
1820 getSelectionColumns_: function() {
1821 return getKeysForCheckedBoxes(this.selectionCheckboxes_);
1822 },
1823
1824 getMergeColumns_: function() {
1825 return getKeysForCheckedBoxes(this.mergeCheckboxes_);
1826 },
1827
1828 shouldMergeSimilarThreads_: function() {
1829 return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
1830 },
1831
1832 fillMergeCheckboxes_: function(parent) {
1833 this.mergeCheckboxes_ = {};
1834
1835 var onChangeFunc = this.onMergeCheckboxChanged_.bind(this);
1836
1837 for (var i = 0; i < MERGEABLE_KEYS.length; ++i) {
1838 var key = MERGEABLE_KEYS[i];
1839 var checkbox = addLabeledCheckbox(parent, getNameForKey(key));
1840 checkbox.onchange = onChangeFunc;
1841 addText(parent, ' ');
1842 this.mergeCheckboxes_[key] = checkbox;
1843 }
1844
1845 for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
1846 this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
1847 }
1848 },
1849
1850 fillGroupingDropdowns_: function() {
1851 var parent = $(GROUP_BY_CONTAINER_ID);
1852 parent.innerHTML = '';
1853
1854 for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) {
1855 // Add a dropdown.
1856 var select = addNode(parent, 'select');
1857 select.onchange = this.onChangedGrouping_.bind(this, select, i);
1858
1859 addOptionsForGroupingSelect(select);
1860
1861 if (i < this.currentGroupingKeys_.length) {
1862 var key = this.currentGroupingKeys_[i];
1863 setSelectedOptionByValue(select, key);
1864 }
1865 }
1866 },
1867
1868 fillSortingDropdowns_: function() {
1869 var parent = $(SORT_BY_CONTAINER_ID);
1870 parent.innerHTML = '';
1871
1872 for (var i = 0; i <= this.currentSortKeys_.length; ++i) {
1873 // Add a dropdown.
1874 var select = addNode(parent, 'select');
1875 select.onchange = this.onChangedSorting_.bind(this, select, i);
1876
1877 addOptionsForSortingSelect(select);
1878
1879 if (i < this.currentSortKeys_.length) {
1880 var key = this.currentSortKeys_[i];
1881 setSelectedOptionByValue(select, key);
1882 }
1883 }
1884 },
1885
1886 onChangedGrouping_: function(select, i) {
1887 updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
1888 this.fillGroupingDropdowns_();
1889 this.updateGroupedData_();
1890 },
1891
1892 onChangedSorting_: function(select, i) {
1893 updateKeyListFromDropdown(this.currentSortKeys_, i, select);
1894 this.fillSortingDropdowns_();
1895 this.sortGroupedData_();
1896 },
1897
1898 onSelectCheckboxChanged_: function() {
1899 this.redrawData_();
1900 },
1901
1902 onMergeCheckboxChanged_: function() {
1903 this.updateMergedData_();
1904 },
1905
1906 onMergeSimilarThreadsCheckboxChanged_: function() {
1907 this.updateMergedData_();
1908 },
1909
1910 onChangedFilter_: function() {
1911 this.updateFilteredData_();
1912 },
1913
1914 /**
1915 * When left-clicking a column, change the primary sort order to that
1916 * column. If we were already sorted on that column then reverse the order.
1917 *
1918 * When alt-clicking, add a secondary sort column. Similarly, if
1919 * alt-clicking a column which was already being sorted on, reverse its
1920 * order.
1921 */
1922 onClickColumn_: function(key, event) {
1923 // If this property wants to start off in descending order rather then
1924 // ascending, flip it.
1925 if (KEY_PROPERTIES[key].sortDescending)
1926 key = reverseSortKey(key);
1927
1928 // Scan through our sort order and see if we are already sorted on this
1929 // key. If so, reverse that sort ordering.
1930 var found_i = -1;
1931 for (var i = 0; i < this.currentSortKeys_.length; ++i) {
1932 var curKey = this.currentSortKeys_[i];
1933 if (sortKeysMatch(curKey, key)) {
1934 this.currentSortKeys_[i] = reverseSortKey(curKey);
1935 found_i = i;
1936 break;
1937 }
1938 }
1939
1940 if (event.altKey) {
1941 if (found_i == -1) {
1942 // If we weren't already sorted on the column that was alt-clicked,
1943 // then add it to our sort.
1944 this.currentSortKeys_.push(key);
1945 }
1946 } else {
1947 if (found_i != 0 ||
1948 !sortKeysMatch(this.currentSortKeys_[found_i], key)) {
1949 // If the column we left-clicked wasn't already our primary column,
1950 // make it so.
1951 this.currentSortKeys_ = [key];
1952 } else {
1953 // If the column we left-clicked was already our primary column (and
1954 // we just reversed it), remove any secondary sorts.
1955 this.currentSortKeys_.length = 1;
1956 }
1957 }
1958
1959 this.fillSortingDropdowns_();
1960 this.sortGroupedData_();
1961 },
1962
1963 getSortingFunction_: function() {
1964 var sortKeys = this.currentSortKeys_.slice(0);
1965
1966 // Eliminate the empty string keys (which means they were unspecified).
1967 deleteValuesFromArray(sortKeys, ['']);
1968
1969 // If no sort is specified, use our default sort.
1970 if (sortKeys.length == 0)
1971 sortKeys = [DEFAULT_SORT_KEYS];
1972
1973 return function(a, b) {
1974 for (var i = 0; i < sortKeys.length; ++i) {
1975 var key = Math.abs(sortKeys[i]);
1976 var factor = sortKeys[i] < 0 ? -1 : 1;
1977
1978 var propA = a[key];
1979 var propB = b[key];
1980
1981 var comparison = compareValuesForKey(key, propA, propB);
1982 comparison *= factor; // Possibly reverse the ordering.
1983
1984 if (comparison != 0)
1985 return comparison;
1986 }
1987
1988 // Tie breaker.
1989 return simpleCompare(JSON.stringify(a), JSON.stringify(b));
1990 };
1991 },
1992
1993 getGroupSortingFunction_: function() {
1994 return function(a, b) {
1995 var groupKey1 = JSON.parse(a);
1996 var groupKey2 = JSON.parse(b);
1997
1998 for (var i = 0; i < groupKey1.length; ++i) {
1999 var comparison = compareValuesForKey(
2000 groupKey1[i].key,
2001 groupKey1[i].value,
2002 groupKey2[i].value);
2003
2004 if (comparison != 0)
2005 return comparison;
2006 }
2007
2008 // Tie breaker.
2009 return simpleCompare(a, b);
2010 };
2011 },
2012
2013 getFilterFunction_: function() {
2014 var searchStr = $(FILTER_SEARCH_ID).value;
2015
2016 // Normalize the search expression.
2017 searchStr = trimWhitespace(searchStr);
2018 searchStr = searchStr.toLowerCase();
2019
2020 return function(x) {
2021 // Match everything when there was no filter.
2022 if (searchStr == '')
2023 return true;
2024
2025 // Treat the search text as a LOWERCASE substring search.
2026 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
2027 var propertyText = getTextValueForProperty(k, x[k]);
2028 if (propertyText.toLowerCase().indexOf(searchStr) != -1)
2029 return true;
2030 }
2031
2032 return false;
2033 };
2034 },
2035
2036 getGroupingFunction_: function() {
2037 var groupings = this.currentGroupingKeys_.slice(0);
2038
2039 // Eliminate the empty string groupings (which means they were
2040 // unspecified).
2041 deleteValuesFromArray(groupings, ['']);
2042
2043 // Eliminate duplicate primary/secondary group by directives, since they
2044 // are redundant.
2045 deleteDuplicateStringsFromArray(groupings);
2046
2047 return function(e) {
2048 var groupKey = [];
2049
2050 for (var i = 0; i < groupings.length; ++i) {
2051 var entry = {key: groupings[i],
2052 value: e[groupings[i]]};
2053 groupKey.push(entry);
2054 }
2055
2056 return JSON.stringify(groupKey);
2057 };
2058 },
2059 };
2060
2061 return MainView;
2062 })();
OLDNEW
« no previous file with comments | « chrome/browser/resources/profiler.html ('k') | chrome/browser/resources/profiler/OWNERS » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698