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