summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/modules/widgets/markers-overview.js
blob: 89bc79a8d650cfcef622a4a52a25b62ea88b7038 (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
/* 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 "markers overview" graph, which is a minimap of all
 * the timeline data. Regions inside it may be selected, determining which
 * markers are visible in the "waterfall".
 */

const { Heritage } = require("devtools/client/shared/widgets/view-helpers");
const { AbstractCanvasGraph } = require("devtools/client/shared/widgets/Graphs");

const { colorUtils } = require("devtools/shared/css/color");
const { getColor } = require("devtools/client/shared/theme");
const ProfilerGlobal = require("devtools/client/performance/modules/global");
const { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
const { TickUtils } = require("devtools/client/performance/modules/waterfall-ticks");
const { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");

// px
const OVERVIEW_HEADER_HEIGHT = 14;
// px
const OVERVIEW_ROW_HEIGHT = 11;

const OVERVIEW_SELECTION_LINE_COLOR = "#666";
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#555";

// ms
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100;
// px
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75;
// px
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9;
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
// px
const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6;
// px
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1;
// px
const OVERVIEW_MARKER_WIDTH_MIN = 4;
// px
const OVERVIEW_GROUP_VERTICAL_PADDING = 5;

/**
 * An overview for the markers data.
 *
 * @param nsIDOMNode parent
 *        The parent node holding the overview.
 * @param Array<String> filter
 *        List of names of marker types that should not be shown.
 */
function MarkersOverview(parent, filter = [], ...args) {
  AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
  this.setTheme();
  this.setFilter(filter);
}

MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
  clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
  selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
  headerHeight: OVERVIEW_HEADER_HEIGHT,
  rowHeight: OVERVIEW_ROW_HEIGHT,
  groupPadding: OVERVIEW_GROUP_VERTICAL_PADDING,

  /**
   * Compute the height of the overview.
   */
  get fixedHeight() {
    return this.headerHeight + this.rowHeight * this._numberOfGroups;
  },

  /**
   * List of marker types that should not be shown in the graph.
   */
  setFilter: function (filter) {
    this._paintBatches = new Map();
    this._filter = filter;
    this._groupMap = Object.create(null);

    let observedGroups = new Set();

    for (let type in TIMELINE_BLUEPRINT) {
      if (filter.indexOf(type) !== -1) {
        continue;
      }
      this._paintBatches.set(type, { definition: TIMELINE_BLUEPRINT[type], batch: [] });
      observedGroups.add(TIMELINE_BLUEPRINT[type].group);
    }

    // Take our set of observed groups and order them and map
    // the group numbers to fill in the holes via `_groupMap`.
    // This normalizes our rows by removing rows that aren't used
    // if filters are enabled.
    let actualPosition = 0;
    for (let groupNumber of Array.from(observedGroups).sort()) {
      this._groupMap[groupNumber] = actualPosition++;
    }
    this._numberOfGroups = Object.keys(this._groupMap).length;
  },

  /**
   * Disables selection and empties this graph.
   */
  clearView: function () {
    this.selectionEnabled = false;
    this.dropSelection();
    this.setData({ duration: 0, markers: [] });
  },

  /**
   * Renders the graph's data source.
   * @see AbstractCanvasGraph.prototype.buildGraphImage
   */
  buildGraphImage: function () {
    let { markers, duration } = this._data;

    let { canvas, ctx } = this._getNamedCanvas("markers-overview-data");
    let canvasWidth = this._width;
    let canvasHeight = this._height;

    // Group markers into separate paint batches. This is necessary to
    // draw all markers sharing the same style at once.
    for (let marker of markers) {
      // Again skip over markers that we're filtering -- we don't want them
      // to be labeled as "Unknown"
      if (!MarkerBlueprintUtils.shouldDisplayMarker(marker, this._filter)) {
        continue;
      }

      let markerType = this._paintBatches.get(marker.name) ||
                                              this._paintBatches.get("UNKNOWN");
      markerType.batch.push(marker);
    }

    // Calculate each row's height, and the time-based scaling.

    let groupHeight = this.rowHeight * this._pixelRatio;
    let groupPadding = this.groupPadding * this._pixelRatio;
    let headerHeight = this.headerHeight * this._pixelRatio;
    let dataScale = this.dataScaleX = canvasWidth / duration;

    // Draw the header and overview background.

    ctx.fillStyle = this.headerBackgroundColor;
    ctx.fillRect(0, 0, canvasWidth, headerHeight);

    ctx.fillStyle = this.backgroundColor;
    ctx.fillRect(0, headerHeight, canvasWidth, canvasHeight);

    // Draw the alternating odd/even group backgrounds.

    ctx.fillStyle = this.alternatingBackgroundColor;
    ctx.beginPath();

    for (let i = 0; i < this._numberOfGroups; i += 2) {
      let top = headerHeight + i * groupHeight;
      ctx.rect(0, top, canvasWidth, groupHeight);
    }

    ctx.fill();

    // Draw the timeline header ticks.

    let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
    let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
    let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
    let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;

    let tickInterval = TickUtils.findOptimalTickInterval({
      ticksMultiple: OVERVIEW_HEADER_TICKS_MULTIPLE,
      ticksSpacingMin: OVERVIEW_HEADER_TICKS_SPACING_MIN,
      dataScale: dataScale
    });

    ctx.textBaseline = "middle";
    ctx.font = fontSize + "px " + fontFamily;
    ctx.fillStyle = this.headerTextColor;
    ctx.strokeStyle = this.headerTimelineStrokeColor;
    ctx.beginPath();

    for (let x = 0; x < canvasWidth; x += tickInterval) {
      let lineLeft = x;
      let textLeft = lineLeft + textPaddingLeft;
      let time = Math.round(x / dataScale);
      let label = ProfilerGlobal.L10N.getFormatStr("timeline.tick", time);
      ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
      ctx.moveTo(lineLeft, 0);
      ctx.lineTo(lineLeft, canvasHeight);
    }

    ctx.stroke();

    // Draw the timeline markers.

    for (let [, { definition, batch }] of this._paintBatches) {
      let group = this._groupMap[definition.group];
      let top = headerHeight + group * groupHeight + groupPadding / 2;
      let height = groupHeight - groupPadding;

      let color = getColor(definition.colorName, this.theme);
      ctx.fillStyle = color;
      ctx.beginPath();

      for (let { start, end } of batch) {
        let left = start * dataScale;
        let width = Math.max((end - start) * dataScale, OVERVIEW_MARKER_WIDTH_MIN);
        ctx.rect(left, top, width, height);
      }

      ctx.fill();

      // Since all the markers in this batch (thus sharing the same style) have
      // been drawn, empty it. The next time new markers will be available,
      // they will be sorted and drawn again.
      batch.length = 0;
    }

    return canvas;
  },

  /**
   * Sets the theme via `theme` to either "light" or "dark",
   * and updates the internal styling to match. Requires a redraw
   * to see the effects.
   */
  setTheme: function (theme) {
    this.theme = theme = theme || "light";
    this.backgroundColor = getColor("body-background", theme);
    this.selectionBackgroundColor = colorUtils.setAlpha(
      getColor("selection-background", theme), 0.25);
    this.selectionStripesColor = colorUtils.setAlpha("#fff", 0.1);
    this.headerBackgroundColor = getColor("body-background", theme);
    this.headerTextColor = getColor("body-color", theme);
    this.headerTimelineStrokeColor = colorUtils.setAlpha(
      getColor("body-color-alt", theme), 0.25);
    this.alternatingBackgroundColor = colorUtils.setAlpha(
      getColor("body-color", theme), 0.05);
  }
});

exports.MarkersOverview = MarkersOverview;