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

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

Issue 8520016: Complete the rename from about:tracking2 --> about:profiler by changing the implementation code t... (Closed) Base URL: svn://chrome-svn/chrome/trunk/src/
Patch Set: try synching again... Created 9 years, 1 month 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
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): Don't repeat the work of grouping, sorting, merging on every
9 // redraw. Rather do it only once when one of its dependencies
10 // change and cache the result.
11
12 /**
13 * Main entry point called once the page has loaded.
14 */
15 function onLoad() {
16 g_browserBridge = new BrowserBridge();
17 g_mainView = new MainView();
18
19 // Ask the browser to send us the current data.
20 g_browserBridge.sendGetData();
21 }
22
23 document.addEventListener('DOMContentLoaded', onLoad);
24
25 /**
26 * This class provides a "bridge" for communicating between the javascript and
27 * the browser. Used as a singleton.
28 */
29 var BrowserBridge = (function() {
30 'use strict';
31
32 /**
33 * @constructor
34 */
35 function BrowserBridge() {
36 }
37
38 BrowserBridge.prototype = {
39 //--------------------------------------------------------------------------
40 // Messages sent to the browser
41 //--------------------------------------------------------------------------
42
43 sendGetData: function() {
44 chrome.send('getData');
45 },
46
47 sendResetData: function() {
48 chrome.send('resetData');
49 },
50
51 //--------------------------------------------------------------------------
52 // Messages received from the browser.
53 //--------------------------------------------------------------------------
54
55 receivedData: function(data) {
56 g_mainView.addData(data);
57 },
58 };
59
60 return BrowserBridge;
61 })();
62
63 /**
64 * This class handles the presentation of our tracking 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 // --------------------------------------------------------------------------
103 // Row keys
104 // --------------------------------------------------------------------------
105
106 // Each row of our data is an array of values rather than a dictionary. This
107 // avoids some overhead from repeating the key string multiple times, and
108 // speeds up the property accesses a bit. The following keys are well-known
109 // indexes into the array for various properties.
110 //
111 // Note that the declaration order will also define the default display order.
112
113 var BEGIN_KEY = 1; // Start at 1 rather than 0 to simplify sorting code.
114 var END_KEY = BEGIN_KEY;
115
116 var KEY_COUNT = END_KEY++;
117 var KEY_RUN_TIME = END_KEY++;
118 var KEY_AVG_RUN_TIME = END_KEY++;
119 var KEY_MAX_RUN_TIME = END_KEY++;
120 var KEY_QUEUE_TIME = END_KEY++;
121 var KEY_AVG_QUEUE_TIME = END_KEY++;
122 var KEY_MAX_QUEUE_TIME = END_KEY++;
123 var KEY_BIRTH_THREAD = END_KEY++;
124 var KEY_DEATH_THREAD = END_KEY++;
125 var KEY_PROCESS_TYPE = END_KEY++;
126 var KEY_PROCESS_ID = END_KEY++;
127 var KEY_FUNCTION_NAME = END_KEY++;
128 var KEY_SOURCE_LOCATION = END_KEY++;
129 var KEY_FILE_NAME = END_KEY++;
130 var KEY_LINE_NUMBER = END_KEY++;
131
132 var NUM_KEYS = END_KEY - BEGIN_KEY;
133
134 // --------------------------------------------------------------------------
135 // Aggregators
136 // --------------------------------------------------------------------------
137
138 // To generalize computing/displaying the aggregate "counts" for each column,
139 // we specify an optional "Aggregator" class to use with each property.
140
141 // The following are actually "Aggregator factories". They create an
142 // aggregator instance by calling 'create()'. The instance is then fed
143 // each row one at a time via the 'consume()' method. After all rows have
144 // been consumed, the 'getValueAsText()' method will return the aggregated
145 // value.
146
147 /**
148 * This aggregator counts the number of unique values that were fed to it.
149 */
150 var UniquifyAggregator = (function() {
151 function Aggregator(key) {
152 this.key_ = key;
153 this.valuesSet_ = {};
154 }
155
156 Aggregator.prototype = {
157 consume: function(e) {
158 this.valuesSet_[e[this.key_]] = true;
159 },
160
161 getValueAsText: function() {
162 return getDictionaryKeys(this.valuesSet_).length + ' unique'
163 },
164 };
165
166 return {
167 create: function(key) { return new Aggregator(key); }
168 };
169 })();
170
171 /**
172 * This aggregator sums a numeric field.
173 */
174 var SumAggregator = (function() {
175 function Aggregator(key) {
176 this.key_ = key;
177 this.sum_ = 0;
178 }
179
180 Aggregator.prototype = {
181 consume: function(e) {
182 this.sum_ += e[this.key_];
183 },
184
185 getValue: function() {
186 return this.sum_;
187 },
188
189 getValueAsText: function() {
190 return formatNumberAsText(this.getValue());
191 },
192 };
193
194 return {
195 create: function(key) { return new Aggregator(key); }
196 };
197 })();
198
199 /**
200 * This aggregator computes an average by summing two
201 * numeric fields, and then dividing the totals.
202 */
203 var AvgAggregator = (function() {
204 function Aggregator(numeratorKey, divisorKey) {
205 this.numeratorKey_ = numeratorKey;
206 this.divisorKey_ = divisorKey;
207
208 this.numeratorSum_ = 0;
209 this.divisorSum_ = 0;
210 }
211
212 Aggregator.prototype = {
213 consume: function(e) {
214 this.numeratorSum_ += e[this.numeratorKey_];
215 this.divisorSum_ += e[this.divisorKey_];
216 },
217
218 getValue: function() {
219 return this.numeratorSum_ / this.divisorSum_;
220 },
221
222 getValueAsText: function() {
223 return formatNumberAsText(this.getValue());
224 },
225 };
226
227 return {
228 create: function(numeratorKey, divisorKey) {
229 return {
230 create: function(key) {
231 return new Aggregator(numeratorKey, divisorKey);
232 },
233 }
234 }
235 };
236 })();
237
238 /**
239 * This aggregator finds the maximum for a numeric field.
240 */
241 var MaxAggregator = (function() {
242 function Aggregator(key) {
243 this.key_ = key;
244 this.max_ = -Infinity;
245 }
246
247 Aggregator.prototype = {
248 consume: function(e) {
249 this.max_ = Math.max(this.max_, e[this.key_]);
250 },
251
252 getValue: function() {
253 return this.max_;
254 },
255
256 getValueAsText: function() {
257 return formatNumberAsText(this.getValue());
258 },
259 };
260
261 return {
262 create: function(key) { return new Aggregator(key); }
263 };
264 })();
265
266 // --------------------------------------------------------------------------
267 // Key properties
268 // --------------------------------------------------------------------------
269
270 // Custom comparator for thread names (sorts main thread and IO thread
271 // higher than would happen lexicographically.)
272 var threadNameComparator =
273 createLexicographicComparatorWithExceptions([
274 'CrBrowserMain',
275 'Chrome_IOThread',
276 'Chrome_FileThread',
277 'Chrome_HistoryThread',
278 'Chrome_DBThread',
279 'Still_Alive',
280 ]);
281
282 /**
283 * Enumerates information about various keys. Such as whether their data is
284 * expected to be numeric or is a string, a descriptive name (title) for the
285 * property, and what function should be used to aggregate the property when
286 * displayed in a column.
287 *
288 * --------------------------------------
289 * The following properties are required:
290 * --------------------------------------
291 *
292 * [name]: This is displayed as the column's label.
293 * [aggregator]: Aggregator factory that is used to compute an aggregate
294 * value for this column.
295 *
296 * --------------------------------------
297 * The following properties are optional:
298 * --------------------------------------
299 *
300 * [inputJsonKey]: The corresponding key for this property in the original
301 * JSON dictionary received from the browser. If this is
302 * present, values for this key will be automatically
303 * populated during import.
304 * [comparator]: A comparator function for sorting this column.
305 * [textPrinter]: A function that transforms values into the user-displayed
306 * text shown in the UI. If unspecified, will default to the
307 * "toString()" function.
308 * [cellAlignment]: The horizonal alignment to use for columns of this
309 * property (for instance 'right'). If unspecified will
310 * default to left alignment.
311 * [sortDescending]: When first clicking on this column, we will default to
312 * sorting by |comparator| in ascending order. If this
313 * property is true, we will reverse that to descending.
314 */
315 var KEY_PROPERTIES = [];
316
317 KEY_PROPERTIES[KEY_PROCESS_ID] = {
318 name: 'PID',
319 cellAlignment: 'right',
320 aggregator: UniquifyAggregator,
321 };
322
323 KEY_PROPERTIES[KEY_PROCESS_TYPE] = {
324 name: 'Process type',
325 aggregator: UniquifyAggregator,
326 };
327
328 KEY_PROPERTIES[KEY_BIRTH_THREAD] = {
329 name: 'Birth thread',
330 inputJsonKey: 'birth_thread',
331 aggregator: UniquifyAggregator,
332 comparator: threadNameComparator,
333 };
334
335 KEY_PROPERTIES[KEY_DEATH_THREAD] = {
336 name: 'Exec thread',
337 inputJsonKey: 'death_thread',
338 aggregator: UniquifyAggregator,
339 comparator: threadNameComparator,
340 };
341
342 KEY_PROPERTIES[KEY_FUNCTION_NAME] = {
343 name: 'Function name',
344 inputJsonKey: 'location.function_name',
345 aggregator: UniquifyAggregator,
346 };
347
348 KEY_PROPERTIES[KEY_FILE_NAME] = {
349 name: 'File name',
350 inputJsonKey: 'location.file_name',
351 aggregator: UniquifyAggregator,
352 };
353
354 KEY_PROPERTIES[KEY_LINE_NUMBER] = {
355 name: 'Line number',
356 cellAlignment: 'right',
357 inputJsonKey: 'location.line_number',
358 aggregator: UniquifyAggregator,
359 };
360
361 KEY_PROPERTIES[KEY_COUNT] = {
362 name: 'Count',
363 cellAlignment: 'right',
364 sortDescending: true,
365 textPrinter: formatNumberAsText,
366 inputJsonKey: 'death_data.count',
367 aggregator: SumAggregator,
368 };
369
370 KEY_PROPERTIES[KEY_QUEUE_TIME] = {
371 name: 'Total queue time',
372 cellAlignment: 'right',
373 sortDescending: true,
374 textPrinter: formatNumberAsText,
375 inputJsonKey: 'death_data.queue_ms',
376 aggregator: SumAggregator,
377 };
378
379 KEY_PROPERTIES[KEY_MAX_QUEUE_TIME] = {
380 name: 'Max queue time',
381 cellAlignment: 'right',
382 sortDescending: true,
383 textPrinter: formatNumberAsText,
384 inputJsonKey: 'death_data.queue_ms_max',
385 aggregator: MaxAggregator,
386 };
387
388 KEY_PROPERTIES[KEY_RUN_TIME] = {
389 name: 'Total run time',
390 cellAlignment: 'right',
391 sortDescending: true,
392 textPrinter: formatNumberAsText,
393 inputJsonKey: 'death_data.run_ms',
394 aggregator: SumAggregator,
395 };
396
397 KEY_PROPERTIES[KEY_AVG_RUN_TIME] = {
398 name: 'Avg run time',
399 cellAlignment: 'right',
400 sortDescending: true,
401 textPrinter: formatNumberAsText,
402 aggregator: AvgAggregator.create(KEY_RUN_TIME, KEY_COUNT),
403 };
404
405 KEY_PROPERTIES[KEY_MAX_RUN_TIME] = {
406 name: 'Max run time',
407 cellAlignment: 'right',
408 sortDescending: true,
409 textPrinter: formatNumberAsText,
410 inputJsonKey: 'death_data.run_ms_max',
411 aggregator: MaxAggregator,
412 };
413
414 KEY_PROPERTIES[KEY_AVG_QUEUE_TIME] = {
415 name: 'Avg queue time',
416 cellAlignment: 'right',
417 sortDescending: true,
418 textPrinter: formatNumberAsText,
419 aggregator: AvgAggregator.create(KEY_QUEUE_TIME, KEY_COUNT),
420 };
421
422 KEY_PROPERTIES[KEY_SOURCE_LOCATION] = {
423 name: 'Source location',
424 type: 'string',
425 aggregator: UniquifyAggregator,
426 };
427
428 /**
429 * Returns the string name for |key|.
430 */
431 function getNameForKey(key) {
432 var props = KEY_PROPERTIES[key];
433 if (props == undefined)
434 throw 'Did not define properties for key: ' + key;
435 return props.name;
436 }
437
438 /**
439 * Ordered list of all keys. This is the order we generally want
440 * to display the properties in. Default to declaration order.
441 */
442 var ALL_KEYS = [];
443 for (var k = BEGIN_KEY; k < END_KEY; ++k)
444 ALL_KEYS.push(k);
445
446 // --------------------------------------------------------------------------
447 // Default settings
448 // --------------------------------------------------------------------------
449
450 /**
451 * List of keys for those properties which we want to initially omit
452 * from the table. (They can be re-enabled by clicking [Edit columns]).
453 */
454 var INITIALLY_HIDDEN_KEYS = [
455 KEY_FILE_NAME,
456 KEY_LINE_NUMBER,
457 KEY_QUEUE_TIME,
458 ];
459
460 /**
461 * The ordered list of grouping choices to expose in the "Group by"
462 * dropdowns. We don't include the numeric properties, since they
463 * leads to awkward bucketing.
464 */
465 var GROUPING_DROPDOWN_CHOICES = [
466 KEY_PROCESS_TYPE,
467 KEY_PROCESS_ID,
468 KEY_BIRTH_THREAD,
469 KEY_DEATH_THREAD,
470 KEY_FUNCTION_NAME,
471 KEY_SOURCE_LOCATION,
472 KEY_FILE_NAME,
473 KEY_LINE_NUMBER,
474 ];
475
476 /**
477 * The ordered list of sorting choices to expose in the "Sort by"
478 * dropdowns.
479 */
480 var SORT_DROPDOWN_CHOICES = ALL_KEYS;
481
482 /**
483 * The ordered list of all columns that can be displayed in the tables (not
484 * including whatever has been hidden via [Edit Columns]).
485 */
486 var ALL_TABLE_COLUMNS = ALL_KEYS;
487
488 /**
489 * The initial keys to sort by when loading the page (can be changed later).
490 */
491 var INITIAL_SORT_KEYS = [-KEY_COUNT];
492
493 /**
494 * The default sort keys to use when nothing has been specified.
495 */
496 var DEFAULT_SORT_KEYS = [-KEY_COUNT];
497
498 /**
499 * The initial keys to group by when loading the page (can be changed later).
500 */
501 var INITIAL_GROUP_KEYS = [];
502
503 /**
504 * The columns to give the option to merge on.
505 */
506 var MERGEABLE_KEYS = [
507 KEY_PROCESS_ID,
508 KEY_PROCESS_TYPE,
509 KEY_BIRTH_THREAD,
510 KEY_DEATH_THREAD,
511 ];
512
513 /**
514 * The columns to merge by default.
515 */
516 var INITIALLY_MERGED_KEYS = [];
517
518 /**
519 * The full set of columns which define the "identity" for a row. A row is
520 * considered equivalent to another row if it matches on all of these
521 * fields. This list is used when merging the data, to determine which rows
522 * should be merged together. The remaining columns not listed in
523 * IDENTITY_KEYS will be aggregated.
524 */
525 var IDENTITY_KEYS = [
526 KEY_BIRTH_THREAD,
527 KEY_DEATH_THREAD,
528 KEY_PROCESS_TYPE,
529 KEY_PROCESS_ID,
530 KEY_FUNCTION_NAME,
531 KEY_SOURCE_LOCATION,
532 KEY_FILE_NAME,
533 KEY_LINE_NUMBER,
534 ];
535
536 // --------------------------------------------------------------------------
537 // General utility functions
538 // --------------------------------------------------------------------------
539
540 /**
541 * Returns a list of all the keys in |dict|.
542 */
543 function getDictionaryKeys(dict) {
544 var keys = [];
545 for (var key in dict) {
546 keys.push(key);
547 }
548 return keys;
549 }
550
551 /**
552 * Formats the number |x| as a decimal integer. Strips off any decimal parts,
553 * and comma separates the number every 3 characters.
554 */
555 function formatNumberAsText(x) {
556 var orig = x.toFixed(0);
557
558 var parts = [];
559 for (var end = orig.length; end > 0; ) {
560 var chunk = Math.min(end, 3);
561 parts.push(orig.substr(end-chunk, chunk));
562 end -= chunk;
563 }
564 return parts.reverse().join(',');
565 }
566
567 /**
568 * Simple comparator function which works for both strings and numbers.
569 */
570 function simpleCompare(a, b) {
571 if (a == b)
572 return 0;
573 if (a < b)
574 return -1;
575 return 1;
576 }
577
578 /**
579 * Returns a comparator function that compares values lexicographically,
580 * but special-cases the values in |orderedList| to have a higher
581 * rank.
582 */
583 function createLexicographicComparatorWithExceptions(orderedList) {
584 var valueToRankMap = {};
585 for (var i = 0; i < orderedList.length; ++i)
586 valueToRankMap[orderedList[i]] = i;
587
588 function getCustomRank(x) {
589 var rank = valueToRankMap[x];
590 if (rank == undefined)
591 rank = Infinity; // Unmatched.
592 return rank;
593 }
594
595 return function(a, b) {
596 var aRank = getCustomRank(a);
597 var bRank = getCustomRank(b);
598
599 // Not matched by any of our exceptions.
600 if (aRank == bRank)
601 return simpleCompare(a, b);
602
603 if (aRank < bRank)
604 return -1;
605 return 1;
606 };
607 }
608
609 /**
610 * Returns dict[key]. Note that if |key| contains periods (.), they will be
611 * interpreted as meaning a sub-property.
612 */
613 function getPropertyByPath(dict, key) {
614 var cur = dict;
615 var parts = key.split('.');
616 for (var i = 0; i < parts.length; ++i) {
617 if (cur == undefined)
618 return undefined;
619 cur = cur[parts[i]];
620 }
621 return cur;
622 }
623
624 /**
625 * Creates and appends a DOM node of type |tagName| to |parent|. Optionally,
626 * sets the new node's text to |opt_text|. Returns the newly created node.
627 */
628 function addNode(parent, tagName, opt_text) {
629 var n = parent.ownerDocument.createElement(tagName);
630 parent.appendChild(n);
631 if (opt_text != undefined) {
632 addText(n, opt_text);
633 }
634 return n;
635 }
636
637 /**
638 * Adds |text| to |parent|.
639 */
640 function addText(parent, text) {
641 var textNode = parent.ownerDocument.createTextNode(text);
642 parent.appendChild(textNode);
643 return textNode;
644 }
645
646 /**
647 * Deletes all the strings in |array| which appear in |valuesToDelete|.
648 */
649 function deleteValuesFromArray(array, valuesToDelete) {
650 var valueSet = arrayToSet(valuesToDelete);
651 for (var i = 0; i < array.length; ) {
652 if (valueSet[array[i]]) {
653 array.splice(i, 1);
654 } else {
655 i++;
656 }
657 }
658 }
659
660 /**
661 * Deletes all the repeated ocurrences of strings in |array|.
662 */
663 function deleteDuplicateStringsFromArray(array) {
664 // Build up set of each entry in array.
665 var seenSoFar = {};
666
667 for (var i = 0; i < array.length; ) {
668 var value = array[i];
669 if (seenSoFar[value]) {
670 array.splice(i, 1);
671 } else {
672 seenSoFar[value] = true;
673 i++;
674 }
675 }
676 }
677
678 /**
679 * Builds a map out of the array |list|.
680 */
681 function arrayToSet(list) {
682 var set = {};
683 for (var i = 0; i < list.length; ++i)
684 set[list[i]] = true;
685 return set;
686 }
687
688 function trimWhitespace(text) {
689 var m = /^\s*(.*)\s*$/.exec(text);
690 return m[1];
691 }
692
693 /**
694 * Selects the option in |select| which has a value of |value|.
695 */
696 function setSelectedOptionByValue(select, value) {
697 for (var i = 0; i < select.options.length; ++i) {
698 if (select.options[i].value == value) {
699 select.options[i].selected = true;
700 return true;
701 }
702 }
703 return false;
704 }
705
706 /**
707 * Return the last component in a path which is separated by either forward
708 * slashes or backslashes.
709 */
710 function getFilenameFromPath(path) {
711 var lastSlash = Math.max(path.lastIndexOf('/'),
712 path.lastIndexOf('\\'));
713 if (lastSlash == -1)
714 return path;
715
716 return path.substr(lastSlash + 1);
717 }
718
719 // --------------------------------------------------------------------------
720 // Functions that augment, bucket, and compute aggregates for the input data.
721 // --------------------------------------------------------------------------
722
723 /**
724 * Selects all the data in |rows| which are matched by |filterFunc|, and
725 * buckets the results using |entryToGroupKeyFunc|. For each bucket aggregates
726 * are computed, and the results are sorted.
727 *
728 * Returns a dictionary whose keys are the group name, and the value is an
729 * objected containing two properties: |rows| and |aggregates|.
730 */
731 function prepareData(rows, entryToGroupKeyFunc, filterFunc, sortingFunc) {
732 var groupedData = {};
733
734 for (var i = 0; i < rows.length; ++i) {
735 var e = rows[i];
736
737 if (!filterFunc(e))
738 continue; // Not matched by our filter, discard the row.
739
740 var groupKey = entryToGroupKeyFunc(e);
741
742 var groupData = groupedData[groupKey];
743 if (!groupData) {
744 groupData = {
745 aggregates: initializeAggregates(ALL_KEYS),
746 rows: [],
747 };
748 groupedData[groupKey] = groupData;
749 }
750
751 // Add the row to our list.
752 groupData.rows.push(e);
753
754 // Update aggregates for each column.
755 consumeAggregates(groupData.aggregates, e);
756 }
757
758 // Sort all the data.
759 for (var groupKey in groupedData)
760 groupedData[groupKey].rows.sort(sortingFunc);
761
762 return groupedData;
763 }
764
765 /**
766 * Adds new derived properties to row. Mutates the provided dictionary |e|.
767 */
768 function augmentDataRow(e) {
769 e[KEY_AVG_QUEUE_TIME] = e[KEY_QUEUE_TIME] / e[KEY_COUNT];
770 e[KEY_AVG_RUN_TIME] = e[KEY_RUN_TIME] / e[KEY_COUNT];
771 e[KEY_SOURCE_LOCATION] = e[KEY_FILE_NAME] + ' [' + e[KEY_LINE_NUMBER] + ']';
772 }
773
774 /**
775 * Creates and initializes an aggregator object for each key in |columns|.
776 * Returns an array whose keys are values from |columns|, and whose
777 * values are Aggregator instances.
778 */
779 function initializeAggregates(columns) {
780 var aggregates = [];
781
782 for (var i = 0; i < columns.length; ++i) {
783 var key = columns[i];
784 var aggregatorFactory = KEY_PROPERTIES[key].aggregator;
785 aggregates[key] = aggregatorFactory.create(key);
786 }
787
788 return aggregates;
789 }
790
791 function consumeAggregates(aggregates, row) {
792 for (var key in aggregates)
793 aggregates[key].consume(row);
794 }
795
796 /**
797 * Merges the rows in |origRows|, by collapsing the columns listed in
798 * |mergeKeys|. Returns an array with the merged rows (in no particular
799 * order).
800 *
801 * If |mergeSimilarThreads| is true, then threads with a similar name will be
802 * considered equivalent. For instance, "WorkerThread-1" and "WorkerThread-2"
803 * will be remapped to "WorkerThread-*".
804 */
805 function mergeRows(origRows, mergeKeys, mergeSimilarThreads) {
806 // Define a translation function for each property. Normally we copy over
807 // properties as-is, but if we have been asked to "merge similar threads" we
808 // we will remap the thread names that end in a numeric suffix.
809 var propertyGetterFunc;
810
811 if (mergeSimilarThreads) {
812 propertyGetterFunc = function(row, key) {
813 var value = row[key];
814 // If the property is a thread name, try to remap it.
815 if (key == KEY_BIRTH_THREAD || key == KEY_DEATH_THREAD) {
816 var m = /^(.*)(\d+)$/.exec(value);
817 if (m)
818 value = m[1] + '*';
819 }
820 return value;
821 }
822 } else {
823 propertyGetterFunc = function(row, key) { return row[key]; };
824 }
825
826 // Determine which sets of properties a row needs to match on to be
827 // considered identical to another row.
828 var identityKeys = IDENTITY_KEYS.slice(0);
829 deleteValuesFromArray(identityKeys, mergeKeys);
830
831 // Set |aggregateKeys| to everything else, since we will be aggregating
832 // their value as part of the merge.
833 var aggregateKeys = ALL_KEYS.slice(0);
834 deleteValuesFromArray(aggregateKeys, IDENTITY_KEYS);
835
836 // Group all the identical rows together, bucketed into |identicalRows|.
837 var identicalRows = {};
838 for (var i = 0; i < origRows.length; ++i) {
839 var e = origRows[i];
840
841 var rowIdentity = [];
842 for (var j = 0; j < identityKeys.length; ++j)
843 rowIdentity.push(propertyGetterFunc(e, identityKeys[j]));
844 rowIdentity = rowIdentity.join('\n');
845
846 var l = identicalRows[rowIdentity];
847 if (!l) {
848 l = [];
849 identicalRows[rowIdentity] = l;
850 }
851 l.push(e);
852 }
853
854 var mergedRows = [];
855
856 // Merge the rows and save the results to |mergedRows|.
857 for (var k in identicalRows) {
858 // We need to smash the list |l| down to a single row...
859 var l = identicalRows[k];
860
861 var newRow = [];
862 mergedRows.push(newRow);
863
864 // Copy over all the identity columns to the new row (since they
865 // were the same for each row matched).
866 for (var i = 0; i < identityKeys.length; ++i)
867 newRow[identityKeys[i]] = propertyGetterFunc(l[0], identityKeys[i]);
868
869 // Compute aggregates for the other columns.
870 var aggregates = initializeAggregates(aggregateKeys);
871
872 // Feed the rows to the aggregators.
873 for (var i = 0; i < l.length; ++i)
874 consumeAggregates(aggregates, l[i]);
875
876 // Suck out the data generated by the aggregators.
877 for (var aggregateKey in aggregates)
878 newRow[aggregateKey] = aggregates[aggregateKey].getValue();
879 }
880
881 return mergedRows;
882 }
883
884 // --------------------------------------------------------------------------
885 // HTML drawing code
886 // --------------------------------------------------------------------------
887
888 /**
889 * Draws a title into |parent| that describes |groupKey|.
890 */
891 function drawGroupTitle(parent, groupKey) {
892 if (groupKey.length == 0) {
893 // Empty group key means there was no grouping.
894 return;
895 }
896
897 var parent = addNode(parent, 'div');
898 parent.className = 'group-title-container';
899
900 // Each component of the group key represents the "key=value" constraint for
901 // this group. Show these as an AND separated list.
902 for (var i = 0; i < groupKey.length; ++i) {
903 if (i > 0)
904 addNode(parent, 'i', ' and ');
905 var e = groupKey[i];
906 addNode(parent, 'b', getNameForKey(e.key) + ' = ');
907 addNode(parent, 'span', e.value);
908 }
909 }
910
911 /**
912 * Renders the information for a particular group.
913 */
914 function drawGroup(parent, groupKey, groupData, columns,
915 columnOnClickHandler, currentSortKeys) {
916 var div = addNode(parent, 'div');
917 div.className = 'group-container';
918
919 drawGroupTitle(div, groupKey);
920
921 var table = addNode(div, 'table');
922
923 drawDataTable(table, groupData, columns, columnOnClickHandler,
924 currentSortKeys);
925 }
926
927 /**
928 * Renders a row that describes all the aggregate values for |columns|.
929 */
930 function drawAggregateRow(tbody, aggregates, columns) {
931 var tr = addNode(tbody, 'tr');
932 tr.className = 'aggregator-row';
933
934 for (var i = 0; i < columns.length; ++i) {
935 var key = columns[i];
936 var td = addNode(tr, 'td');
937
938 // Most of our outputs are numeric, so we want to align them to the right.
939 // However for the unique counts we will center.
940 if (KEY_PROPERTIES[key].aggregator == UniquifyAggregator) {
941 td.align = 'center';
942 } else {
943 td.align = 'right';
944 }
945
946 var aggregator = aggregates[key];
947 if (aggregator)
948 td.innerText = aggregator.getValueAsText();
949 }
950 }
951
952 /**
953 * Renders a table which summarizes all |column| fields for |data|.
954 */
955 function drawDataTable(table, data, columns, columnOnClickHandler,
956 currentSortKeys) {
957 table.className = 'results-table';
958 var thead = addNode(table, 'thead');
959 var tbody = addNode(table, 'tbody');
960
961 drawAggregateRow(thead, data.aggregates, columns);
962 drawTableHeader(thead, columns, columnOnClickHandler, currentSortKeys);
963 drawTableBody(tbody, data.rows, columns);
964 }
965
966 function drawTableHeader(thead, columns, columnOnClickHandler,
967 currentSortKeys) {
968 var tr = addNode(thead, 'tr');
969 for (var i = 0; i < columns.length; ++i) {
970 var key = columns[i];
971 var th = addNode(tr, 'th', getNameForKey(key));
972 th.onclick = columnOnClickHandler.bind(this, key);
973
974 // Draw an indicator if we are currently sorted on this column.
975 // TODO(eroman): Should use an icon instead of asterisk!
976 for (var j = 0; j < currentSortKeys.length; ++j) {
977 if (sortKeysMatch(currentSortKeys[j], key)) {
978 var sortIndicator = addNode(th, 'span', '*');
979 sortIndicator.style.color = 'red';
980 if (sortKeyIsReversed(currentSortKeys[j])) {
981 // Use double-asterisk for descending columns.
982 addText(sortIndicator, '*');
983 }
984 break;
985 }
986 }
987 }
988 }
989
990 function getTextValueForProperty(key, value) {
991 if (value == undefined) {
992 // A value may be undefined as a result of having merging rows. We
993 // won't actually draw it, but this might be called by the filter.
994 return '';
995 }
996
997 var textPrinter = KEY_PROPERTIES[key].textPrinter;
998 if (textPrinter)
999 return textPrinter(value);
1000 return value.toString();
1001 }
1002
1003 /**
1004 * Renders the property value |value| into cell |td|. The name of this
1005 * property is |key|.
1006 */
1007 function drawValueToCell(td, key, value) {
1008 // Get a text representation of the value.
1009 var text = getTextValueForProperty(key, value);
1010
1011 // Apply the desired cell alignment.
1012 var cellAlignment = KEY_PROPERTIES[key].cellAlignment;
1013 if (cellAlignment)
1014 td.align = cellAlignment;
1015
1016 if (key == KEY_SOURCE_LOCATION) {
1017 // Linkify the source column so it jumps to the source code. This doesn't
1018 // take into account the particular code this build was compiled from, or
1019 // local edits to source. It should however work correctly for top of tree
1020 // builds.
1021 var m = /^(.*) \[(\d+)\]$/.exec(text);
1022 if (m) {
1023 var filepath = m[1];
1024 var filename = getFilenameFromPath(filepath);
1025 var linenumber = m[2];
1026
1027 var link = addNode(td, 'a', filename + ' [' + linenumber + ']');
1028 // http://chromesrc.appspot.com is a server I wrote specifically for
1029 // this task. It redirects to the appropriate source file; the file
1030 // paths given by the compiler can be pretty crazy and different
1031 // between platforms.
1032 link.href = 'http://chromesrc.appspot.com/?path=' +
1033 encodeURIComponent(filepath) + '&line=' + linenumber;
1034 return;
1035 }
1036 }
1037
1038 // String values can get pretty long. If the string contains no spaces, then
1039 // CSS fails to wrap it, and it overflows the cell causing the table to get
1040 // really big. We solve this using a hack: insert a <wbr> element after
1041 // every single character. This will allow the rendering engine to wrap the
1042 // value, and hence avoid it overflowing!
1043 var kMinLengthBeforeWrap = 20;
1044
1045 addText(td, text.substr(0, kMinLengthBeforeWrap));
1046 for (var i = kMinLengthBeforeWrap; i < text.length; ++i) {
1047 addNode(td, 'wbr');
1048 addText(td, text.substr(i, 1));
1049 }
1050 }
1051
1052 function drawTableBody(tbody, rows, columns) {
1053 for (var i = 0; i < rows.length; ++i) {
1054 var e = rows[i];
1055
1056 var tr = addNode(tbody, 'tr');
1057
1058 for (var c = 0; c < columns.length; ++c) {
1059 var key = columns[c];
1060 var value = e[key];
1061
1062 var td = addNode(tr, 'td');
1063 drawValueToCell(td, key, value);
1064 }
1065 }
1066 }
1067
1068 // --------------------------------------------------------------------------
1069 // Helper code for handling the sort and grouping dropdowns.
1070 // --------------------------------------------------------------------------
1071
1072 function addOptionsForGroupingSelect(select) {
1073 // Add "no group" choice.
1074 addNode(select, 'option', '---').value = '';
1075
1076 for (var i = 0; i < GROUPING_DROPDOWN_CHOICES.length; ++i) {
1077 var key = GROUPING_DROPDOWN_CHOICES[i];
1078 var option = addNode(select, 'option', getNameForKey(key));
1079 option.value = key;
1080 }
1081 }
1082
1083 function addOptionsForSortingSelect(select) {
1084 // Add "no sort" choice.
1085 addNode(select, 'option', '---').value = '';
1086
1087 // Add a divider.
1088 addNode(select, 'optgroup').label = '';
1089
1090 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1091 var key = SORT_DROPDOWN_CHOICES[i];
1092 addNode(select, 'option', getNameForKey(key)).value = key;
1093 }
1094
1095 // Add a divider.
1096 addNode(select, 'optgroup').label = '';
1097
1098 // Add the same options, but for descending.
1099 for (var i = 0; i < SORT_DROPDOWN_CHOICES.length; ++i) {
1100 var key = SORT_DROPDOWN_CHOICES[i];
1101 var n = addNode(select, 'option', getNameForKey(key) + ' (DESC)');
1102 n.value = reverseSortKey(key);
1103 }
1104 }
1105
1106 /**
1107 * Helper function used to update the sorting and grouping lists after a
1108 * dropdown changes.
1109 */
1110 function updateKeyListFromDropdown(list, i, select) {
1111 // Update the list.
1112 if (i < list.length) {
1113 list[i] = select.value;
1114 } else {
1115 list.push(select.value);
1116 }
1117
1118 // Normalize the list, so setting 'none' as primary zeros out everything
1119 // else.
1120 for (var i = 0; i < list.length; ++i) {
1121 if (list[i] == '') {
1122 list.splice(i, list.length - i);
1123 break;
1124 }
1125 }
1126 }
1127
1128 /**
1129 * Comparator for property |key|, having values |value1| and |value2|.
1130 * If the key has defined a custom comparator use it. Otherwise use a
1131 * default "less than" comparison.
1132 */
1133 function compareValuesForKey(key, value1, value2) {
1134 var comparator = KEY_PROPERTIES[key].comparator;
1135 if (comparator)
1136 return comparator(value1, value2);
1137 return simpleCompare(value1, value2);
1138 }
1139
1140 function reverseSortKey(key) {
1141 return -key;
1142 }
1143
1144 function sortKeyIsReversed(key) {
1145 return key < 0;
1146 }
1147
1148 function sortKeysMatch(key1, key2) {
1149 return Math.abs(key1) == Math.abs(key2);
1150 }
1151
1152 function getKeysForCheckedBoxes(checkboxes) {
1153 var keys = [];
1154 for (var k in checkboxes) {
1155 if (checkboxes[k].checked)
1156 keys.push(k);
1157 }
1158 return keys;
1159 }
1160
1161 // --------------------------------------------------------------------------
1162
1163 /**
1164 * @constructor
1165 */
1166 function MainView() {
1167 // Make sure we have a definition for each key.
1168 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1169 if (!KEY_PROPERTIES[k])
1170 throw 'KEY_PROPERTIES[] not defined for key: ' + k;
1171 }
1172
1173 this.init_();
1174 }
1175
1176 MainView.prototype = {
1177 addData: function(data) {
1178 var pid = data.process_id;
1179 var ptype = data.process_type;
1180
1181 // Augment each data row with the process information.
1182 var rows = data.list;
1183 for (var i = 0; i < rows.length; ++i) {
1184 // Transform the data from a dictionary to an array. This internal
1185 // representation is more compact and faster to access.
1186 var origRow = rows[i];
1187 var newRow = [];
1188
1189 newRow[KEY_PROCESS_ID] = pid;
1190 newRow[KEY_PROCESS_TYPE] = ptype;
1191
1192 // Copy over the known properties which have a 1:1 mapping with JSON.
1193 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1194 var inputJsonKey = KEY_PROPERTIES[k].inputJsonKey;
1195 if (inputJsonKey != undefined) {
1196 newRow[k] = getPropertyByPath(origRow, inputJsonKey);
1197 }
1198 }
1199
1200 // Add our computed properties.
1201 augmentDataRow(newRow);
1202
1203 this.allData_.push(newRow);
1204 }
1205
1206 this.redrawData_();
1207 },
1208
1209 redrawData_: function() {
1210 // Eliminate columns which we are merging on.
1211 var mergedKeys = this.getMergeColumns_();
1212 var data = mergeRows(
1213 this.allData_, mergedKeys, this.shouldMergeSimilarThreads_());
1214
1215 // Figure out what columns to include, based on the selected checkboxes.
1216 var columns = this.getSelectionColumns_();
1217 deleteValuesFromArray(columns, mergedKeys);
1218
1219 // Group, aggregate, filter, and sort the data.
1220 var groupedData = prepareData(
1221 data, this.getGroupingFunction_(), this.getFilterFunction_(),
1222 this.getSortingFunction_());
1223
1224 // Figure out a display order for the groups.
1225 var groupKeys = getDictionaryKeys(groupedData);
1226 groupKeys.sort(this.getGroupSortingFunction_());
1227
1228 // Clear the results div, sine we may be overwriting older data.
1229 var parent = $(RESULTS_DIV_ID);
1230 parent.innerHTML = '';
1231
1232 if (groupKeys.length > 0) {
1233 // The grouping will be the the same for each so just pick the first.
1234 var randomGroupKey = JSON.parse(groupKeys[0]);
1235
1236 // The grouped properties are going to be the same for each row in our,
1237 // table, so avoid drawing them in our table!
1238 var keysToExclude = []
1239
1240 for (var i = 0; i < randomGroupKey.length; ++i)
1241 keysToExclude.push(randomGroupKey[i].key);
1242 columns = columns.slice(0);
1243 deleteValuesFromArray(columns, keysToExclude);
1244 }
1245
1246 var columnOnClickHandler = this.onClickColumn_.bind(this);
1247
1248 // Draw each group.
1249 for (var i = 0; i < groupKeys.length; ++i) {
1250 var groupKeyString = groupKeys[i];
1251 var groupData = groupedData[groupKeyString];
1252 var groupKey = JSON.parse(groupKeyString);
1253
1254 drawGroup(parent, groupKey, groupData, columns,
1255 columnOnClickHandler, this.currentSortKeys_);
1256 }
1257 },
1258
1259 init_: function() {
1260 this.allData_ = [];
1261 this.fillSelectionCheckboxes_($(COLUMN_TOGGLES_CONTAINER_ID));
1262 this.fillMergeCheckboxes_($(COLUMN_MERGE_TOGGLES_CONTAINER_ID));
1263
1264 $(FILTER_SEARCH_ID).onsearch = this.onChangedFilter_.bind(this);
1265
1266 this.currentSortKeys_ = INITIAL_SORT_KEYS.slice(0);
1267 this.currentGroupingKeys_ = INITIAL_GROUP_KEYS.slice(0);
1268
1269 this.fillGroupingDropdowns_();
1270 this.fillSortingDropdowns_();
1271
1272 $(EDIT_COLUMNS_LINK_ID).onclick = this.toggleEditColumns_.bind(this);
1273
1274 $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).onchange =
1275 this.onMergeSimilarThreadsCheckboxChanged_.bind(this);
1276 },
1277
1278 toggleEditColumns_: function() {
1279 var n = $(EDIT_COLUMNS_ROW);
1280 if (n.style.display == '') {
1281 n.style.display = 'none';
1282 } else {
1283 n.style.display = '';
1284 }
1285 },
1286
1287 fillSelectionCheckboxes_: function(parent) {
1288 this.selectionCheckboxes_ = {};
1289
1290 for (var i = 0; i < ALL_TABLE_COLUMNS.length; ++i) {
1291 var key = ALL_TABLE_COLUMNS[i];
1292 var checkbox = addNode(parent, 'input');
1293 checkbox.type = 'checkbox';
1294 checkbox.onchange = this.onSelectCheckboxChanged_.bind(this);
1295 checkbox.checked = true;
1296 addNode(parent, 'span', getNameForKey(key) + ' ');
1297 this.selectionCheckboxes_[key] = checkbox;
1298 }
1299
1300 for (var i = 0; i < INITIALLY_HIDDEN_KEYS.length; ++i) {
1301 this.selectionCheckboxes_[INITIALLY_HIDDEN_KEYS[i]].checked = false;
1302 }
1303 },
1304
1305 getSelectionColumns_: function() {
1306 return getKeysForCheckedBoxes(this.selectionCheckboxes_);
1307 },
1308
1309 getMergeColumns_: function() {
1310 return getKeysForCheckedBoxes(this.mergeCheckboxes_);
1311 },
1312
1313 shouldMergeSimilarThreads_: function() {
1314 return $(MERGE_SIMILAR_THREADS_CHECKBOX_ID).checked;
1315 },
1316
1317 fillMergeCheckboxes_: function(parent) {
1318 this.mergeCheckboxes_ = {};
1319
1320 for (var i = 0; i < MERGEABLE_KEYS.length; ++i) {
1321 var key = MERGEABLE_KEYS[i];
1322 var checkbox = addNode(parent, 'input');
1323 checkbox.type = 'checkbox';
1324 checkbox.onchange = this.onMergeCheckboxChanged_.bind(this);
1325 checkbox.checked = false;
1326 addNode(parent, 'span', getNameForKey(key) + ' ');
1327 this.mergeCheckboxes_[key] = checkbox;
1328 }
1329
1330 for (var i = 0; i < INITIALLY_MERGED_KEYS.length; ++i) {
1331 this.mergeCheckboxes_[INITIALLY_MERGED_KEYS[i]].checked = true;
1332 }
1333 },
1334
1335 fillGroupingDropdowns_: function() {
1336 var parent = $(GROUP_BY_CONTAINER_ID);
1337 parent.innerHTML = '';
1338
1339 for (var i = 0; i <= this.currentGroupingKeys_.length; ++i) {
1340 // Add a dropdown.
1341 var select = addNode(parent, 'select');
1342 select.onchange = this.onChangedGrouping_.bind(this, select, i);
1343
1344 addOptionsForGroupingSelect(select);
1345
1346 if (i < this.currentGroupingKeys_.length) {
1347 var key = this.currentGroupingKeys_[i];
1348 setSelectedOptionByValue(select, key);
1349 }
1350 }
1351 },
1352
1353 fillSortingDropdowns_: function() {
1354 var parent = $(SORT_BY_CONTAINER_ID);
1355 parent.innerHTML = '';
1356
1357 for (var i = 0; i <= this.currentSortKeys_.length; ++i) {
1358 // Add a dropdown.
1359 var select = addNode(parent, 'select');
1360 select.onchange = this.onChangedSorting_.bind(this, select, i);
1361
1362 addOptionsForSortingSelect(select);
1363
1364 if (i < this.currentSortKeys_.length) {
1365 var key = this.currentSortKeys_[i];
1366 setSelectedOptionByValue(select, key);
1367 }
1368 }
1369 },
1370
1371 onChangedGrouping_: function(select, i) {
1372 updateKeyListFromDropdown(this.currentGroupingKeys_, i, select);
1373 this.fillGroupingDropdowns_();
1374 this.redrawData_();
1375 },
1376
1377 onChangedSorting_: function(select, i) {
1378 updateKeyListFromDropdown(this.currentSortKeys_, i, select);
1379 this.fillSortingDropdowns_();
1380 this.redrawData_();
1381 },
1382
1383 onSelectCheckboxChanged_: function() {
1384 this.redrawData_();
1385 },
1386
1387 onMergeCheckboxChanged_: function() {
1388 this.redrawData_();
1389 },
1390
1391 onMergeSimilarThreadsCheckboxChanged_: function() {
1392 this.redrawData_();
1393 },
1394
1395 onChangedFilter_: function() {
1396 this.redrawData_();
1397 },
1398
1399 /**
1400 * When left-clicking a column, change the primary sort order to that
1401 * column. If we were already sorted on that column then reverse the order.
1402 *
1403 * When alt-clicking, add a secondary sort column. Similarly, if
1404 * alt-clicking a column which was already being sorted on, reverse its
1405 * order.
1406 */
1407 onClickColumn_: function(key, event) {
1408 // If this property wants to start off in descending order rather then
1409 // ascending, flip it.
1410 if (KEY_PROPERTIES[key].sortDescending)
1411 key = reverseSortKey(key);
1412
1413 // Scan through our sort order and see if we are already sorted on this
1414 // key. If so, reverse that sort ordering.
1415 var found_i = -1;
1416 for (var i = 0; i < this.currentSortKeys_.length; ++i) {
1417 var curKey = this.currentSortKeys_[i];
1418 if (sortKeysMatch(curKey, key)) {
1419 this.currentSortKeys_[i] = reverseSortKey(curKey);
1420 found_i = i;
1421 break;
1422 }
1423 }
1424
1425 if (event.altKey) {
1426 if (found_i == -1) {
1427 // If we weren't already sorted on the column that was alt-clicked,
1428 // then add it to our sort.
1429 this.currentSortKeys_.push(key);
1430 }
1431 } else {
1432 if (found_i != 0 ||
1433 !sortKeysMatch(this.currentSortKeys_[found_i], key)) {
1434 // If the column we left-clicked wasn't already our primary column,
1435 // make it so.
1436 this.currentSortKeys_ = [key];
1437 } else {
1438 // If the column we left-clicked was already our primary column (and
1439 // we just reversed it), remove any secondary sorts.
1440 this.currentSortKeys_.length = 1;
1441 }
1442 }
1443
1444 this.fillSortingDropdowns_();
1445 this.redrawData_();
1446 },
1447
1448 getSortingFunction_: function() {
1449 var sortKeys = this.currentSortKeys_.slice(0);
1450
1451 // Eliminate the empty string keys (which means they were unspecified).
1452 deleteValuesFromArray(sortKeys, ['']);
1453
1454 // If no sort is specified, use our default sort.
1455 if (sortKeys.length == 0)
1456 sortKeys = [DEFAULT_SORT_KEYS];
1457
1458 return function(a, b) {
1459 for (var i = 0; i < sortKeys.length; ++i) {
1460 var key = Math.abs(sortKeys[i]);
1461 var factor = sortKeys[i] < 0 ? -1 : 1;
1462
1463 var propA = a[key];
1464 var propB = b[key];
1465
1466 var comparison = compareValuesForKey(key, propA, propB);
1467 comparison *= factor; // Possibly reverse the ordering.
1468
1469 if (comparison != 0)
1470 return comparison;
1471 }
1472
1473 // Tie breaker.
1474 return simpleCompare(JSON.stringify(a), JSON.stringify(b));
1475 };
1476 },
1477
1478 getGroupSortingFunction_: function() {
1479 return function(a, b) {
1480 var groupKey1 = JSON.parse(a);
1481 var groupKey2 = JSON.parse(b);
1482
1483 for (var i = 0; i < groupKey1.length; ++i) {
1484 var comparison = compareValuesForKey(
1485 groupKey1[i].key,
1486 groupKey1[i].value,
1487 groupKey2[i].value);
1488
1489 if (comparison != 0)
1490 return comparison;
1491 }
1492
1493 // Tie breaker.
1494 return simpleCompare(a, b);
1495 };
1496 },
1497
1498 getFilterFunction_: function() {
1499 var searchStr = $(FILTER_SEARCH_ID).value;
1500
1501 // Normalize the search expression.
1502 searchStr = trimWhitespace(searchStr);
1503 searchStr = searchStr.toLowerCase();
1504
1505 return function(x) {
1506 // Match everything when there was no filter.
1507 if (searchStr == '')
1508 return true;
1509
1510 // Treat the search text as a LOWERCASE substring search.
1511 for (var k = BEGIN_KEY; k < END_KEY; ++k) {
1512 var propertyText = getTextValueForProperty(k, x[k]);
1513 if (propertyText.toLowerCase().indexOf(searchStr) != -1)
1514 return true;
1515 }
1516
1517 return false;
1518 };
1519 },
1520
1521 getGroupingFunction_: function() {
1522 var groupings = this.currentGroupingKeys_.slice(0);
1523
1524 // Eliminate the empty string groupings (which means they were
1525 // unspecified).
1526 deleteValuesFromArray(groupings, ['']);
1527
1528 // Eliminate duplicate primary/secondary group by directives, since they
1529 // are redundant.
1530 deleteDuplicateStringsFromArray(groupings);
1531
1532 return function(e) {
1533 var groupKey = [];
1534
1535 for (var i = 0; i < groupings.length; ++i) {
1536 var entry = {key: groupings[i],
1537 value: e[groupings[i]]};
1538 groupKey.push(entry);
1539 }
1540
1541 return JSON.stringify(groupKey);
1542 };
1543 },
1544 };
1545
1546 return MainView;
1547 })();
OLDNEW
« no previous file with comments | « chrome/browser/resources/tracking.html ('k') | chrome/browser/ui/webui/chrome_web_ui_factory.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698