1const { TouchBarScrubber } = require("electron");
2
3// Default range: 0 to 300s, shared between both views
4var RANGE_LEFT_INIT = 0;
5var RANGE_RIGHT_INIT = 300;
6
7// Global timeline start
8var g_StartingSec = undefined;
9
10function ShouldShowDebugInfo() {
11  if (g_cb_debug_info.checked) return true;
12  else return false;
13}
14
15function GetHistoryHistogram() {
16  return HistoryHistogram;
17}
18
19function RenderHistogramForImageData(ctx, key) {
20  let PAD = 1,   // To make up for the extra stroke width
21      PAD2 = 2;  // To preserve some space at both ends of the histogram
22
23  let cumDensity0 = 0, cumDensity1 = 0;
24
25  //      Left normalized index  Left value  Right normalized index, Right value
26  let threshEntry = [[undefined, undefined], [undefined, undefined]];
27  const x = 0, y = 0, w = HISTOGRAM_W, h = HISTOGRAM_H;
28  let hist = GetHistoryHistogram()[key];
29  if (hist == undefined) return;
30
31  let buckets = hist[2];
32  let dw = w * 1.0 / buckets.length;
33  let maxCount = 0, totalCount = 0;
34  for (let i = 0; i < buckets.length; i++) {
35    if (maxCount < buckets[i]) {
36      maxCount = buckets[i];
37    }
38    totalCount += buckets[i];
39  }
40  ctx.fillStyle = '#FFF';
41  ctx.fillRect(x, y, w, h);
42
43  ctx.strokeStyle = '#AAA';
44  ctx.fillStyle = '#000';
45  ctx.lineWidth = 1;
46  ctx.strokeRect(x + PAD, y + PAD, w - 2 * PAD, h - 2 * PAD);
47  for (let i = 0; i < buckets.length; i++) {
48    const bucketsLen = buckets.length;
49    if (buckets[i] > 0) {
50      let dx0 = x + PAD2 + (w - 2 * PAD2) * 1.0 * i / buckets.length,
51          dx1 = x + PAD2 + (w - 2 * PAD2) * 1.0 * (i + 1) / buckets.length,
52          dy0 = y + h - h * 1.0 * buckets[i] / maxCount, dy1 = y + h;
53      let delta_density = buckets[i] / totalCount;
54      cumDensity0 = cumDensity1;
55      cumDensity1 += delta_density;
56
57      // Write thresholds
58      if (cumDensity0 < HISTOGRAM_LEFT_TAIL_WIDTH &&
59          cumDensity1 >= HISTOGRAM_LEFT_TAIL_WIDTH) {
60        threshEntry[0][0] = i / buckets.length;
61        threshEntry[0][1] = hist[0] + (hist[1] - hist[0]) / bucketsLen * i;
62      }
63      if (cumDensity0 < 1 - HISTOGRAM_RIGHT_TAIL_WIDTH &&
64          cumDensity1 >= 1 - HISTOGRAM_RIGHT_TAIL_WIDTH) {
65        threshEntry[1][0] = (i - 1) / buckets.length;
66        threshEntry[1][1] =
67            hist[0] + (hist[1] - hist[0]) / bucketsLen * (i - 1);
68      }
69
70      ctx.fillRect(dx0, dy0, dx1 - dx0, dy1 - dy0);
71    }
72  }
73
74  // Mark the threshold regions
75  ctx.fillStyle = 'rgba(0,255,0,0.1)';
76  let dx = x + PAD2;
77  dw = (w - 2 * PAD2) * 1.0 * threshEntry[0][0];
78  ctx.fillRect(dx, y, dw, h);
79
80  ctx.fillStyle = 'rgba(255,0,0,0.1)';
81  ctx.beginPath();
82  dx = x + PAD2 + (w - 2 * PAD2) * 1.0 * threshEntry[1][0];
83  dw = (w - 2 * PAD2) * 1.0 * (1 - threshEntry[1][0]);
84  ctx.fillRect(dx, y, dw, h);
85
86  IsCanvasDirty = true;
87  return [ctx.getImageData(x, y, w, h), threshEntry];
88}
89
90function RenderHistogram(ctx, key, xMid, yMid) {
91  if (GetHistoryHistogram()[key] == undefined) {
92    return;
93  }
94  if (IpmiVizHistogramImageData[key] == undefined) {
95    return;
96  }
97  let hist = GetHistoryHistogram()[key];
98  ctx.putImageData(
99      IpmiVizHistogramImageData[key], xMid - HISTOGRAM_W / 2,
100      yMid - HISTOGRAM_H / 2);
101
102  let ub = '';  // Upper bound label
103  ctx.textAlign = 'left';
104  ctx.fillStyle = '#000';
105  if (hist[1] > 1000) {
106    ub = (hist[1] / 1000.0).toFixed(1) + 'ms';
107  } else {
108    ub = hist[1].toFixed(1) + 'us';
109  }
110  ctx.fillText(ub, xMid + HISTOGRAM_W / 2, yMid);
111
112  let lb = '';  // Lower bound label
113  if (hist[0] > 1000) {
114    lb = (hist[0] / 1000.0).toFixed(1) + 'ms';
115  } else {
116    lb = hist[0].toFixed(1) + 'us';
117  }
118  ctx.textAlign = 'right';
119  ctx.textBaseline = 'middle';
120  ctx.fillText(lb, xMid - HISTOGRAM_W / 2, yMid);
121}
122
123// A TimelineView contains data that has already gone through
124// the Layout step and is ready for showing
125class TimelineView {
126  constructor() {
127    this.Intervals = [];
128    this.Titles = [];  // { "header":true|false, "title":string, "intervals_idxes":[int] }
129    this.Heights = [];  // Visual height for each line
130    this.HeaderCollapsed = {};
131    this.TitleProperties = []; // [Visual height, Is Header]
132    this.LowerBoundTime = RANGE_LEFT_INIT;
133    this.UpperBoundTime = RANGE_RIGHT_INIT;
134    this.LowerBoundTimeTarget = this.LowerBoundTime;
135    this.UpperBoundTimeTarget = this.UpperBoundTime;
136    this.LastTimeLowerBound = 0;
137    this.LastTimeUpperBound = 0;
138    this.IsCanvasDirty = true;
139    this.IsHighlightDirty = true;
140    this.IsAnimating = false;
141    this.IpmiVizHistogramImageData = {};
142    this.IpmiVizHistHighlighted = {};
143    this.HighlightedRequests = [];
144    this.Canvas = undefined;
145    this.TitleDispLengthLimit = 32;  // display this many chars for title
146    this.IsTimeDistributionEnabled = false;
147    this.AccentColor = '#000';
148    this.CurrentFileName = '';
149    this.VisualLineStartIdx = 0;
150
151    // For connecting to the data model
152    this.GroupBy = [];
153    this.GroupByStr = '';
154
155    // For keyboard navigation
156    this.CurrDeltaX = 0;
157    this.CurrDeltaZoom = 0;
158    this.CurrShiftFlag = false;
159    this.MouseState = {
160      hovered: true,
161      pressed: false,
162      x: 0,
163      y: 0,
164      hoveredVisibleLineIndex: -999,
165      hoveredSide: undefined,  // 'left', 'right', 'scroll', 'timeline'
166      drag_begin_title_start_idx: undefined,
167      drag_begin_y: undefined,
168      IsDraggingScrollBar: function() {
169        return (this.drag_begin_y != undefined);
170      },
171      EndDragScrollBar: function() {
172        this.drag_begin_y = undefined;
173        this.drag_begin_title_start_idx = undefined;
174      },
175      IsHoveredOverHorizontalScrollbar: function() {
176        if (this.hoveredSide == "top_horizontal_scrollbar") return true;
177        else if (this.hoveredSide == "bottom_horizontal_scrollbar") return true;
178        else return false;
179      }
180    };
181    this.ScrollBarState = {
182      y0: undefined,
183      y1: undefined,
184    };
185    this.HighlightedRegion = {t0: -999, t1: -999};
186
187    // The linked view will move and zoom with this view
188    this.linked_views = [];
189  }
190
191  // Performs layout operation, move overlapping intervals to different
192  // lines
193  LayoutForOverlappingIntervals() {
194    this.Heights = [];
195    const MAX_STACK = 10; // Stack level limit: 10, arbitrarily chosen
196
197    for (let i=0; i<this.Titles.length; i++) {
198      let last_x = {};
199      let ymax = 0;
200
201      const title_data = this.Titles[i];
202
203      const intervals_idxes = title_data.intervals_idxes;
204
205      // TODO: What happens if there are > 1
206      if (title_data.header == false) {
207        const line = this.Intervals[intervals_idxes[0]];
208
209        for (let j=0; j<line.length; j++) {
210          const entry = line[j];
211          let y = 0;
212          for (; y<MAX_STACK; y++) {
213            if (!(y in last_x)) { break; }
214            if (last_x[y] <= entry[0]) {
215              break;
216            }
217          }
218
219          const end_time = entry[1];
220          if (end_time != undefined && !isNaN(end_time)) {
221            last_x[y] = end_time;
222          } else {
223            last_x[y] = entry[0];
224          }
225          entry[4] = y;
226          ymax = Math.max(y, ymax);
227        }
228      } else if (intervals_idxes.length == 0) {
229        // Don't do anything, set height to 1
230      }
231      this.Heights.push(ymax+1);
232    }
233  }
234
235  TotalVisualHeight() {
236    let ret = 0;
237    this.Heights.forEach((h) => {
238      ret += h;
239    })
240    return ret;
241  }
242
243  // Returns [Index, Offset]
244  VisualLineIndexToDataLineIndex(x) {
245    if (this.Heights.length < 1) return undefined;
246    let lb = 0, ub = this.Heights[0]-1;
247    for (let i=0; i<this.Heights.length; i++) {
248      ub = lb + this.Heights[i] - 1;
249      if (lb <= x && ub >= x) {
250        return [i, x-lb];
251      }
252      lb = ub+1;
253    }
254    return -999;
255  }
256
257  IsEmpty() {
258    return (this.Intervals.length < 1);
259  }
260
261  GetTitleWidthLimit() {
262    if (this.IsTimeDistributionEnabled == true) {
263      return 32;
264    } else {
265      return 64;
266    }
267  }
268
269  ToLines(t, limit) {
270    let ret = [];
271    for (let i = 0; i < t.length; i += limit) {
272      let j = Math.min(i + limit, t.length);
273      ret.push(t.substr(i, j));
274    }
275    return ret;
276  }
277
278  Zoom(dz, mid = undefined, iter = 1) {
279    if (this.CurrShiftFlag) dz *= 2;
280    if (dz != 0) {
281      if (mid == undefined) {
282        mid = (this.LowerBoundTime + this.UpperBoundTime) / 2;
283      }
284      this.LowerBoundTime = mid - (mid - this.LowerBoundTime) * (1 - dz);
285      this.UpperBoundTime = mid + (this.UpperBoundTime - mid) * (1 - dz);
286      this.IsCanvasDirty = true;
287      this.IsAnimating = false;
288    }
289
290    if (iter > 0) {
291      this.linked_views.forEach(function(v) {
292        v.Zoom(dz, mid, iter - 1);
293      });
294    }
295  }
296
297  BeginZoomAnimation(dz, mid = undefined, iter = 1) {
298    if (mid == undefined) {
299      mid = (this.LowerBoundTime + this.UpperBoundTime) / 2;
300    }
301    this.LowerBoundTimeTarget = mid - (mid - this.LowerBoundTime) * (1 - dz);
302    this.UpperBoundTimeTarget = mid + (this.UpperBoundTime - mid) * (1 - dz);
303    this.IsCanvasDirty = true;
304    this.IsAnimating = true;
305
306    if (iter > 0) {
307      this.linked_views.forEach(function(v) {
308        v.BeginZoomAnimation(dz, mid, iter - 1);
309      });
310    }
311  }
312
313  BeginPanScreenAnimaton(delta_screens, iter = 1) {
314    let deltat = (this.UpperBoundTime - this.LowerBoundTime) * delta_screens;
315    this.BeginSetBoundaryAnimation(
316        this.LowerBoundTime + deltat, this.UpperBoundTime + deltat);
317
318    if (iter > 0) {
319      this.linked_views.forEach(function(v) {
320        v.BeginPanScreenAnimaton(delta_screens, iter - 1);
321      });
322    }
323  }
324
325  BeginSetBoundaryAnimation(lt, rt, iter = 1) {
326    this.IsAnimating = true;
327    this.LowerBoundTimeTarget = lt;
328    this.UpperBoundTimeTarget = rt;
329
330    if (iter > 0) {
331      this.linked_views.forEach(function(v) {
332        v.BeginSetBoundaryAnimation(lt, rt, iter - 1);
333      });
334    }
335  }
336
337  BeginWarpToRequestAnimation(req, iter = 1) {
338    let mid_new = (req[0] + req[1]) / 2;
339    let mid = (this.LowerBoundTime + this.UpperBoundTime) / 2;
340    let lt = this.LowerBoundTime + (mid_new - mid);
341    let rt = this.UpperBoundTime + (mid_new - mid);
342    this.BeginSetBoundaryAnimation(lt, rt, 0);
343
344    this.linked_views.forEach(function(v) {
345      v.BeginSetBoundaryAnimation(lt, rt, 0);
346    });
347  }
348
349  UpdateAnimation() {
350    const EPS = 1e-3;
351    if (Math.abs(this.LowerBoundTime - this.LowerBoundTimeTarget) < EPS &&
352        Math.abs(this.UpperBoundTime - this.UpperBoundTimeTarget) < EPS) {
353      this.LowerBoundTime = this.LowerBoundTimeTarget;
354      this.UpperBoundTime = this.UpperBoundTimeTarget;
355      this.IsAnimating = false;
356    }
357    if (this.IsAnimating) {
358      let t = 0.80;
359      this.LowerBoundTime =
360          this.LowerBoundTime * t + this.LowerBoundTimeTarget * (1 - t);
361      this.UpperBoundTime =
362          this.UpperBoundTime * t + this.UpperBoundTimeTarget * (1 - t);
363      this.IsCanvasDirty = true;
364    }
365  }
366
367  IsHighlighted() {
368    return (
369        this.HighlightedRegion.t0 != -999 && this.HighlightedRegion.t1 != -999);
370  }
371
372  RenderHistogram(ctx, key, xMid, yMid) {
373    if (GetHistoryHistogram()[key] == undefined) {
374      return;
375    }
376    if (this.IpmiVizHistogramImageData[key] == undefined) {
377      return;
378    }
379    let hist = GetHistoryHistogram()[key];
380    ctx.putImageData(
381        this.IpmiVizHistogramImageData[key], xMid - HISTOGRAM_W / 2,
382        yMid - HISTOGRAM_H / 2);
383
384    let ub = '';  // Upper bound label
385    ctx.textAlign = 'left';
386    ctx.fillStyle = '#000';
387    if (hist[1] > 1000) {
388      ub = (hist[1] / 1000.0).toFixed(1) + 'ms';
389    } else {
390      ub = hist[1].toFixed(1) + 'us';
391    }
392    ctx.fillText(ub, xMid + HISTOGRAM_W / 2, yMid);
393
394    let lb = '';  // Lower bound label
395    if (hist[0] > 1000) {
396      lb = (hist[0] / 1000.0).toFixed(1) + 'ms';
397    } else {
398      lb = hist[0].toFixed(1) + 'us';
399    }
400    ctx.textAlign = 'right';
401    ctx.textBaseline = 'middle';
402    ctx.fillText(lb, xMid - HISTOGRAM_W / 2, yMid);
403  }
404
405  IsMouseOverTimeline() {
406    return this.MouseState.x > LEFT_MARGIN;
407  }
408
409  MouseXToTimestamp(x) {
410    let ret = (x - LEFT_MARGIN) / (RIGHT_MARGIN - LEFT_MARGIN) *
411            (this.UpperBoundTime - this.LowerBoundTime) +
412        this.LowerBoundTime;
413    ret = Math.max(ret, this.LowerBoundTime);
414    ret = Math.min(ret, this.UpperBoundTime);
415    return ret;
416  }
417
418  Unhighlight() {
419    this.HighlightedRegion.t0 = -999;
420    this.HighlightedRegion.t1 = -999;
421  }
422
423  OnMouseMove() {
424    // Drag gestures
425    if (this.MouseState.pressed == true) {
426      const h = this.MouseState.hoveredSide;
427      if (h == 'timeline') {
428        // Update highlighted area
429        this.HighlightedRegion.t1 =
430          this.MouseXToTimestamp(this.MouseState.x);
431      }
432    }
433
434    const PAD = 2;
435    if (this.MouseState.x < LEFT_MARGIN)
436      this.MouseState.hovered = false;
437    else if (this.MouseState.x > RIGHT_MARGIN)
438      this.MouseState.hovered = false;
439    else
440      this.MouseState.hovered = true;
441
442    this.IsCanvasDirty = true;
443    let lineIndex =
444        Math.floor((this.MouseState.y - YBEGIN + TEXT_Y0) / LINE_SPACING);
445
446    if (this.MouseState.x <= 0 ||
447        this.MouseState.x >= RIGHT_MARGIN) {
448      lineIndex = undefined;
449    }
450
451    const old_hoveredSide = this.MouseState.hoveredSide;
452
453    // Left/right overflow markers or time axis drag
454    this.MouseState.hoveredVisibleLineIndex = -999;
455    if (this.MouseState.hoveredSide != "scrollbar" &&
456        this.MouseState.pressed == false) {
457      if (lineIndex != undefined) {
458        this.MouseState.hoveredVisibleLineIndex = lineIndex;
459
460        let should_hide_cursor = false;  // Should we hide the vertical cursor for linked views?
461
462        if (this.MouseState.x <= PAD + LINE_SPACING / 2 + LEFT_MARGIN &&
463            this.MouseState.x >= PAD + LEFT_MARGIN) {
464          this.MouseState.hoveredSide = 'left';
465          this.IsCanvasDirty = true;
466        } else if (
467            this.MouseState.x <= RIGHT_MARGIN - PAD &&
468            this.MouseState.x >= RIGHT_MARGIN - PAD - LINE_SPACING / 2) {
469          this.MouseState.hoveredSide = 'right';
470          this.IsCanvasDirty = true;
471        } else if (this.MouseState.x >= PAD + LEFT_MARGIN &&
472                   this.MouseState.y <= TOP_HORIZONTAL_SCROLLBAR_HEIGHT &&
473                   this.MouseState.y >  0) {
474          this.MouseState.hoveredVisibleLineIndex = undefined;
475          this.MouseState.hoveredSide = 'top_horizontal_scrollbar';
476        } else if (this.MouseState.x >= PAD + LEFT_MARGIN &&
477                   this.MouseState.y >= this.Canvas.height - BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT &&
478                   this.MouseState.y <= this.Canvas.height) {
479          this.MouseState.hoveredVisibleLineIndex = undefined;
480          this.MouseState.hoveredSide = 'bottom_horizontal_scrollbar';
481        } else {
482          this.MouseState.hoveredSide = undefined;
483        }
484      }
485    }
486
487    // During a dragging session
488    if (this.MouseState.pressed == true) {
489
490      if (this.MouseState.hoveredSide == "top_horizontal_scrollbar" ||
491          this.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
492        const sec_per_px = (this.MouseState.begin_UpperBoundTime - this.MouseState.begin_LowerBoundTime) / (RIGHT_MARGIN - LEFT_MARGIN);
493        const pan_secs = (this.MouseState.x - this.MouseState.begin_drag_x) * sec_per_px;
494
495        const new_lb = this.MouseState.begin_LowerBoundTime - pan_secs;
496        const new_ub = this.MouseState.begin_UpperBoundTime - pan_secs;
497        this.LowerBoundTime = new_lb;
498        this.UpperBoundTime = new_ub;
499
500        // Sync to all other views
501        this.linked_views.forEach((v) => {
502          v.LowerBoundTime = new_lb; v.UpperBoundTime = new_ub;
503        })
504      }
505
506      const tvh = this.TotalVisualHeight();
507      if (this.MouseState.hoveredSide == 'scrollbar') {
508        const diff_y = this.MouseState.y - this.MouseState.drag_begin_y;
509        const diff_title_idx = tvh * diff_y / this.Canvas.height;
510        let new_title_start_idx = this.MouseState.drag_begin_title_start_idx + parseInt(diff_title_idx);
511        if (new_title_start_idx < 0) { new_title_start_idx = 0; }
512        else if (new_title_start_idx >= tvh) {
513          new_title_start_idx = tvh - 1;
514        }
515        this.VisualLineStartIdx = new_title_start_idx;
516      }
517    }
518  }
519
520  OnMouseLeave() {
521    // When dragging the scroll bar, allow mouse to temporarily leave the element since we only
522    // care about delta Y
523    if (this.MouseState.hoveredSide == 'scrollbar') {
524
525    } else {
526      this.MouseState.hovered = false;
527      this.MouseState.hoveredSide = undefined;
528      this.IsCanvasDirty = true;
529      this.MouseState.hoveredVisibleLineIndex = undefined;
530      this.MouseState.y = undefined;
531      this.MouseState.x = undefined;
532    }
533  }
534
535  // Assume event.button is zero (left mouse button)
536  OnMouseDown(iter = 1) {
537    // If hovering over an overflowing triangle, warp to the nearest overflowed
538    //     request on that line
539    if (this.MouseState.hoveredVisibleLineIndex >= 0 &&
540        this.MouseState.hoveredVisibleLineIndex < this.Intervals.length &&
541        this.MouseState.hoveredSide != undefined) {
542      const x = this.VisualLineIndexToDataLineIndex(this.MouseState.hoveredVisibleLineIndex);
543      if (x == undefined) return;
544      const line = this.Intervals[x[0]];
545      if (this.MouseState.hoveredSide == 'left') {
546        for (let i = line.length - 1; i >= 0; i--) {
547          if (line[i][1] <= this.LowerBoundTime) {
548            this.BeginWarpToRequestAnimation(line[i]);
549            // TODO: pass timeline X to linked view
550            break;
551          }
552        }
553      } else if (this.MouseState.hoveredSide == 'right') {
554        for (let i = 0; i < line.length; i++) {
555          if (line[i][0] >= this.UpperBoundTime) {
556            // TODO: pass timeline X to linked view
557            this.BeginWarpToRequestAnimation(line[i]);
558            break;
559          }
560        }
561      }
562    }
563
564    let tx = this.MouseXToTimestamp(this.MouseState.x);
565    let t0 = Math.min(this.HighlightedRegion.t0, this.HighlightedRegion.t1),
566        t1 = Math.max(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
567    if (this.MouseState.x > LEFT_MARGIN) {
568
569      // If clicking on the horizontal scroll bar, start panning the viewport
570      if (this.MouseState.hoveredSide == "top_horizontal_scrollbar" ||
571          this.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
572        this.MouseState.pressed = true;
573        this.MouseState.begin_drag_x = this.MouseState.x;
574        this.MouseState.begin_LowerBoundTime = this.LowerBoundTime;
575        this.MouseState.begin_UpperBoundTime = this.UpperBoundTime;
576      } else if (tx >= t0 && tx <= t1) {
577        // If clicking inside highlighted area, zoom around the area
578        this.BeginSetBoundaryAnimation(t0, t1);
579        this.Unhighlight();
580        this.IsCanvasDirty = true;
581
582        this.linked_views.forEach(function(v) {
583          v.BeginSetBoundaryAnimation(t0, t1, 0);
584          v.Unhighlight();
585          v.IsCanvasDirty = false;
586        });
587      } else {  // If in the timeline area, start a new dragging action
588        this.MouseState.hoveredSide = 'timeline';
589        this.MouseState.pressed = true;
590        this.HighlightedRegion.t0 = this.MouseXToTimestamp(this.MouseState.x);
591        this.HighlightedRegion.t1 = this.HighlightedRegion.t0;
592        this.IsCanvasDirty = true;
593      }
594    } else if (this.MouseState.x < SCROLL_BAR_WIDTH) {  // Todo: draagging the scroll bar
595      const THRESH = 4;
596      if (this.MouseState.y >= this.ScrollBarState.y0 - THRESH &&
597          this.MouseState.y <= this.ScrollBarState.y1 + THRESH) {
598        this.MouseState.pressed = true;
599        this.MouseState.drag_begin_y = this.MouseState.y;
600        this.MouseState.drag_begin_title_start_idx = this.VisualLineStartIdx;
601        this.MouseState.hoveredSide = 'scrollbar';
602      }
603    }
604
605    // Collapse or expand a "header"
606    if (this.MouseState.x < LEFT_MARGIN &&
607        this.MouseState.hoveredVisibleLineIndex != undefined) {
608      const x = this.VisualLineIndexToDataLineIndex(this.VisualLineStartIdx + this.MouseState.hoveredVisibleLineIndex);
609      if (x != undefined) {
610        const tidx = x[0];
611        if (this.Titles[tidx] != undefined && this.Titles[tidx].header == true) {
612
613          // Currently, only DBus pane supports column headers, so we can hard-code the DBus re-group function (rather than to figure out which pane we're in)
614          this.HeaderCollapsed[this.Titles[tidx].title] = !(this.HeaderCollapsed[this.Titles[tidx].title]);
615          OnGroupByConditionChanged_DBus();
616        }
617      }
618    }
619  }
620
621  // Assume event.button == 0 (left mouse button)
622  OnMouseUp() {
623    this.MouseState.EndDragScrollBar();
624    this.MouseState.pressed = false;
625    this.IsCanvasDirty = true;
626    this.UnhighlightIfEmpty();
627    this.IsHighlightDirty = true;
628    this.MouseState.hoveredSide = undefined;
629  }
630
631  UnhighlightIfEmpty() {
632    if (this.HighlightedRegion.t0 == this.HighlightedRegion.t1) {
633      this.Unhighlight();
634      this.IsCanvasDirty = true;
635      return true;
636    } else
637      return false;
638  }
639
640  OnMouseWheel(event) {
641    event.preventDefault();
642    const v = this;
643
644    let is_mouse_on_horizontal_scrollbar = false;
645    if (this.MouseState.y > 0 && this.MouseState.y < TOP_HORIZONTAL_SCROLLBAR_HEIGHT)
646      is_mouse_on_horizontal_scrollbar = true;
647    if (this.MouseState.y > this.Canvas.height - BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT &&
648        this.MouseState.y < this.Canvas.height)
649      is_mouse_on_horizontal_scrollbar = true;
650
651    if (/*v.IsMouseOverTimeline()*/ is_mouse_on_horizontal_scrollbar) {
652      let dz = 0;
653      if (event.deltaY > 0) {  // Scroll down, zoom out
654        dz = -0.3;
655      } else if (event.deltaY < 0) {  // Scroll up, zoom in
656        dz = 0.3;
657      }
658      v.Zoom(dz, v.MouseXToTimestamp(v.MouseState.x));
659    } else {
660      if (event.deltaY > 0) {
661        v.ScrollY(1);
662      } else if (event.deltaY < 0) {
663        v.ScrollY(-1);
664      }
665    }
666  }
667
668  ScrollY(delta) {
669    this.VisualLineStartIdx += delta;
670    if (this.VisualLineStartIdx < 0) {
671      this.VisualLineStartIdx = 0;
672    } else if (this.VisualLineStartIdx >= this.TotalVisualHeight()) {
673      this.VisualLineStartIdx = this.TotalVisualHeight() - 1;
674    }
675  }
676
677  // This function is called in Render to draw a line of Intervals.
678  // It is made into its own function for brevity in Render().
679  // It depends on too much context so it doesn't look very clean though
680  do_RenderIntervals(ctx, intervals_j, j, dy0, dy1,
681    data_line_idx, visual_line_offset_within_data_line,
682    isAggregateSelection,
683    vars) {
684    // To reduce the number of draw calls while preserve the accuracy in
685    // the visual presentation, combine rectangles that are within 1 pixel
686    // into one
687    let last_dx_begin = LEFT_MARGIN;
688    let last_dx_end = LEFT_MARGIN;
689
690    for (let i = 0; i < intervals_j.length; i++) {
691      let lb = intervals_j[i][0], ub = intervals_j[i][1];
692      const yoffset = intervals_j[i][4];
693      if (yoffset != visual_line_offset_within_data_line)
694        continue;
695      if (lb > ub)
696        continue;  // Unmatched (only enter & no exit timestamp)
697
698      let isHighlighted = false;
699      let durationUsec =
700          (intervals_j[i][1] - intervals_j[i][0]) * 1000000;
701      let lbub = [lb, ub];
702      if (this.IsHighlighted()) {
703        if (IsIntersected(lbub, vars.highlightedInterval)) {
704          vars.numIntersected++;
705          isHighlighted = true;
706          vars.currHighlightedReqs.push(intervals_j[i][2]);
707        }
708      }
709
710      if (ub < this.LowerBoundTime) {
711        vars.numOverflowEntriesToTheLeft++;
712        continue;
713      }
714      if (lb > this.UpperBoundTime) {
715        vars.numOverflowEntriesToTheRight++;
716        continue;
717      }
718      // Failed request
719      if (ub == undefined && lb < this.UpperBoundTime) {
720        vars.numOverflowEntriesToTheLeft++;
721        continue;
722      }
723
724      let dx0 = MapXCoord(
725              lb, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
726              this.UpperBoundTime),
727          dx1 = MapXCoord(
728              ub, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
729              this.UpperBoundTime);
730
731      dx0 = Math.max(dx0, LEFT_MARGIN);
732      dx1 = Math.min(dx1, RIGHT_MARGIN);
733      let dw = Math.max(0, dx1 - dx0);
734
735      if (isHighlighted) {
736        ctx.fillStyle = 'rgba(128,128,255,0.5)';
737        ctx.fillRect(dx0, dy0, dw, dy1 - dy0);
738      }
739
740      let isCurrentReqHovered = false;
741      // Intersect with mouse using pixel coordinates
742
743      // When the mouse position is within 4 pixels distance from an entry, consider
744      // the mouse to be over that entry and show the information popup
745      const X_TOLERANCE = 4;
746
747      if (vars.theHoveredReq == undefined &&
748          IsIntersectedPixelCoords(
749              [dx0 - X_TOLERANCE, dx0 + dw + X_TOLERANCE],
750              [this.MouseState.x, this.MouseState.x]) &&
751          IsIntersectedPixelCoords(
752              [dy0, dy1], [this.MouseState.y, this.MouseState.y])) {
753        ctx.fillStyle = 'rgba(255,255,0,0.5)';
754        ctx.fillRect(dx0, dy0, dw, dy1 - dy0);
755        vars.theHoveredReq = intervals_j[i][2];
756        vars.theHoveredInterval = intervals_j[i];
757        isCurrentReqHovered = true;
758      }
759
760      ctx.lineWidth = 0.5;
761
762
763      // If this request is taking too long/is quick enough, use red/green
764      let entry = HistogramThresholds[this.Titles[data_line_idx].title];
765
766      let isError = false;
767      if (intervals_j[i][3] == 'error') {
768        isError = true;
769      }
770
771      if (entry != undefined) {
772        if (entry[0][1] != undefined && durationUsec < entry[0][1]) {
773          ctx.strokeStyle = '#0F0';
774        } else if (
775            entry[1][1] != undefined && durationUsec > entry[1][1]) {
776          ctx.strokeStyle = '#A00';
777        } else {
778          ctx.strokeStyle = '#000';
779        }
780      } else {
781        ctx.strokeStyle = '#000';
782      }
783
784      const duration = intervals_j[i][1] - intervals_j[i][0];
785      if (!isNaN(duration)) {
786        if (isError) {
787          ctx.fillStyle = 'rgba(192, 128, 128, 0.8)';
788          ctx.fillRect(dx0, dy0, dw, dy1 - dy0);
789          ctx.strokeStyle = 'rgba(192, 128, 128, 1)';
790        } else {
791          ctx.fillStyle = undefined;
792          ctx.strokeStyle = '#000';
793        }
794
795        // This keeps track of the current "cluster" of requests
796        // that might visually overlap (i.e less than 1 pixel wide).
797        // This can greatly reduce overdraw and keep render time under
798        // a reasonable bound.
799        if (!ShouldShowDebugInfo()) {
800          if (dx0+dw - last_dx_begin > 1 ||
801              i == intervals_j.length - 1) {
802            ctx.strokeRect(last_dx_begin, dy0,
803              /*dx0+dw-last_dx_begin*/
804              last_dx_end - last_dx_begin, // At least 1 pixel wide
805              dy1-dy0);
806            last_dx_begin = dx0;
807          }
808        } else {
809          ctx.strokeRect(dx0, dy0, dw, dy1 - dy0);
810        }
811        last_dx_end = dx0 + dw;
812        this.numVisibleRequests++;
813      } else {
814        // This entry has only a beginning and not an end
815        // perhaps the original method call did not return
816        if (isCurrentReqHovered) {
817          ctx.fillStyle = 'rgba(192, 192, 0, 0.8)';
818        } else {
819          ctx.fillStyle = 'rgba(255, 128, 128, 0.8)';
820        }
821        ctx.beginPath();
822        ctx.arc(dx0, (dy0 + dy1) / 2, HISTOGRAM_H * 0.17, 0, 2 * Math.PI);
823        ctx.fill();
824      }
825
826
827      // Affects whether this req will be reflected in the aggregate info
828      //     section
829      if ((isAggregateSelection == false) ||
830          (isAggregateSelection == true && isHighlighted == true)) {
831        if (!isNaN(duration)) {
832          vars.numVisibleRequestsCurrLine++;
833          vars.totalSecsCurrLine += duration;
834        } else {
835          vars.numFailedRequestsCurrLine++;
836        }
837
838        // If a histogram exists for Titles[j], process the highlighted
839        //     histogram buckets
840        if (GetHistoryHistogram()[this.Titles[data_line_idx].title] != undefined) {
841          let histogramEntry = GetHistoryHistogram()[this.Titles[data_line_idx].title];
842          let bucketInterval = (histogramEntry[1] - histogramEntry[0]) /
843              histogramEntry[2].length;
844          let bucketIndex =
845              Math.floor(
846                  (durationUsec - histogramEntry[0]) / bucketInterval) /
847              histogramEntry[2].length;
848
849          if (this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title] == undefined) {
850            this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title] = new Set();
851          }
852          let entry = this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title];
853          entry.add(bucketIndex);
854        }
855      }
856    }  // end for (i=0 to interval_j.length-1)
857
858    if (!ShouldShowDebugInfo()) {
859      ctx.strokeRect(last_dx_begin, dy0,
860        /*dx0+dw-last_dx_begin*/
861        last_dx_end - last_dx_begin, // At least 1 pixel wide
862        dy1-dy0);
863    }
864  }
865
866  // For the header:
867  do_RenderHeader(ctx, header, j, dy0, dy1,
868    data_line_idx, visual_line_offset_within_data_line,
869    isAggregateSelection,
870    vars) {
871
872    const dy = (dy0+dy1) / 2;
873    ctx.fillStyle = "rgba(192,192,255, 1)";
874
875    ctx.strokeStyle = "rgba(192,192,255, 1)"
876
877    const title_text = header.title + " (" + header.intervals_idxes.length + ")";
878    let skip_render = false;
879
880    ctx.save();
881
882    if (this.HeaderCollapsed[header.title] == false) {  // Expanded
883      const x0 = LEFT_MARGIN - LINE_HEIGHT;
884      ctx.fillRect(0, dy-LINE_HEIGHT/2, x0, LINE_HEIGHT);
885
886      ctx.beginPath();
887      ctx.moveTo(x0, dy0);
888      ctx.lineTo(x0, dy1);
889      ctx.lineTo(x0 + LINE_HEIGHT, dy1);
890      ctx.fill();
891      ctx.closePath();
892
893      ctx.beginPath();
894      ctx.lineWidth = 1.5;
895      ctx.moveTo(0, dy1);
896      ctx.lineTo(RIGHT_MARGIN, dy1);
897      ctx.stroke();
898      ctx.closePath();
899
900      ctx.fillStyle = '#003';
901      ctx.textBaseline = 'center';
902      ctx.textAlign = 'right';
903      ctx.fillText(title_text, LEFT_MARGIN - LINE_HEIGHT, dy);
904
905      // Don't draw the timelines so visual clutter is reduced
906      skip_render = true;
907    } else {
908      const x0 = LEFT_MARGIN - LINE_HEIGHT / 2;
909      ctx.fillRect(0, dy-LINE_HEIGHT/2, x0, LINE_HEIGHT);
910
911      ctx.beginPath();
912      ctx.lineWidth = 1.5;
913      ctx.moveTo(x0, dy0);
914      ctx.lineTo(x0 + LINE_HEIGHT/2, dy);
915      ctx.lineTo(x0, dy1);
916      ctx.closePath();
917      ctx.fill();
918
919      /*
920      ctx.beginPath();
921      ctx.moveTo(0, dy);
922      ctx.lineTo(RIGHT_MARGIN, dy);
923      ctx.stroke();
924      ctx.closePath();
925      */
926
927      ctx.fillStyle = '#003';
928      ctx.textBaseline = 'center';
929      ctx.textAlign = 'right';
930      ctx.fillText(title_text, LEFT_MARGIN - LINE_HEIGHT, dy);
931    }
932
933    ctx.fillStyle = "rgba(160,120,255,0.8)";
934
935    ctx.restore();
936
937    // Draw the merged intervals
938    // Similar to drawing the actual messages in do_Render(), but no collision detection against the mouse, and no hovering tooltip processing involved
939    const merged_intervals = header.merged_intervals;
940    let dxx0 = undefined, dxx1 = undefined;
941    for (let i=0; i<merged_intervals.length; i++) {
942      const lb = merged_intervals[i][0], ub = merged_intervals[i][1], weight = merged_intervals[i][2];
943      let duration = ub-lb;
944      let duration_usec = duration * 1000000;
945      const lbub = [lb, ub];
946
947      let isHighlighted = false;
948      if (this.IsHighlighted()) {
949        if (IsIntersected(lbub, vars.highlightedInterval)) {
950          vars.numIntersected += weight;
951          isHighlighted = true;
952        }
953      }
954
955      if (ub < this.LowerBoundTime) continue;
956      if (lb > this.UpperBoundTime) continue;
957
958      // Render only if collapsed
959      if (!skip_render) {
960        let dx0 = MapXCoord(
961          lb, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
962          this.UpperBoundTime),
963            dx1 = MapXCoord(
964          ub, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
965          this.UpperBoundTime);
966        dx0 = Math.max(dx0, LEFT_MARGIN);
967        dx1 = Math.min(dx1, RIGHT_MARGIN);
968        let dw = Math.max(1, dx1 - dx0);  // At least 1 pixel wide during rendering
969
970        // Draw this interval
971        //ctx.fillRect(dx0, dy0, dw, dy1-dy0);
972        if (dxx0 == undefined || dxx1 == undefined) {
973          dxx0 = dx0;
974        }
975
976        const MERGE_THRESH = 0.5;  // Pixels
977
978        let should_draw = true;
979        if (dxx1 == undefined || dx0 < dxx1 + MERGE_THRESH) should_draw = false;
980        if (i == merged_intervals.length - 1) {
981          should_draw = true;
982          dxx1 = dx1 + MERGE_THRESH;
983        }
984
985        if (should_draw) {
986          //console.log(dxx0 + ", " + dy0 + ", " + (dx1-dxx0) + ", " + LINE_HEIGHT);
987          ctx.fillRect(dxx0, dy0, dxx1-dxx0, LINE_HEIGHT);
988          dxx0 = undefined; dxx1 = undefined;
989        } else {
990          // merge
991          dxx1 = dx1 + MERGE_THRESH;
992        }
993      }
994
995      if ((isAggregateSelection == false) ||
996          (isAggregateSelection == true && isHighlighted == true)) {
997        vars.totalSecsCurrLine += duration;
998        vars.numVisibleRequestsCurrLine += weight;
999      }
1000    }
1001  }
1002
1003  Render(ctx) {
1004    // Wait for initialization
1005    if (this.Canvas == undefined) return;
1006
1007    // Update
1008    let toFixedPrecision = 2;
1009    const extent = this.UpperBoundTime - this.LowerBoundTime;
1010    {
1011      if (extent < 0.1) {
1012        toFixedPrecision = 4;
1013      } else if (extent < 1) {
1014        toFixedPrecision = 3;
1015      }
1016    }
1017
1018    let dx = this.CurrDeltaX;
1019    if (dx != 0) {
1020      if (this.CurrShiftFlag) dx *= 5;
1021      this.LowerBoundTime += dx * extent;
1022      this.UpperBoundTime += dx * extent;
1023      this.IsCanvasDirty = true;
1024    }
1025
1026    // Hovered interval for display
1027    let theHoveredReq = undefined;
1028    let theHoveredInterval = undefined;
1029    let currHighlightedReqs = [];
1030
1031    let dz = this.CurrDeltaZoom;
1032    this.Zoom(dz);
1033    this.UpdateAnimation();
1034
1035    this.LastTimeLowerBound = this.LowerBoundTime;
1036    this.LastTimeUpperBound = this.UpperBoundTime;
1037
1038    if (this.IsCanvasDirty) {
1039      this.IsCanvasDirty = false;
1040      // Shorthand for HighlightedRegion.t{0,1}
1041      let t0 = undefined, t1 = undefined;
1042
1043      // Highlight
1044      let highlightedInterval = [];
1045      let numIntersected =
1046          0;  // How many requests intersect with highlighted area
1047      if (this.IsHighlighted()) {
1048        t0 = Math.min(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
1049        t1 = Math.max(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
1050        highlightedInterval = [t0, t1];
1051      }
1052      this.IpmiVizHistHighlighted = {};
1053
1054      const width = this.Canvas.width;
1055      const height = this.Canvas.height;
1056
1057      ctx.globalCompositeOperation = 'source-over';
1058      ctx.clearRect(0, 0, width, height);
1059      ctx.strokeStyle = '#000';
1060      ctx.fillStyle = '#000';
1061      ctx.lineWidth = 1;
1062
1063      ctx.font = '12px Monospace';
1064
1065      // Highlight current line
1066      if (this.MouseState.hoveredVisibleLineIndex != undefined) {
1067        const hv_lidx = this.MouseState.hoveredVisibleLineIndex + this.VisualLineStartIdx;
1068        if (hv_lidx >= 0 &&
1069            hv_lidx < this.Titles.length) {
1070          ctx.fillStyle = 'rgba(32,32,32,0.2)';
1071          let dy = YBEGIN + LINE_SPACING * this.MouseState.hoveredVisibleLineIndex -
1072              LINE_SPACING / 2;
1073          ctx.fillRect(0, dy, RIGHT_MARGIN, LINE_SPACING);
1074        }
1075      }
1076
1077      // Draw highlighted background over time labels when the mouse is hovering over
1078      // the time axis
1079      ctx.fillStyle = "#FF9";
1080      if (this.MouseState.hoveredSide == "top_horizontal_scrollbar") {
1081        ctx.fillRect(LEFT_MARGIN, 0, RIGHT_MARGIN-LEFT_MARGIN, TOP_HORIZONTAL_SCROLLBAR_HEIGHT);
1082      } else if (this.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
1083        ctx.fillRect(LEFT_MARGIN, height-BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT, RIGHT_MARGIN-LEFT_MARGIN, BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT);
1084      }
1085
1086      ctx.fillStyle = '#000';
1087      // Time marks at the beginning & end of the visible range
1088      ctx.textBaseline = 'bottom';
1089      ctx.textAlign = 'left';
1090      ctx.fillText(
1091          '' + this.LowerBoundTime.toFixed(toFixedPrecision) + 's',
1092          LEFT_MARGIN + 3, height);
1093      ctx.textAlign = 'end';
1094      ctx.fillText(
1095          '' + this.UpperBoundTime.toFixed(toFixedPrecision) + 's',
1096          RIGHT_MARGIN - 3, height);
1097
1098      ctx.textBaseline = 'top';
1099      ctx.textAlign = 'left';
1100      ctx.fillText(
1101          '' + this.LowerBoundTime.toFixed(toFixedPrecision) + 's',
1102          LEFT_MARGIN + 3, TEXT_Y0);
1103      ctx.textAlign = 'right';
1104      ctx.fillText(
1105          '' + this.UpperBoundTime.toFixed(toFixedPrecision) + 's',
1106          RIGHT_MARGIN - 3, TEXT_Y0);
1107
1108      let y = YBEGIN;
1109      let numVisibleRequests = 0;
1110
1111      ctx.beginPath();
1112      ctx.moveTo(LEFT_MARGIN, 0);
1113      ctx.lineTo(LEFT_MARGIN, height);
1114      ctx.stroke();
1115
1116      ctx.beginPath();
1117      ctx.moveTo(RIGHT_MARGIN, 0);
1118      ctx.lineTo(RIGHT_MARGIN, height);
1119      ctx.stroke();
1120
1121      // Column Titles
1122      ctx.fillStyle = '#000';
1123      let columnTitle = '(All requests)';
1124      if (this.GroupByStr.length > 0) {
1125        columnTitle = this.GroupByStr;
1126      }
1127      ctx.textAlign = 'right';
1128      ctx.textBaseline = 'top';
1129      // Split into lines
1130      {
1131        let lines = this.ToLines(columnTitle, this.TitleDispLengthLimit)
1132        for (let i = 0; i < lines.length; i++) {
1133          ctx.fillText(lines[i], LEFT_MARGIN - 3, 3 + i * LINE_HEIGHT);
1134        }
1135      }
1136
1137      if (this.IsTimeDistributionEnabled) {
1138        // "Histogram" title
1139        ctx.fillStyle = '#000';
1140        ctx.textBaseline = 'top';
1141        ctx.textAlign = 'center';
1142        ctx.fillText('Time Distribution', HISTOGRAM_X, TEXT_Y0);
1143
1144        ctx.textAlign = 'right'
1145        ctx.fillText('In dataset /', HISTOGRAM_X, TEXT_Y0 + LINE_SPACING - 2);
1146
1147        ctx.fillStyle = '#00F';
1148
1149        ctx.textAlign = 'left'
1150        if (this.IsHighlighted()) {
1151          ctx.fillText(
1152              ' In selection', HISTOGRAM_X, TEXT_Y0 + LINE_SPACING - 2);
1153        }
1154        else {
1155          ctx.fillText(' In viewport', HISTOGRAM_X, TEXT_Y0 + LINE_SPACING - 2);
1156        }
1157      }
1158
1159      ctx.fillStyle = '#000';
1160
1161      // Time Axis Breaks
1162      const breakWidths = [
1163        86400,  10800,  3600,    1800,    1200,   600,   300,   120,
1164        60,     30,     10,      5,       2,      1,     0.5,   0.2,
1165        0.1,    0.05,   0.02,    0.01,    0.005,  0.002, 0.001, 0.0005,
1166        0.0002, 0.0001, 0.00005, 0.00002, 0.00001
1167      ];
1168      const BreakDrawLimit = 1000;  // Only draw up to this many grid lines
1169
1170      let bidx = 0;
1171      while (bidx < breakWidths.length &&
1172             breakWidths[bidx] > this.UpperBoundTime - this.LowerBoundTime) {
1173        bidx++;
1174      }
1175      let breakWidth = breakWidths[bidx + 1];
1176      if (bidx < breakWidths.length) {
1177        let t2 = 0;  // Cannot name as "t0" otherwise clash
1178        bidx = 0;
1179        while (bidx < breakWidths.length) {
1180          while (t2 + breakWidths[bidx] < this.LowerBoundTime) {
1181            t2 += breakWidths[bidx];
1182          }
1183          if (t2 + breakWidths[bidx] >= this.LowerBoundTime &&
1184              t2 + breakWidths[bidx] <= this.UpperBoundTime) {
1185            break;
1186          }
1187          bidx++;
1188        }
1189        let draw_count = 0;
1190        if (bidx < breakWidths.length) {
1191          for (; t2 < this.UpperBoundTime; t2 += breakWidth) {
1192            if (t2 > this.LowerBoundTime) {
1193              ctx.beginPath();
1194              let dx = MapXCoord(
1195                  t2, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
1196                  this.UpperBoundTime);
1197              ctx.strokeStyle = '#C0C0C0';
1198              ctx.moveTo(dx, 0);
1199              ctx.lineTo(dx, height);
1200              ctx.stroke();
1201              ctx.closePath();
1202              ctx.fillStyle = '#C0C0C0';
1203
1204              ctx.textAlign = 'left';
1205              ctx.textBaseline = 'bottom';
1206              let label2 = t2.toFixed(toFixedPrecision) + 's';
1207              let w = ctx.measureText(label2).width;
1208              if (dx + w > RIGHT_MARGIN) ctx.textAlign = 'right';
1209              ctx.fillText(label2, dx, height);
1210
1211              ctx.textBaseline = 'top';
1212              ctx.fillText(label2, dx, TEXT_Y0);
1213
1214              draw_count++;
1215              if (draw_count > BreakDrawLimit) break;
1216            }
1217          }
1218        }
1219      }
1220
1221      // Whether we aggregate selected requests or visible requests
1222      let isAggregateSelection = false;
1223      if (this.IsHighlighted()) isAggregateSelection = true;
1224      let numVisibleRequestsPerLine = {}; // DataLineIndex -> Count
1225      let numFailedRequestsPerLine = {};
1226      let totalSecondsPerLine = {};
1227
1228      // Range of Titles that were displayed
1229      let title_start_idx = this.VisualLineStartIdx, title_end_idx = title_start_idx;
1230
1231      const tvh = this.TotalVisualHeight();
1232
1233      // This is used to handle Intervals that have overlapping entries
1234      let last_data_line_idx = -999;//this.VisualLineIndexToDataLineIndex(this.VisualLineStartIdx);
1235
1236      // 'j' iterates over the "visual rows" that need to be displayed.
1237      // A "visual row" might be one of:
1238      // 1. a "header" line
1239      // 2. an actual row of data (in the Intervals variable)
1240      for (let j = this.VisualLineStartIdx; j < tvh; j++) {
1241        const tmp = this.VisualLineIndexToDataLineIndex(j);
1242        if (tmp == undefined) break;
1243        const data_line_idx = tmp[0];
1244        const visual_line_offset_within_data_line = tmp[1];
1245
1246        const is_different_data_line = (data_line_idx != last_data_line_idx);
1247        last_data_line_idx = data_line_idx;
1248
1249        if (is_different_data_line && data_line_idx != -999) { // Only draw line title and histogram per data line index not visual line index
1250          ctx.textBaseline = 'middle';
1251          ctx.textAlign = 'right';
1252          let desc_width = 0;
1253          if (NetFnCmdToDescription[this.Titles[data_line_idx].title] != undefined) {
1254            let desc = ' (' + NetFnCmdToDescription[this.Titles[data_line_idx].title] + ')';
1255            desc_width = ctx.measureText(desc).width;
1256            ctx.fillStyle = '#888';  // Grey
1257            ctx.fillText(desc, LEFT_MARGIN - 3, y);
1258          }
1259
1260
1261          // Plot histogram
1262          if (this.IsTimeDistributionEnabled == true) {
1263            const t = this.Titles[data_line_idx].title;
1264            if (GetHistoryHistogram()[t] != undefined) {
1265              if (this.IpmiVizHistogramImageData[t] == undefined) {
1266                let tmp = RenderHistogramForImageData(ctx, t);
1267                this.IpmiVizHistogramImageData[t] = tmp[0];
1268                HistogramThresholds[t] = tmp[1];
1269              }
1270              this.RenderHistogram(ctx, t, HISTOGRAM_X, y);
1271              ctx.textAlignment = 'right';
1272            } else {
1273            }
1274          }
1275
1276          // If is HEADER: do not draw here, darw in do_RenderHeader()
1277          if (this.Titles[data_line_idx].header == false) {
1278            ctx.textAlignment = 'right';
1279            ctx.textBaseline = 'middle';
1280            ctx.fillStyle = '#000000';  // Revert to Black
1281            ctx.strokeStyle = '#000000';
1282            let tj_draw = this.Titles[data_line_idx].title;
1283            const title_disp_length_limit = this.GetTitleWidthLimit();
1284            if (tj_draw != undefined && tj_draw.length > title_disp_length_limit) {
1285              tj_draw = tj_draw.substr(0, title_disp_length_limit) + '...'
1286            }
1287            ctx.fillText(tj_draw, LEFT_MARGIN - 3 - desc_width, y);
1288          }
1289        } else if (is_different_data_line && data_line_idx == -999) {
1290          continue;
1291        }
1292
1293        let numOverflowEntriesToTheLeft = 0;  // #entries below the lower bound
1294        let numOverflowEntriesToTheRight =
1295            0;                               // #entries beyond the upper bound
1296        let numVisibleRequestsCurrLine = 0;  // #entries visible
1297        let totalSecsCurrLine = 0;           // Total duration in seconds
1298        let numFailedRequestsCurrLine = 0;
1299
1300        const intervals_idxes = this.Titles[data_line_idx].intervals_idxes;
1301
1302        let intervals_j = undefined;
1303        if (intervals_idxes.length == 1) {
1304          intervals_j = this.Intervals[intervals_idxes[0]];
1305        }
1306
1307        // Draw the contents in the set of intervals
1308        // The drawing method depends on whether this data line is a header or not
1309
1310        // Save the context for reference for the rendering routines
1311        let vars = {
1312          "theHoveredReq": theHoveredReq,
1313          "theHoveredInterval": theHoveredInterval,
1314          "numIntersected": numIntersected,
1315          "numOverflowEntriesToTheLeft": numOverflowEntriesToTheLeft,
1316          "numOverflowEntriesToTheRight": numOverflowEntriesToTheRight,
1317          "currHighlightedReqs": currHighlightedReqs,
1318          "totalSecondsPerLine": totalSecondsPerLine,
1319          "highlightedInterval": highlightedInterval,
1320          "numVisibleRequestsCurrLine": numVisibleRequestsCurrLine,
1321          "totalSecsCurrLine": totalSecsCurrLine,
1322        }  // Emulate a reference
1323
1324        let dy0 = y - LINE_HEIGHT / 2, dy1 = y + LINE_HEIGHT / 2;
1325        if (this.Titles[data_line_idx].header == false) {
1326          if (intervals_j != undefined) {
1327            this.do_RenderIntervals(ctx, intervals_j, j, dy0, dy1,
1328              data_line_idx, visual_line_offset_within_data_line, isAggregateSelection, vars);
1329          }
1330        } else {
1331          this.do_RenderHeader(ctx, this.Titles[data_line_idx],
1332            j, dy0, dy1,
1333            data_line_idx, visual_line_offset_within_data_line, isAggregateSelection, vars);
1334        }
1335
1336        // Update the context variables with updated values
1337        theHoveredReq = vars.theHoveredReq;
1338        theHoveredInterval = vars.theHoveredInterval;
1339        numIntersected = vars.numIntersected;
1340        numOverflowEntriesToTheLeft = vars.numOverflowEntriesToTheLeft;
1341        numOverflowEntriesToTheRight = vars.numOverflowEntriesToTheRight;
1342        currHighlightedReqs = vars.currHighlightedReqs;
1343        totalSecondsPerLine = vars.totalSecondsPerLine;
1344        highlightedInterval = vars.highlightedInterval;
1345        numVisibleRequestsCurrLine = vars.numVisibleRequestsCurrLine;
1346        totalSecsCurrLine = vars.totalSecsCurrLine;
1347
1348        // Triangle markers for entries outside of the viewport
1349        {
1350          const PAD = 2, H = LINE_SPACING;
1351          if (this.MouseState.hoveredVisibleLineIndex + this.VisualLineStartIdx == data_line_idx &&
1352              this.MouseState.hoveredSide == 'left') {
1353            ctx.fillStyle = '#0000FF';
1354          } else {
1355            ctx.fillStyle = 'rgba(128,128,0,0.5)';
1356          }
1357          if (numOverflowEntriesToTheLeft > 0) {
1358            ctx.beginPath();
1359            ctx.moveTo(LEFT_MARGIN + PAD + H / 2, y - H / 2);
1360            ctx.lineTo(LEFT_MARGIN + PAD, y);
1361            ctx.lineTo(LEFT_MARGIN + PAD + H / 2, y + H / 2);
1362            ctx.closePath();
1363            ctx.fill();
1364            ctx.textAlign = 'left';
1365            ctx.textBaseline = 'center';
1366            ctx.fillText(
1367                '+' + numOverflowEntriesToTheLeft,
1368                LEFT_MARGIN + 2 * PAD + H / 2, y);
1369          }
1370
1371          if (this.MouseState.hoveredVisibleLineIndex + this.VisualLineStartIdx == j &&
1372              this.MouseState.hoveredSide == 'right') {
1373            ctx.fillStyle = '#0000FF';
1374          } else {
1375            ctx.fillStyle = 'rgba(128,128,0,0.5)';
1376          }
1377          if (numOverflowEntriesToTheRight > 0) {
1378            ctx.beginPath();
1379            ctx.moveTo(RIGHT_MARGIN - PAD - H / 2, y - H / 2);
1380            ctx.lineTo(RIGHT_MARGIN - PAD, y);
1381            ctx.lineTo(RIGHT_MARGIN - PAD - H / 2, y + H / 2);
1382            ctx.closePath();
1383            ctx.fill();
1384            ctx.textAlign = 'right';
1385            ctx.fillText(
1386                '+' + numOverflowEntriesToTheRight,
1387                RIGHT_MARGIN - 2 * PAD - H / 2, y);
1388          }
1389        }
1390        y = y + LINE_SPACING;
1391
1392        numVisibleRequestsPerLine[data_line_idx] = numVisibleRequestsCurrLine;
1393        numFailedRequestsPerLine[data_line_idx] = numFailedRequestsCurrLine;
1394        totalSecondsPerLine[data_line_idx] = totalSecsCurrLine;
1395
1396        title_end_idx = j;
1397        if (y > height) break;
1398      }
1399
1400      {
1401        let nbreaks = this.TotalVisualHeight();
1402        // Draw a scroll bar on the left
1403        if (!(title_start_idx == 0 && title_end_idx == nbreaks - 1)) {
1404
1405          const y0 = title_start_idx * height / nbreaks;
1406          const y1 = (1 + title_end_idx) * height / nbreaks;
1407
1408          let highlighted = false;
1409          const THRESH = 8;
1410          if (this.MouseState.IsDraggingScrollBar()) {
1411            highlighted = true;
1412          }
1413          this.ScrollBarState.highlighted = highlighted;
1414
1415          // If not dragging, let title_start_idx drive y0 and y1, else let the
1416          // user's input drive y0 and y1 and title_start_idx
1417          if (!this.MouseState.IsDraggingScrollBar()) {
1418            this.ScrollBarState.y0 = y0;
1419            this.ScrollBarState.y1 = y1;
1420          }
1421
1422          if (highlighted) {
1423            ctx.fillStyle = "#FF3";
1424          } else {
1425            ctx.fillStyle = this.AccentColor;
1426          }
1427          ctx.fillRect(0, y0, SCROLL_BAR_WIDTH, y1 - y0);
1428
1429        } else {
1430          this.ScrollBarState.y0 = undefined;
1431          this.ScrollBarState.y1 = undefined;
1432          this.ScrollBarState.highlighted = false;
1433        }
1434      }
1435
1436      // Draw highlighted sections for the histograms
1437      if (this.IsTimeDistributionEnabled) {
1438        y = YBEGIN;
1439        for (let j = this.TitleStartIdx; j < this.Intervals.length; j++) {
1440          if (this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title] != undefined) {
1441            let entry = HistogramThresholds[this.Titles[data_line_idx].title];
1442            const theSet =
1443                Array.from(this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title]);
1444            for (let i = 0; i < theSet.length; i++) {
1445              bidx = theSet[i];
1446              if (entry != undefined) {
1447                if (bidx < entry[0][0]) {
1448                  if (bidx < 0) {
1449                    bidx = 0;
1450                  }
1451                  ctx.fillStyle = 'rgba(0, 255, 0, 0.3)';
1452                } else if (bidx > entry[1][0]) {
1453                  if (bidx > 1) {
1454                    bidx = 1;
1455                  }
1456                  ctx.fillStyle = 'rgba(255,0,0,0.3)';
1457                } else {
1458                  ctx.fillStyle = 'rgba(0,0,255,0.3)';
1459                }
1460              } else {
1461                ctx.fillStyle = 'rgba(0,0,255,0.3)';
1462              }
1463              const dx = HISTOGRAM_X - HISTOGRAM_W / 2 + HISTOGRAM_W * bidx;
1464
1465              const r = HISTOGRAM_H * 0.17;
1466              ctx.beginPath();
1467              ctx.ellipse(dx, y, r, r, 0, 0, 3.14159 * 2);
1468              ctx.fill();
1469            }
1470          }
1471          y += LINE_SPACING;
1472        }
1473      }
1474
1475      // Render number of visible requests versus totals
1476      ctx.textAlign = 'left';
1477      ctx.textBaseline = 'top';
1478      let totalOccs = 0, totalSecs = 0;
1479      if (this.IsHighlighted()) {
1480        ctx.fillStyle = '#00F';
1481        ctx.fillText('# / time', 3, TEXT_Y0);
1482        ctx.fillText('in selection', 3, TEXT_Y0 + LINE_SPACING - 2);
1483      } else {
1484        ctx.fillStyle = '#000';
1485        ctx.fillText('# / time', 3, TEXT_Y0);
1486        ctx.fillText('in viewport', 3, TEXT_Y0 + LINE_SPACING - 2);
1487      }
1488
1489      let timeDesc = '';
1490      ctx.textBaseline = 'middle';
1491      last_data_line_idx = -999;
1492
1493      for (let j = this.VisualLineStartIdx, i = 0;
1494               j < tvh && (YBEGIN + i*LINE_SPACING)<height; j++, i++) {
1495        const x = this.VisualLineIndexToDataLineIndex(j);
1496        if (x == undefined) break;
1497        const data_line_idx = x[0];
1498        if (data_line_idx == undefined) break;
1499        if (data_line_idx != last_data_line_idx) {
1500          let y1 = YBEGIN + LINE_SPACING * (i);
1501          let totalSeconds = totalSecondsPerLine[data_line_idx];
1502          if (totalSeconds < 1) {
1503            timeDesc = (totalSeconds * 1000.0).toFixed(toFixedPrecision) + 'ms';
1504          } else {
1505            timeDesc = totalSeconds.toFixed(toFixedPrecision) + 's';
1506          }
1507
1508          const n0 = numVisibleRequestsPerLine[data_line_idx];
1509          const n1 = numFailedRequestsPerLine[data_line_idx];
1510          let txt = '';
1511          if (n1 > 0) {
1512            txt = '' + n0 + '+' + n1 + ' / ' + timeDesc;
1513          } else {
1514            txt = '' + n0 + ' / ' + timeDesc;
1515          }
1516
1517          const tw = ctx.measureText(txt).width;
1518          const PAD = 8;
1519
1520          ctx.fillStyle = '#000';
1521          ctx.fillText(txt, 3, y1);
1522          totalOccs += numVisibleRequestsPerLine[data_line_idx];
1523          totalSecs += totalSeconds;
1524        }
1525        last_data_line_idx = data_line_idx;
1526      }
1527
1528      // This does not get displayed correctly, so disabling for now
1529      //timeDesc = '';
1530      //if (totalSecs < 1) {
1531      //  timeDesc = '' + (totalSecs * 1000).toFixed(toFixedPrecision) + 'ms';
1532      //} else {
1533      //  timeDesc = '' + totalSecs.toFixed(toFixedPrecision) + 's';
1534      //}
1535
1536      //ctx.fillText('Sum:', 3, y + 2 * LINE_SPACING);
1537      //ctx.fillText('' + totalOccs + ' / ' + timeDesc, 3, y + 3 * LINE_SPACING);
1538
1539      // Update highlighted requests
1540      if (this.IsHighlightDirty) {
1541        this.HighlightedRequests = currHighlightedReqs;
1542        this.IsHighlightDirty = false;
1543
1544        // Todo: This callback will be different for the DBus pane
1545        OnHighlightedChanged(HighlightedRequests);
1546      }
1547
1548      // Render highlight statistics
1549      if (this.IsHighlighted()) {
1550        ctx.fillStyle = 'rgba(128,128,255,0.5)';
1551        let x0 = MapXCoord(
1552            t0, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
1553            this.UpperBoundTime);
1554        let x1 = MapXCoord(
1555            t1, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
1556            this.UpperBoundTime);
1557        ctx.fillRect(x0, 0, x1 - x0, height);
1558
1559        let label0 = '' + t0.toFixed(toFixedPrecision) + 's';
1560        let label1 = '' + t1.toFixed(toFixedPrecision) + 's';
1561        let width0 = ctx.measureText(label0).width;
1562        let width1 = ctx.measureText(label1).width;
1563        let dispWidth = x1 - x0;
1564        // Draw time marks outside or inside?
1565        ctx.fillStyle = '#0000FF';
1566        ctx.textBaseline = 'top';
1567        if (dispWidth > width0 + width1) {
1568          ctx.textAlign = 'left';
1569          ctx.fillText(label0, x0, LINE_SPACING + TEXT_Y0);
1570          ctx.textAlign = 'right';
1571          ctx.fillText(label1, x1, LINE_SPACING + TEXT_Y0);
1572        } else {
1573          ctx.textAlign = 'right';
1574          ctx.fillText(label0, x0, LINE_SPACING + TEXT_Y0);
1575          ctx.textAlign = 'left';
1576          ctx.fillText(label1, x1, LINE_SPACING + TEXT_Y0);
1577        }
1578
1579        // This was calculated earlier
1580        ctx.textAlign = 'center';
1581        label1 = 'Duration: ' + (t1 - t0).toFixed(toFixedPrecision) + 's';
1582        ctx.fillText(label1, (x0 + x1) / 2, height - LINE_SPACING * 2);
1583      }
1584
1585      // Hovering cursor
1586      // Only draw when the mouse is not over any hotizontal scroll bar
1587      let should_hide_cursor = false;
1588
1589      if (this.MouseState.hoveredSide == "top_horizontal_scrollbar" ||
1590          this.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
1591        should_hide_cursor = true;
1592      }
1593      this.linked_views.forEach((v) => {
1594        if (v.MouseState.hoveredSide == "top_horizontal_scrollbar" ||
1595            v.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
1596          should_hide_cursor = true;
1597        }
1598      })
1599
1600      if (this.MouseState.hovered == true &&
1601          this.MouseState.hoveredSide == undefined &&
1602          should_hide_cursor == false) {
1603        ctx.beginPath();
1604        ctx.strokeStyle = '#0000FF';
1605        ctx.lineWidth = 1;
1606        if (this.IsHighlighted()) {
1607          ctx.moveTo(this.MouseState.x, 0);
1608          ctx.lineTo(this.MouseState.x, height);
1609        } else {
1610          ctx.moveTo(this.MouseState.x, LINE_SPACING * 2);
1611          ctx.lineTo(this.MouseState.x, height - LINE_SPACING * 2);
1612        }
1613        ctx.stroke();
1614
1615        if (this.IsHighlighted() == false) {
1616          let dispWidth = this.MouseState.x - LEFT_MARGIN;
1617          let label = '' +
1618              this.MouseXToTimestamp(this.MouseState.x)
1619                  .toFixed(toFixedPrecision) +
1620              's';
1621          let width0 = ctx.measureText(label).width;
1622          ctx.fillStyle = '#0000FF';
1623          ctx.textBaseline = 'bottom';
1624          ctx.textAlign = 'center';
1625          ctx.fillText(label, this.MouseState.x, height - LINE_SPACING);
1626          ctx.textBaseline = 'top';
1627          ctx.fillText(label, this.MouseState.x, LINE_SPACING + TEXT_Y0);
1628        }
1629      }
1630
1631      // Tooltip box next to hovered entry
1632      if (theHoveredReq !== undefined) {
1633        this.RenderToolTip(
1634            ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height);
1635      }
1636    }  // End IsCanvasDirty
1637  }
1638};
1639
1640// The extended classes have their own way of drawing popups for hovered entries
1641class IPMITimelineView extends TimelineView {
1642  RenderToolTip(
1643      ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1644    if (theHoveredReq == undefined) {
1645      return;
1646    }
1647    const PAD = 2, DELTA_Y = 14;
1648
1649    let labels = [];
1650    let netFn = theHoveredReq[0];
1651    let cmd = theHoveredReq[1];
1652    let t0 = theHoveredInterval[0];
1653    let t1 = theHoveredInterval[1];
1654
1655    labels.push('Netfn and CMD : (' + netFn + ', ' + cmd + ')');
1656    let key = netFn + ', ' + cmd;
1657
1658    if (NetFnCmdToDescription[key] != undefined) {
1659      labels.push('Description   : ' + NetFnCmdToDescription[key]);
1660    }
1661
1662    if (theHoveredReq.offset != undefined) {
1663      labels.push('Offset      : ' + theHoveredReq.offset);
1664    }
1665
1666    let req = theHoveredReq[4];
1667    labels.push('Request Data  : ' + req.length + ' bytes');
1668    if (req.length > 0) {
1669      labels.push('Hex   : ' + ToHexString(req, '', ' '));
1670      labels.push('ASCII : ' + ToASCIIString(req));
1671    }
1672    let resp = theHoveredReq[5];
1673    labels.push('Response Data : ' + theHoveredReq[5].length + ' bytes');
1674    if (resp.length > 0) {
1675      labels.push('Hex   : ' + ToHexString(resp, '', ' '));
1676      labels.push('ASCII : ' + ToASCIIString(resp));
1677    }
1678    labels.push('Start         : ' + t0.toFixed(toFixedPrecision) + 's');
1679    labels.push('End           : ' + t1.toFixed(toFixedPrecision) + 's');
1680    labels.push('Duration      : ' + ((t1 - t0) * 1000).toFixed(3) + 'ms');
1681
1682
1683    let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1684    for (let i = 0; i < labels.length; i++) {
1685      w = Math.max(w, ctx.measureText(labels[i]).width);
1686    }
1687    let dy = this.MouseState.y + DELTA_Y;
1688    if (dy + h > height) {
1689      dy = height - h;
1690    }
1691    let dx = this.MouseState.x;
1692    if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1693
1694    ctx.fillStyle = 'rgba(0,0,0,0.5)';
1695    ctx.fillRect(dx, dy, w + 2 * PAD, h);
1696
1697    ctx.textAlign = 'left';
1698    ctx.textBaseline = 'middle';
1699    ctx.fillStyle = '#FFFFFF';
1700    for (let i = 0; i < labels.length; i++) {
1701      ctx.fillText(
1702          labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1703    }
1704  }
1705};
1706
1707class DBusTimelineView extends TimelineView {
1708  RenderToolTip(
1709      ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1710    if (theHoveredReq == undefined) {
1711      return;
1712    }
1713    const PAD = 2, DELTA_Y = 14;
1714
1715    let labels = [];
1716    let msg_type = theHoveredReq[0];
1717    let serial = theHoveredReq[2];
1718    let sender = theHoveredReq[3];
1719    let destination = theHoveredReq[4];
1720    let path = theHoveredReq[5];
1721    let iface = theHoveredReq[6];
1722    let member = theHoveredReq[7];
1723
1724    let t0 = theHoveredInterval[0];
1725    let t1 = theHoveredInterval[1];
1726
1727    labels.push('Message type: ' + msg_type);
1728    labels.push('Serial      : ' + serial);
1729    labels.push('Sender      : ' + sender);
1730    labels.push('Destination : ' + destination);
1731    labels.push('Path        : ' + path);
1732    labels.push('Interface   : ' + iface);
1733    labels.push('Member      : ' + member);
1734
1735    let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1736    for (let i = 0; i < labels.length; i++) {
1737      w = Math.max(w, ctx.measureText(labels[i]).width);
1738    }
1739    let dy = this.MouseState.y + DELTA_Y;
1740    if (dy + h > height) {
1741      dy = height - h;
1742    }
1743    let dx = this.MouseState.x;
1744    if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1745
1746    ctx.fillStyle = 'rgba(0,0,0,0.5)';
1747    ctx.fillRect(dx, dy, w + 2 * PAD, h);
1748
1749    ctx.textAlign = 'left';
1750    ctx.textBaseline = 'middle';
1751    ctx.fillStyle = '#FFFFFF';
1752    for (let i = 0; i < labels.length; i++) {
1753      ctx.fillText(
1754          labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1755    }
1756  }
1757};
1758
1759class BoostASIOHandlerTimelineView extends TimelineView {
1760  RenderToolTip(
1761      ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1762    if (theHoveredReq == undefined) {
1763      return;
1764    }
1765    const PAD = 2, DELTA_Y = 14;
1766
1767    let labels = [];
1768    let create_time = theHoveredReq[2];
1769    let enter_time = theHoveredReq[3];
1770    let exit_time = theHoveredReq[4];
1771    let desc = theHoveredReq[5];
1772
1773    let t0 = theHoveredInterval[0];
1774    let t1 = theHoveredInterval[1];
1775
1776    labels.push('Creation time: ' + create_time);
1777    labels.push('Entry time   : ' + enter_time);
1778    labels.push('Exit time    : ' + exit_time);
1779    labels.push('Creation->Entry : ' + (enter_time - create_time));
1780    labels.push('Entry->Exit     : ' + (exit_time - enter_time));
1781    labels.push('Description  : ' + desc);
1782
1783    let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1784    for (let i = 0; i < labels.length; i++) {
1785      w = Math.max(w, ctx.measureText(labels[i]).width);
1786    }
1787    let dy = this.MouseState.y + DELTA_Y;
1788    if (dy + h > height) {
1789      dy = height - h;
1790    }
1791    let dx = this.MouseState.x;
1792    if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1793
1794    ctx.fillStyle = 'rgba(0,0,0,0.5)';
1795    ctx.fillRect(dx, dy, w + 2 * PAD, h);
1796
1797    ctx.textAlign = 'left';
1798    ctx.textBaseline = 'middle';
1799    ctx.fillStyle = '#FFFFFF';
1800    for (let i = 0; i < labels.length; i++) {
1801      ctx.fillText(
1802          labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1803    }
1804  }
1805}
1806