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