OLD | NEW |
| (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 })(); | |
OLD | NEW |