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