1<!DOCTYPE html> 2<!-- 3Copyright (c) 2015 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/iteration_helpers.html"> 9<link rel="import" href="/tracing/ui/base/dom_helpers.html"> 10<link rel="import" href="/tracing/value/numeric.html"> 11<link rel="import" href="/tracing/value/ui/scalar_span.html"> 12 13<script> 14'use strict'; 15 16/** 17 * @fileoverview Helper code for memory dump sub-views. 18 */ 19tr.exportTo('tr.ui.analysis', function() { 20 21 var NO_BREAK_SPACE = String.fromCharCode(160); 22 var RIGHTWARDS_ARROW = String.fromCharCode(8594); 23 24 var COLLATOR = new Intl.Collator(undefined, {numeric: true}); 25 26 /** 27 * A table column for displaying memory dump row titles. 28 * 29 * @constructor 30 */ 31 function TitleColumn(title) { 32 this.title = title; 33 } 34 35 TitleColumn.prototype = { 36 supportsCellSelection: false, 37 38 /** 39 * Get the title associated with a given row. 40 * 41 * This method will decorate the title with color and '+++'/'---' prefix if 42 * appropriate (as determined by the optional row.contexts field). 43 * Examples: 44 * 45 * +----------------------+-----------------+--------+--------+ 46 * | Contexts provided at | Interpretation | Prefix | Color | 47 * +----------------------+-----------------+--------+--------+ 48 * | 1111111111 | always present | | | 49 * | 0000111111 | added | +++ | red | 50 * | 1111111000 | deleted | --- | green | 51 * | 1100111111* | flaky | | purple | 52 * | 0001001111 | added + flaky | +++ | purple | 53 * | 1111100010 | deleted + flaky | --- | purple | 54 * +----------------------+-----------------+--------+--------+ 55 * 56 * *) This means that, given a selection of 10 memory dumps, a particular 57 * row (e.g. a process) was present in the first 2 and last 6 of them 58 * (but not in the third and fourth dump). 59 * 60 * This method should therefore NOT be overriden by subclasses. The 61 * formatTitle method should be overriden instead when necessary. 62 */ 63 value: function(row) { 64 var formattedTitle = this.formatTitle(row); 65 66 var contexts = row.contexts; 67 if (contexts === undefined || contexts.length === 0) 68 return formattedTitle; 69 70 // Determine if the row was provided in the first and last row and how 71 // many times it changed between being provided and not provided. 72 var firstContext = contexts[0]; 73 var lastContext = contexts[contexts.length - 1]; 74 var changeDefinedContextCount = 0; 75 for (var i = 1; i < contexts.length; i++) { 76 if ((contexts[i] === undefined) !== (contexts[i - 1] === undefined)) 77 changeDefinedContextCount++; 78 } 79 80 // Determine the color and prefix of the title. 81 var color = undefined; 82 var prefix = undefined; 83 if (!firstContext && lastContext) { 84 // The row was added. 85 color = 'red'; 86 prefix = '+++'; 87 } else if (firstContext && !lastContext) { 88 // The row was removed. 89 color = 'green'; 90 prefix = '---'; 91 } 92 if (changeDefinedContextCount > 1) { 93 // The row was flaky (added/removed more than once). 94 color = 'purple'; 95 } 96 97 if (color === undefined && prefix === undefined) 98 return formattedTitle; 99 100 var titleEl = document.createElement('span'); 101 if (prefix !== undefined) { 102 var prefixEl = tr.ui.b.createSpan({textContent: prefix}); 103 // Enforce same width of '+++' and '---'. 104 prefixEl.style.fontFamily = 'monospace'; 105 titleEl.appendChild(prefixEl); 106 titleEl.appendChild(tr.ui.b.asHTMLOrTextNode(NO_BREAK_SPACE)); 107 } 108 if (color !== undefined) 109 titleEl.style.color = color; 110 titleEl.appendChild(tr.ui.b.asHTMLOrTextNode(formattedTitle)); 111 return titleEl; 112 }, 113 114 /** 115 * Format the title associated with a given row. This method is intended to 116 * be overriden by subclasses. 117 */ 118 formatTitle: function(row) { 119 return row.title; 120 }, 121 122 cmp: function(rowA, rowB) { 123 return COLLATOR.compare(rowA.title, rowB.title); 124 } 125 }; 126 127 /** 128 * Abstract table column for displaying memory dump data. 129 * 130 * @constructor 131 */ 132 function MemoryColumn(name, cellPath, aggregationMode) { 133 this.name = name; 134 this.cellPath = cellPath; 135 136 // See MemoryColumn.AggregationMode enum in this file. 137 this.aggregationMode = aggregationMode; 138 } 139 140 /** 141 * Construct columns from cells in a hierarchy of rows and a list of rules. 142 * 143 * The list of rules contains objects with three fields: 144 * 145 * condition: Optional string or regular expression matched against the 146 * name of a cell. If omitted, the rule will match any cell. 147 * importance: Mandatory number which determines the final order of the 148 * columns. The column with the highest importance will be first in the 149 * returned array. 150 * columnConstructor: Mandatory memory column constructor. 151 * 152 * Example: 153 * 154 * var importanceRules = [ 155 * { 156 * condition: 'page_size', 157 * columnConstructor: NumericMemoryColumn, 158 * importance: 8 159 * }, 160 * { 161 * condition: /size/, 162 * columnConstructor: CustomNumericMemoryColumn, 163 * importance: 10 164 * }, 165 * { 166 * // No condition: matches all columns. 167 * columnConstructor: NumericMemoryColumn, 168 * importance: 9 169 * } 170 * ]; 171 * 172 * Given a name of a cell, the corresponding column constructor and 173 * importance are determined by the first rule whose condition matches the 174 * column's name. For example, given a cell with name 'inner_size', the 175 * corresponding column will be constructed using CustomNumericMemoryColumn 176 * and its importance (for sorting purposes) will be 10 (second rule). 177 * 178 * After columns are constructed for all cell names, they are sorted in 179 * descending order of importance and the resulting list is returned. In the 180 * example above, the constructed columns will be sorted into three groups as 181 * follows: 182 * 183 * [most important, left in the resulting table] 184 * 1. columns whose name contains 'size' excluding 'page_size' because it 185 * would have already matched the first rule (Note that string matches 186 * must be exact so a column named 'page_size2' would not match the 187 * first rule and would therefore belong to this group). 188 * 2. columns whose name does not contain 'size'. 189 * 3. columns whose name is 'page_size'. 190 * [least important, right in the resulting table] 191 * 192 * where columns will be sorted alphabetically within each group. 193 */ 194 MemoryColumn.fromRows = function(rows, cellKey, aggregationMode, rules) { 195 // Recursively find the names of all cells of the rows (and their sub-rows). 196 var cellNames = new Set(); 197 function gatherCellNames(rows) { 198 rows.forEach(function(row) { 199 if (row === undefined) 200 return; 201 var fieldCells = row[cellKey]; 202 if (fieldCells !== undefined) { 203 tr.b.iterItems(fieldCells, function(fieldName, fieldCell) { 204 if (fieldCell === undefined || fieldCell.fields === undefined) 205 return; 206 cellNames.add(fieldName); 207 }); 208 } 209 var subRows = row.subRows; 210 if (subRows !== undefined) 211 gatherCellNames(subRows); 212 }); 213 } 214 gatherCellNames(rows); 215 216 // Based on the provided list of rules, construct the columns and calculate 217 // their importance. 218 var positions = []; 219 cellNames.forEach(function(cellName) { 220 var cellPath = [cellKey, cellName]; 221 var matchingRule = MemoryColumn.findMatchingRule(cellName, rules); 222 var constructor = matchingRule.columnConstructor; 223 var column = new constructor(cellName, cellPath, aggregationMode); 224 positions.push({ 225 importance: matchingRule.importance, 226 column: column 227 }); 228 }); 229 230 positions.sort(function(a, b) { 231 // Sort columns with the same importance alphabetically. 232 if (a.importance === b.importance) 233 return COLLATOR.compare(a.column.name, b.column.name); 234 235 // Sort columns in descending order of importance. 236 return b.importance - a.importance; 237 }); 238 239 return positions.map(function(position) { return position.column }); 240 }; 241 242 MemoryColumn.spaceEqually = function(columns) { 243 var columnWidth = (100 / columns.length).toFixed(3) + '%'; 244 columns.forEach(function(column) { 245 column.width = columnWidth; 246 }); 247 }; 248 249 MemoryColumn.findMatchingRule = function(name, rules) { 250 for (var i = 0; i < rules.length; i++) { 251 var rule = rules[i]; 252 if (MemoryColumn.nameMatchesCondition(name, rule.condition)) 253 return rule; 254 } 255 return undefined; 256 }; 257 258 MemoryColumn.nameMatchesCondition = function(name, condition) { 259 // Rules without conditions match all columns. 260 if (condition === undefined) 261 return true; 262 263 // String conditions must match the column name exactly. 264 if (typeof(condition) === 'string') 265 return name === condition; 266 267 // If the condition is not a string, assume it is a RegExp. 268 return condition.test(name); 269 }; 270 271 /** @enum */ 272 MemoryColumn.AggregationMode = { 273 DIFF: 0, 274 MAX: 1 275 }; 276 277 MemoryColumn.SOME_TIMESTAMPS_INFO_QUANTIFIER = 'at some selected timestamps'; 278 279 MemoryColumn.prototype = { 280 get title() { 281 return this.name; 282 }, 283 284 cell: function(row) { 285 var cell = row; 286 var cellPath = this.cellPath; 287 for (var i = 0; i < cellPath.length; i++) { 288 if (cell === undefined) 289 return undefined; 290 cell = cell[cellPath[i]]; 291 } 292 return cell; 293 }, 294 295 aggregateCells: function(row, subRows) { 296 // No generic aggregation. 297 }, 298 299 fields: function(row) { 300 var cell = this.cell(row); 301 if (cell === undefined) 302 return undefined; 303 return cell.fields; 304 }, 305 306 /** 307 * Format a cell associated with this column from the given row. This 308 * method is not intended to be overriden. 309 */ 310 value: function(row) { 311 var fields = this.fields(row); 312 if (this.hasAllRelevantFieldsUndefined(fields)) 313 return ''; 314 315 // Determine the color and infos of the resulting element. 316 var contexts = row.contexts; 317 var color = this.color(fields, contexts); 318 var infos = []; 319 this.addInfos(fields, contexts, infos); 320 321 // Format the actual fields. 322 var formattedFields = this.formatFields(fields); 323 324 // If no color is specified and there are no infos, there is no need to 325 // wrap the value in a span element.# 326 if ((color === undefined || formattedFields === '') && infos.length === 0) 327 return formattedFields; 328 329 var fieldEl = document.createElement('span'); 330 fieldEl.style.display = 'flex'; 331 fieldEl.style.alignItems = 'center'; 332 fieldEl.appendChild(tr.ui.b.asHTMLOrTextNode(formattedFields)); 333 334 // Add info icons with tooltips. 335 infos.forEach(function(info) { 336 var infoEl = document.createElement('span'); 337 infoEl.style.paddingLeft = '4px'; 338 infoEl.style.cursor = 'help'; 339 infoEl.style.fontWeight = 'bold'; 340 infoEl.textContent = info.icon; 341 if (info.color !== undefined) 342 infoEl.style.color = info.color; 343 infoEl.title = info.message; 344 fieldEl.appendChild(infoEl); 345 }, this); 346 347 // Set the color of the element. 348 if (color !== undefined) 349 fieldEl.style.color = color; 350 351 return fieldEl; 352 }, 353 354 /** 355 * Returns true iff all fields of a row which are relevant for the current 356 * aggregation mode (e.g. first and last field for diff mode) are undefined. 357 */ 358 hasAllRelevantFieldsUndefined: function(fields) { 359 if (fields === undefined) 360 return true; 361 362 switch (this.aggregationMode) { 363 case MemoryColumn.AggregationMode.DIFF: 364 // Only the first and last field are relevant. 365 return fields[0] === undefined && 366 fields[fields.length - 1] === undefined; 367 368 case MemoryColumn.AggregationMode.MAX: 369 default: 370 // All fields are relevant. 371 return fields.every(function(field) { return field === undefined; }); 372 } 373 }, 374 375 /** 376 * Get the color of the given fields formatted by this column. At least one 377 * field relevant for the current aggregation mode is guaranteed to be 378 * defined. 379 */ 380 color: function(fields, contexts) { 381 return undefined; 382 }, 383 384 /** 385 * Format an arbitrary number of fields. At least one field relevant for 386 * the current aggregation mode is guaranteed to be defined. 387 */ 388 formatFields: function(fields) { 389 if (fields.length === 1) 390 return this.formatSingleField(fields[0]); 391 else 392 return this.formatMultipleFields(fields); 393 }, 394 395 /** 396 * Format a single defined field. 397 * 398 * This method is intended to be overriden by field type specific columns 399 * (e.g. show '1.0 KiB' instead of '1024' for ScalarNumeric(s) representing 400 * bytes). 401 */ 402 formatSingleField: function(field) { 403 throw new Error('Not implemented'); 404 }, 405 406 /** 407 * Format multiple fields. At least one field relevant for the current 408 * aggregation mode is guaranteed to be defined. 409 * 410 * The aggregation mode specializations of this method (e.g. 411 * formatMultipleFieldsDiff) are intended to be overriden by field type 412 * specific columns. 413 */ 414 formatMultipleFields: function(fields) { 415 switch (this.aggregationMode) { 416 case MemoryColumn.AggregationMode.DIFF: 417 return this.formatMultipleFieldsDiff( 418 fields[0], fields[fields.length - 1]); 419 420 case MemoryColumn.AggregationMode.MAX: 421 return this.formatMultipleFieldsMax(fields); 422 423 default: 424 return tr.ui.b.createSpan({ 425 textContent: '(unsupported aggregation mode)', 426 italic: true 427 }); 428 } 429 }, 430 431 formatMultipleFieldsDiff: function(firstField, lastField) { 432 throw new Error('Not implemented'); 433 }, 434 435 formatMultipleFieldsMax: function(fields) { 436 return this.formatSingleField(this.getMaxField(fields)); 437 }, 438 439 cmp: function(rowA, rowB) { 440 var fieldsA = this.fields(rowA); 441 var fieldsB = this.fields(rowB); 442 443 // Sanity check. 444 if (fieldsA !== undefined && fieldsB !== undefined && 445 fieldsA.length !== fieldsB.length) 446 throw new Error('Different number of fields'); 447 448 // Handle empty fields. 449 var undefinedA = this.hasAllRelevantFieldsUndefined(fieldsA); 450 var undefinedB = this.hasAllRelevantFieldsUndefined(fieldsB); 451 if (undefinedA && undefinedB) 452 return 0; 453 if (undefinedA) 454 return -1; 455 if (undefinedB) 456 return 1; 457 458 return this.compareFields(fieldsA, fieldsB); 459 }, 460 461 /** 462 * Compare a pair of single or multiple fields. At least one field relevant 463 * for the current aggregation mode is guaranteed to be defined in each of 464 * the two lists. 465 */ 466 compareFields: function(fieldsA, fieldsB) { 467 if (fieldsA.length === 1) 468 return this.compareSingleFields(fieldsA[0], fieldsB[0]); 469 else 470 return this.compareMultipleFields(fieldsA, fieldsB); 471 }, 472 473 /** 474 * Compare a pair of single defined fields. 475 * 476 * This method is intended to be overriden by field type specific columns. 477 */ 478 compareSingleFields: function(fieldA, fieldB) { 479 throw new Error('Not implemented'); 480 }, 481 482 /** 483 * Compare a pair of multiple fields. At least one field relevant for the 484 * current aggregation mode is guaranteed to be defined in each of the two 485 * lists. 486 * 487 * The aggregation mode specializations of this method (e.g. 488 * compareMultipleFieldsDiff) are intended to be overriden by field type 489 * specific columns. 490 */ 491 compareMultipleFields: function(fieldsA, fieldsB) { 492 switch (this.aggregationMode) { 493 case MemoryColumn.AggregationMode.DIFF: 494 return this.compareMultipleFieldsDiff( 495 fieldsA[0], fieldsA[fieldsA.length - 1], 496 fieldsB[0], fieldsB[fieldsB.length - 1]); 497 498 case MemoryColumn.AggregationMode.MAX: 499 return this.compareMultipleFieldsMax(fieldsA, fieldsB); 500 501 default: 502 return 0; 503 } 504 }, 505 506 compareMultipleFieldsDiff: function(firstFieldA, lastFieldA, firstFieldB, 507 lastFieldB) { 508 throw new Error('Not implemented'); 509 }, 510 511 compareMultipleFieldsMax: function(fieldsA, fieldsB) { 512 return this.compareSingleFields( 513 this.getMaxField(fieldsA), this.getMaxField(fieldsB)); 514 }, 515 516 getMaxField: function(fields) { 517 return fields.reduce(function(accumulator, field) { 518 if (field === undefined) 519 return accumulator; 520 if (accumulator === undefined || 521 this.compareSingleFields(field, accumulator) > 0) { 522 return field; 523 } 524 return accumulator; 525 }.bind(this), undefined); 526 }, 527 528 addInfos: function(fields, contexts, infos) { 529 // No generic infos. 530 }, 531 532 getImportance: function(importanceRules) { 533 if (importanceRules.length === 0) 534 return 0; 535 536 // Find the first matching rule. 537 var matchingRule = 538 MemoryColumn.findMatchingRule(this.name, importanceRules); 539 if (matchingRule !== undefined) 540 return matchingRule.importance; 541 542 // No matching rule. Return lower importance than all rules. 543 var minImportance = importanceRules[0].importance; 544 for (var i = 1; i < importanceRules.length; i++) 545 minImportance = Math.min(minImportance, importanceRules[i].importance); 546 return minImportance - 1; 547 } 548 }; 549 550 /** 551 * @constructor 552 */ 553 function StringMemoryColumn(name, cellPath, aggregationMode) { 554 MemoryColumn.call(this, name, cellPath, aggregationMode); 555 } 556 557 StringMemoryColumn.prototype = { 558 __proto__: MemoryColumn.prototype, 559 560 formatSingleField: function(string) { 561 return string; 562 }, 563 564 formatMultipleFieldsDiff: function(firstString, lastString) { 565 if (firstString === undefined) { 566 // String was added ("+NEW_VALUE" in red). 567 var spanEl = tr.ui.b.createSpan({color: 'red'}); 568 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode('+')); 569 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode( 570 this.formatSingleField(lastString))); 571 return spanEl; 572 } else if (lastString === undefined) { 573 // String was removed ("-OLD_VALUE" in green). 574 var spanEl = tr.ui.b.createSpan({color: 'green'}); 575 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode('-')); 576 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode( 577 this.formatSingleField(firstString))); 578 return spanEl; 579 } else if (firstString === lastString) { 580 // String didn't change ("VALUE" with unchanged color). 581 return this.formatSingleField(firstString); 582 } else { 583 // String changed ("OLD_VALUE -> NEW_VALUE" in orange). 584 var spanEl = tr.ui.b.createSpan({color: 'DarkOrange'}); 585 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode( 586 this.formatSingleField(firstString))); 587 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode( 588 ' ' + RIGHTWARDS_ARROW + ' ')); 589 spanEl.appendChild(tr.ui.b.asHTMLOrTextNode( 590 this.formatSingleField(lastString))); 591 return spanEl; 592 } 593 }, 594 595 compareSingleFields: function(stringA, stringB) { 596 return COLLATOR.compare(stringA, stringB); 597 }, 598 599 compareMultipleFieldsDiff: function(firstStringA, lastStringA, firstStringB, 600 lastStringB) { 601 // If one of the strings was added (and the other one wasn't), mark the 602 // corresponding diff as greater. 603 if (firstStringA === undefined && firstStringB !== undefined) 604 return 1; 605 if (firstStringA !== undefined && firstStringB === undefined) 606 return -1; 607 608 // If both strings were added, compare the last values (greater last 609 // value implies greater diff). 610 if (firstStringA === undefined && firstStringB === undefined) 611 return this.compareSingleFields(lastStringA, lastStringB); 612 613 // If one of the strings was removed (and the other one wasn't), mark the 614 // corresponding diff as lower. 615 if (lastStringA === undefined && lastStringB !== undefined) 616 return -1; 617 if (lastStringA !== undefined && lastStringB === undefined) 618 return 1; 619 620 // If both strings were removed, compare the first values (greater first 621 // value implies smaller (!) diff). 622 if (lastStringA === undefined && lastStringB === undefined) 623 return this.compareSingleFields(firstStringB, firstStringA); 624 625 var areStringsAEqual = firstStringA === lastStringA; 626 var areStringsBEqual = firstStringB === lastStringB; 627 628 // Consider diffs of strings that did not change to be smaller than diffs 629 // of strings that did change. 630 if (areStringsAEqual && areStringsBEqual) 631 return 0; 632 if (areStringsAEqual) 633 return -1; 634 if (areStringsBEqual) 635 return 1; 636 637 // Both strings changed. We are unable to determine the ordering of the 638 // diffs. 639 return 0; 640 } 641 }; 642 643 /** 644 * @constructor 645 */ 646 function NumericMemoryColumn(name, cellPath, aggregationMode) { 647 MemoryColumn.call(this, name, cellPath, aggregationMode); 648 } 649 650 // Avoid tiny positive/negative diffs (displayed in the UI as '+0.0 B' and 651 // '-0.0 B') due to imprecise floating-point arithmetic by treating all diffs 652 // within the (-DIFF_EPSILON, DIFF_EPSILON) range as zeros. 653 NumericMemoryColumn.DIFF_EPSILON = 0.0001; 654 655 NumericMemoryColumn.prototype = { 656 __proto__: MemoryColumn.prototype, 657 658 aggregateCells: function(row, subRows) { 659 var subRowCells = subRows.map(this.cell, this); 660 661 // Determine if there is at least one defined numeric in the sub-row 662 // cells and the timestamp count. 663 var hasDefinedSubRowNumeric = false; 664 var timestampCount = undefined; 665 subRowCells.forEach(function(subRowCell) { 666 if (subRowCell === undefined) 667 return; 668 669 var subRowNumerics = subRowCell.fields; 670 if (subRowNumerics === undefined) 671 return; 672 673 if (timestampCount === undefined) 674 timestampCount = subRowNumerics.length; 675 else if (timestampCount !== subRowNumerics.length) 676 throw new Error('Sub-rows have different numbers of timestamps'); 677 678 if (hasDefinedSubRowNumeric) 679 return; // Avoid unnecessary traversals of the numerics. 680 hasDefinedSubRowNumeric = subRowNumerics.some(function(numeric) { 681 return numeric !== undefined; 682 }); 683 }); 684 if (!hasDefinedSubRowNumeric) 685 return; // No numeric to aggregate. 686 687 // Get or create the row cell. 688 var cellPath = this.cellPath; 689 var rowCell = row; 690 for (var i = 0; i < cellPath.length; i++) { 691 var nextStepName = cellPath[i]; 692 var nextStep = rowCell[nextStepName]; 693 if (nextStep === undefined) { 694 if (i < cellPath.length - 1) 695 nextStep = {}; 696 else 697 nextStep = new MemoryCell(undefined); 698 rowCell[nextStepName] = nextStep; 699 } 700 rowCell = nextStep; 701 } 702 if (rowCell.fields === undefined) { 703 rowCell.fields = new Array(timestampCount); 704 } else if (rowCell.fields.length !== timestampCount) { 705 throw new Error( 706 'Row has a different number of timestamps than sub-rows'); 707 } 708 709 for (var i = 0; i < timestampCount; i++) { 710 if (rowCell.fields[i] !== undefined) 711 continue; 712 rowCell.fields[i] = tr.model.MemoryAllocatorDump.aggregateNumerics( 713 subRowCells.map(function(subRowCell) { 714 if (subRowCell === undefined || subRowCell.fields === undefined) 715 return undefined; 716 return subRowCell.fields[i]; 717 })); 718 } 719 }, 720 721 formatSingleField: function(numeric) { 722 if (numeric === undefined) 723 return ''; 724 return tr.v.ui.createScalarSpan(numeric); 725 }, 726 727 formatMultipleFieldsDiff: function(firstNumeric, lastNumeric) { 728 return this.formatSingleField( 729 this.getDiffField_(firstNumeric, lastNumeric)); 730 }, 731 732 compareSingleFields: function(numericA, numericB) { 733 return numericA.value - numericB.value; 734 }, 735 736 compareMultipleFieldsDiff: function(firstNumericA, lastNumericA, 737 firstNumericB, lastNumericB) { 738 return this.getDiffFieldValue_(firstNumericA, lastNumericA) - 739 this.getDiffFieldValue_(firstNumericB, lastNumericB); 740 }, 741 742 getDiffField_: function(firstNumeric, lastNumeric) { 743 var definedNumeric = firstNumeric || lastNumeric; 744 return new tr.v.ScalarNumeric(definedNumeric.unit.correspondingDeltaUnit, 745 this.getDiffFieldValue_(firstNumeric, lastNumeric)); 746 }, 747 748 getDiffFieldValue_: function(firstNumeric, lastNumeric) { 749 var firstValue = firstNumeric === undefined ? 0 : firstNumeric.value; 750 var lastValue = lastNumeric === undefined ? 0 : lastNumeric.value; 751 var diff = lastValue - firstValue; 752 return Math.abs(diff) < NumericMemoryColumn.DIFF_EPSILON ? 0 : diff; 753 } 754 }; 755 756 /** 757 * @constructor 758 */ 759 function MemoryCell(fields) { 760 this.fields = fields; 761 } 762 763 MemoryCell.extractFields = function(cell) { 764 if (cell === undefined) 765 return undefined; 766 return cell.fields; 767 }; 768 769 /** Limit for the number of sub-rows for recursive table row expansion. */ 770 var RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT = 10; 771 772 function expandTableRowsRecursively(table) { 773 var currentLevelRows = table.tableRows; 774 var totalVisibleRowCount = currentLevelRows.length; 775 776 while (currentLevelRows.length > 0) { 777 // Calculate the total number of sub-rows on the current level. 778 var nextLevelRowCount = 0; 779 currentLevelRows.forEach(function(currentLevelRow) { 780 var subRows = currentLevelRow.subRows; 781 if (subRows === undefined || subRows.length === 0) 782 return; 783 nextLevelRowCount += subRows.length; 784 }); 785 786 // Determine whether expanding all rows on the current level would cause 787 // the total number of visible rows go over the limit. 788 if (totalVisibleRowCount + nextLevelRowCount > 789 RECURSIVE_EXPANSION_MAX_VISIBLE_ROW_COUNT) { 790 break; 791 } 792 793 // Expand all rows on the current level and gather their sub-rows. 794 var nextLevelRows = new Array(nextLevelRowCount); 795 var nextLevelRowIndex = 0; 796 currentLevelRows.forEach(function(currentLevelRow) { 797 var subRows = currentLevelRow.subRows; 798 if (subRows === undefined || subRows.length === 0) 799 return; 800 table.setExpandedForTableRow(currentLevelRow, true); 801 subRows.forEach(function(subRow) { 802 nextLevelRows[nextLevelRowIndex++] = subRow; 803 }); 804 }); 805 806 // Update the total number of visible rows and progress to the next level. 807 totalVisibleRowCount += nextLevelRowCount; 808 currentLevelRows = nextLevelRows; 809 } 810 } 811 812 function aggregateTableRowCellsRecursively(row, columns, opt_predicate) { 813 var subRows = row.subRows; 814 if (subRows === undefined || subRows.length === 0) 815 return; 816 817 subRows.forEach(function(subRow) { 818 aggregateTableRowCellsRecursively(subRow, columns, opt_predicate); 819 }); 820 821 if (opt_predicate === undefined || opt_predicate(row.contexts)) 822 aggregateTableRowCells(row, subRows, columns); 823 } 824 825 function aggregateTableRowCells(row, subRows, columns) { 826 columns.forEach(function(column) { 827 if (!(column instanceof MemoryColumn)) 828 return; 829 column.aggregateCells(row, subRows); 830 }); 831 } 832 833 function createCells(timeToValues, valueFieldsGetter) { 834 var fieldNameToFields = tr.b.invertArrayOfDicts( 835 timeToValues, valueFieldsGetter); 836 return tr.b.mapItems(fieldNameToFields, function(fieldName, fields) { 837 return new tr.ui.analysis.MemoryCell(fields); 838 }); 839 } 840 841 function createWarningInfo(message) { 842 return { 843 message: message, 844 icon: String.fromCharCode(9888), 845 color: 'red' 846 }; 847 } 848 849 return { 850 TitleColumn: TitleColumn, 851 MemoryColumn: MemoryColumn, 852 StringMemoryColumn: StringMemoryColumn, 853 NumericMemoryColumn: NumericMemoryColumn, 854 MemoryCell: MemoryCell, 855 expandTableRowsRecursively: expandTableRowsRecursively, 856 aggregateTableRowCellsRecursively: aggregateTableRowCellsRecursively, 857 aggregateTableRowCells: aggregateTableRowCells, 858 createCells: createCells, 859 createWarningInfo: createWarningInfo 860 }; 861}); 862</script> 863