summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/modules/widgets/tree-view.js
blob: d3d81fe3bf9dd50d88d6cfc2a229da038db72643 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

/**
 * This file contains the tree view, displaying all the samples and frames
 * received from the proviler in a tree-like structure.
 */

const { L10N } = require("devtools/client/performance/modules/global");
const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");

const URL_LABEL_TOOLTIP = L10N.getStr("table.url.tooltiptext");
const VIEW_OPTIMIZATIONS_TOOLTIP = L10N.getStr("table.view-optimizations.tooltiptext2");

// px
const CALL_TREE_INDENTATION = 16;

// Used for rendering values in cells
const FORMATTERS = {
  TIME: (value) => L10N.getFormatStr("table.ms2", L10N.numberWithDecimals(value, 2)),
  PERCENT: (value) => L10N.getFormatStr("table.percentage3",
                                        L10N.numberWithDecimals(value, 2)),
  NUMBER: (value) => value || 0,
  BYTESIZE: (value) => L10N.getFormatStr("table.bytes", (value || 0))
};

/**
 * Definitions for rendering cells. Triads of class name, property name from
 * `frame.getInfo()`, and a formatter function.
 */
const CELLS = {
  duration: ["duration", "totalDuration", FORMATTERS.TIME],
  percentage: ["percentage", "totalPercentage", FORMATTERS.PERCENT],
  selfDuration: ["self-duration", "selfDuration", FORMATTERS.TIME],
  selfPercentage: ["self-percentage", "selfPercentage", FORMATTERS.PERCENT],
  samples: ["samples", "samples", FORMATTERS.NUMBER],

  selfSize: ["self-size", "selfSize", FORMATTERS.BYTESIZE],
  selfSizePercentage: ["self-size-percentage", "selfSizePercentage", FORMATTERS.PERCENT],
  selfCount: ["self-count", "selfCount", FORMATTERS.NUMBER],
  selfCountPercentage: ["self-count-percentage", "selfCountPercentage",
                        FORMATTERS.PERCENT],
  size: ["size", "totalSize", FORMATTERS.BYTESIZE],
  sizePercentage: ["size-percentage", "totalSizePercentage", FORMATTERS.PERCENT],
  count: ["count", "totalCount", FORMATTERS.NUMBER],
  countPercentage: ["count-percentage", "totalCountPercentage", FORMATTERS.PERCENT],
};
const CELL_TYPES = Object.keys(CELLS);

const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
  let dataA = frameA.getDisplayedData();
  let dataB = frameB.getDisplayedData();
  let isAllocations = "totalSize" in dataA;

  if (isAllocations) {
    if (this.inverted && dataA.selfSize !== dataB.selfSize) {
      return dataA.selfSize < dataB.selfSize ? 1 : -1;
    }
    return dataA.totalSize < dataB.totalSize ? 1 : -1;
  }

  if (this.inverted && dataA.selfPercentage !== dataB.selfPercentage) {
    return dataA.selfPercentage < dataB.selfPercentage ? 1 : -1;
  }
  return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
};

// depth
const DEFAULT_AUTO_EXPAND_DEPTH = 3;
const DEFAULT_VISIBLE_CELLS = {
  duration: true,
  percentage: true,
  selfDuration: true,
  selfPercentage: true,
  samples: true,
  function: true,

  // allocation columns
  count: false,
  selfCount: false,
  size: false,
  selfSize: false,
  countPercentage: false,
  selfCountPercentage: false,
  sizePercentage: false,
  selfSizePercentage: false,
};

/**
 * An item in a call tree view, which looks like this:
 *
 *   Time (ms)  |   Cost   | Calls | Function
 * ============================================================================
 *     1,000.00 |  100.00% |       | ▼ (root)
 *       500.12 |   50.01% |   300 |   ▼ foo                          Categ. 1
 *       300.34 |   30.03% |  1500 |     ▼ bar                        Categ. 2
 *        10.56 |    0.01% |    42 |       ▶ call_with_children       Categ. 3
 *        90.78 |    0.09% |    25 |         call_without_children    Categ. 4
 *
 * Every instance of a `CallView` represents a row in the call tree. The same
 * parent node is used for all rows.
 *
 * @param CallView caller
 *        The CallView considered the "caller" frame. This newly created
 *        instance will be represent the "callee". Should be null for root nodes.
 * @param ThreadNode | FrameNode frame
 *        Details about this function, like { samples, duration, calls } etc.
 * @param number level [optional]
 *        The indentation level in the call tree. The root node is at level 0.
 * @param boolean hidden [optional]
 *        Whether this node should be hidden and not contribute to depth/level
 *        calculations. Defaults to false.
 * @param boolean inverted [optional]
 *        Whether the call tree has been inverted (bottom up, rather than
 *        top-down). Defaults to false.
 * @param function sortingPredicate [optional]
 *        The predicate used to sort the tree items when created. Defaults to
 *        the caller's `sortingPredicate` if a caller exists, otherwise defaults
 *        to DEFAULT_SORTING_PREDICATE. The two passed arguments are FrameNodes.
 * @param number autoExpandDepth [optional]
 *        The depth to which the tree should automatically expand. Defualts to
 *        the caller's `autoExpandDepth` if a caller exists, otherwise defaults
 *        to DEFAULT_AUTO_EXPAND_DEPTH.
 * @param object visibleCells
 *        An object specifying which cells are visible in the tree. Defaults to
 *        the caller's `visibleCells` if a caller exists, otherwise defaults
 *        to DEFAULT_VISIBLE_CELLS.
 * @param boolean showOptimizationHint [optional]
 *        Whether or not to show an icon indicating if the frame has optimization
 *        data.
 */
function CallView({
  caller, frame, level, hidden, inverted,
  sortingPredicate, autoExpandDepth, visibleCells,
  showOptimizationHint
}) {
  AbstractTreeItem.call(this, {
    parent: caller,
    level: level | 0 - (hidden ? 1 : 0)
  });

  if (sortingPredicate != null) {
    this.sortingPredicate = sortingPredicate;
  } else if (caller) {
    this.sortingPredicate = caller.sortingPredicate;
  } else {
    this.sortingPredicate = DEFAULT_SORTING_PREDICATE;
  }

  if (autoExpandDepth != null) {
    this.autoExpandDepth = autoExpandDepth;
  } else if (caller) {
    this.autoExpandDepth = caller.autoExpandDepth;
  } else {
    this.autoExpandDepth = DEFAULT_AUTO_EXPAND_DEPTH;
  }

  if (visibleCells != null) {
    this.visibleCells = visibleCells;
  } else if (caller) {
    this.visibleCells = caller.visibleCells;
  } else {
    this.visibleCells = Object.create(DEFAULT_VISIBLE_CELLS);
  }

  this.caller = caller;
  this.frame = frame;
  this.hidden = hidden;
  this.inverted = inverted;
  this.showOptimizationHint = showOptimizationHint;

  this._onUrlClick = this._onUrlClick.bind(this);
}

CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
  /**
   * Creates the view for this tree node.
   * @param nsIDOMNode document
   * @param nsIDOMNode arrowNode
   * @return nsIDOMNode
   */
  _displaySelf: function (document, arrowNode) {
    let frameInfo = this.getDisplayedData();
    let cells = [];

    for (let type of CELL_TYPES) {
      if (this.visibleCells[type]) {
        // Inline for speed, but pass in the formatted value via
        // cell definition, as well as the element type.
        cells.push(this._createCell(document, CELLS[type][2](frameInfo[CELLS[type][1]]),
                                    CELLS[type][0]));
      }
    }

    if (this.visibleCells.function) {
      cells.push(this._createFunctionCell(document, arrowNode, frameInfo.name, frameInfo,
                                          this.level));
    }

    let targetNode = document.createElement("hbox");
    targetNode.className = "call-tree-item";
    targetNode.setAttribute("origin", frameInfo.isContent ? "content" : "chrome");
    targetNode.setAttribute("category", frameInfo.categoryData.abbrev || "");
    targetNode.setAttribute("tooltiptext", frameInfo.tooltiptext);

    if (this.hidden) {
      targetNode.style.display = "none";
    }

    for (let i = 0; i < cells.length; i++) {
      targetNode.appendChild(cells[i]);
    }

    return targetNode;
  },

  /**
   * Populates this node in the call tree with the corresponding "callees".
   * These are defined in the `frame` data source for this call view.
   * @param array:AbstractTreeItem children
   */
  _populateSelf: function (children) {
    let newLevel = this.level + 1;

    for (let newFrame of this.frame.calls) {
      children.push(new CallView({
        caller: this,
        frame: newFrame,
        level: newLevel,
        inverted: this.inverted
      }));
    }

    // Sort the "callees" asc. by samples, before inserting them in the tree,
    // if no other sorting predicate was specified on this on the root item.
    children.sort(this.sortingPredicate.bind(this));
  },

  /**
   * Functions creating each cell in this call view.
   * Invoked by `_displaySelf`.
   */
  _createCell: function (doc, value, type) {
    let cell = doc.createElement("description");
    cell.className = "plain call-tree-cell";
    cell.setAttribute("type", type);
    cell.setAttribute("crop", "end");
    // Add a tabulation to the cell text in case it's is selected and copied.
    cell.textContent = value + "\t";
    return cell;
  },

  _createFunctionCell: function (doc, arrowNode, frameName, frameInfo, frameLevel) {
    let cell = doc.createElement("hbox");
    cell.className = "call-tree-cell";
    cell.style.marginInlineStart = (frameLevel * CALL_TREE_INDENTATION) + "px";
    cell.setAttribute("type", "function");
    cell.appendChild(arrowNode);

    // Render optimization hint if this frame has opt data.
    if (this.root.showOptimizationHint && frameInfo.hasOptimizations &&
        !frameInfo.isMetaCategory) {
      let icon = doc.createElement("description");
      icon.setAttribute("tooltiptext", VIEW_OPTIMIZATIONS_TOOLTIP);
      icon.className = "opt-icon";
      cell.appendChild(icon);
    }

    // Don't render a name label node if there's no function name. A different
    // location label node will be rendered instead.
    if (frameName) {
      let nameNode = doc.createElement("description");
      nameNode.className = "plain call-tree-name";
      nameNode.textContent = frameName;
      cell.appendChild(nameNode);
    }

    // Don't render detailed labels for meta category frames
    if (!frameInfo.isMetaCategory) {
      this._appendFunctionDetailsCells(doc, cell, frameInfo);
    }

    // Don't render an expando-arrow for leaf nodes.
    let hasDescendants = Object.keys(this.frame.calls).length > 0;
    if (!hasDescendants) {
      arrowNode.setAttribute("invisible", "");
    }

    // Add a line break to the last description of the row in case it's selected
    // and copied.
    let lastDescription = cell.querySelector("description:last-of-type");
    lastDescription.textContent = lastDescription.textContent + "\n";

    // Add spaces as frameLevel indicators in case the row is selected and
    // copied. These spaces won't be displayed in the cell content.
    let firstDescription = cell.querySelector("description:first-of-type");
    let levelIndicator = frameLevel > 0 ? " ".repeat(frameLevel) : "";
    firstDescription.textContent = levelIndicator + firstDescription.textContent;

    return cell;
  },

  _appendFunctionDetailsCells: function (doc, cell, frameInfo) {
    if (frameInfo.fileName) {
      let urlNode = doc.createElement("description");
      urlNode.className = "plain call-tree-url";
      urlNode.textContent = frameInfo.fileName;
      urlNode.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + frameInfo.url);
      urlNode.addEventListener("mousedown", this._onUrlClick);
      cell.appendChild(urlNode);
    }

    if (frameInfo.line) {
      let lineNode = doc.createElement("description");
      lineNode.className = "plain call-tree-line";
      lineNode.textContent = ":" + frameInfo.line;
      cell.appendChild(lineNode);
    }

    if (frameInfo.column) {
      let columnNode = doc.createElement("description");
      columnNode.className = "plain call-tree-column";
      columnNode.textContent = ":" + frameInfo.column;
      cell.appendChild(columnNode);
    }

    if (frameInfo.host) {
      let hostNode = doc.createElement("description");
      hostNode.className = "plain call-tree-host";
      hostNode.textContent = frameInfo.host;
      cell.appendChild(hostNode);
    }

    if (frameInfo.categoryData.label) {
      let categoryNode = doc.createElement("description");
      categoryNode.className = "plain call-tree-category";
      categoryNode.style.color = frameInfo.categoryData.color;
      categoryNode.textContent = frameInfo.categoryData.label;
      cell.appendChild(categoryNode);
    }
  },

  /**
   * Gets the data displayed about this tree item, based on the FrameNode
   * model associated with this view.
   *
   * @return object
   */
  getDisplayedData: function () {
    if (this._cachedDisplayedData) {
      return this._cachedDisplayedData;
    }

    this._cachedDisplayedData = this.frame.getInfo({
      root: this.root.frame,
      allocations: (this.visibleCells.count || this.visibleCells.selfCount)
    });

    return this._cachedDisplayedData;

    /**
     * When inverting call tree, the costs and times are dependent on position
     * in the tree. We must only count leaf nodes with self cost, and total costs
     * dependent on how many times the leaf node was found with a full stack path.
     *
     *   Total |  Self | Calls | Function
     * ============================================================================
     *  100%   |  100% |   100 | ▼ C
     *   50%   |   0%  |    50 |   ▼ B
     *   50%   |   0%  |    50 |     ▼ A
     *   50%   |   0%  |    50 |   ▼ B
     *
     * Every instance of a `CallView` represents a row in the call tree. The same
     * container node is used for all rows.
     */
  },

  /**
   * Toggles the category information hidden or visible.
   * @param boolean visible
   */
  toggleCategories: function (visible) {
    if (!visible) {
      this.container.setAttribute("categories-hidden", "");
    } else {
      this.container.removeAttribute("categories-hidden");
    }
  },

  /**
   * Handler for the "click" event on the url node of this call view.
   */
  _onUrlClick: function (e) {
    e.preventDefault();
    e.stopPropagation();
    // Only emit for left click events
    if (e.button === 0) {
      this.root.emit("link", this);
    }
  },
});

exports.CallView = CallView;