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 undefined;
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    is_in_viewport) {
685    // To reduce the number of draw calls while preserve the accuracy in
686    // the visual presentation, combine rectangles that are within 1 pixel
687    // into one
688    let last_dx_begin = LEFT_MARGIN;
689    let last_dx_end = LEFT_MARGIN;
690
691    for (let i = 0; i < intervals_j.length; i++) {
692      let lb = intervals_j[i][0], ub = intervals_j[i][1];
693      const yoffset = intervals_j[i][4];
694      if (yoffset != visual_line_offset_within_data_line)
695        continue;
696      if (lb > ub)
697        continue;  // Unmatched (only enter & no exit timestamp)
698
699      let isHighlighted = false;
700      let durationUsec =
701          (intervals_j[i][1] - intervals_j[i][0]) * 1000000;
702      let lbub = [lb, ub];
703      if (this.IsHighlighted()) {
704        if (IsIntersected(lbub, vars.highlightedInterval)) {
705          vars.numIntersected++;
706          isHighlighted = true;
707          vars.currHighlightedReqs.push(intervals_j[i][2]);
708        }
709      }
710
711      if (ub < this.LowerBoundTime) {
712        vars.numOverflowEntriesToTheLeft++;
713        continue;
714      }
715      if (lb > this.UpperBoundTime) {
716        vars.numOverflowEntriesToTheRight++;
717        continue;
718      }
719      // Failed request
720      if (ub == undefined && lb < this.UpperBoundTime) {
721        vars.numOverflowEntriesToTheLeft++;
722        continue;
723      }
724
725      let dx0 = MapXCoord(
726              lb, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
727              this.UpperBoundTime),
728          dx1 = MapXCoord(
729              ub, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
730              this.UpperBoundTime);
731
732      dx0 = Math.max(dx0, LEFT_MARGIN);
733      dx1 = Math.min(dx1, RIGHT_MARGIN);
734      let dw = Math.max(0, dx1 - dx0);
735
736      if (isHighlighted && is_in_viewport) {
737        ctx.fillStyle = 'rgba(128,128,255,0.5)';
738        ctx.fillRect(dx0, dy0, dw, dy1 - dy0);
739      }
740
741      let isCurrentReqHovered = false;
742      // Intersect with mouse using pixel coordinates
743
744      // When the mouse position is within 4 pixels distance from an entry, consider
745      // the mouse to be over that entry and show the information popup
746      const X_TOLERANCE = 4;
747
748      if (vars.theHoveredReq == undefined &&
749          IsIntersectedPixelCoords(
750              [dx0 - X_TOLERANCE, dx0 + dw + X_TOLERANCE],
751              [this.MouseState.x, this.MouseState.x]) &&
752          IsIntersectedPixelCoords(
753              [dy0, dy1], [this.MouseState.y, this.MouseState.y])) {
754        ctx.fillStyle = 'rgba(255,255,0,0.5)';
755        if (is_in_viewport) ctx.fillRect(dx0, dy0, dw, dy1 - dy0);
756        vars.theHoveredReq = intervals_j[i][2];
757        vars.theHoveredInterval = intervals_j[i];
758        isCurrentReqHovered = true;
759      }
760
761      ctx.lineWidth = 0.5;
762
763
764      // If this request is taking too long/is quick enough, use red/green
765      let entry = HistogramThresholds[this.Titles[data_line_idx].title];
766
767      let isError = false;
768      if (intervals_j[i][3] == 'error') {
769        isError = true;
770      }
771
772      if (entry != undefined) {
773        if (entry[0][1] != undefined && durationUsec < entry[0][1]) {
774          ctx.strokeStyle = '#0F0';
775        } else if (
776            entry[1][1] != undefined && durationUsec > entry[1][1]) {
777          ctx.strokeStyle = '#A00';
778        } else {
779          ctx.strokeStyle = '#000';
780        }
781      } else {
782        ctx.strokeStyle = '#000';
783      }
784
785      const duration = intervals_j[i][1] - intervals_j[i][0];
786      if (!isNaN(duration)) {
787        if (is_in_viewport) {
788          if (isError) {
789            ctx.fillStyle = 'rgba(192, 128, 128, 0.8)';
790            ctx.fillRect(dx0, dy0, dw, dy1 - dy0);
791            ctx.strokeStyle = 'rgba(192, 128, 128, 1)';
792          } else {
793            ctx.fillStyle = undefined;
794            ctx.strokeStyle = '#000';
795          }
796        }
797
798        // This keeps track of the current "cluster" of requests
799        // that might visually overlap (i.e less than 1 pixel wide).
800        // This can greatly reduce overdraw and keep render time under
801        // a reasonable bound.
802        if (!ShouldShowDebugInfo()) {
803          if (dx0+dw - last_dx_begin > 1 ||
804              i == intervals_j.length - 1) {
805            if (is_in_viewport) {
806              ctx.strokeRect(last_dx_begin, dy0,
807                /*dx0+dw-last_dx_begin*/
808                last_dx_end - last_dx_begin, // At least 1 pixel wide
809                dy1-dy0);
810            }
811            last_dx_begin = dx0;
812          }
813        } else {
814          if (is_in_viewport) {
815            ctx.strokeRect(dx0, dy0, dw, dy1 - dy0);
816          }
817        }
818        last_dx_end = dx0 + dw;
819        this.numVisibleRequests++;
820      } else {
821        // This entry has only a beginning and not an end
822        // perhaps the original method call did not return
823        if (is_in_viewport) {
824          if (isCurrentReqHovered) {
825            ctx.fillStyle = 'rgba(192, 192, 0, 0.8)';
826          } else {
827            ctx.fillStyle = 'rgba(255, 128, 128, 0.8)';
828          }
829          ctx.beginPath();
830          ctx.arc(dx0, (dy0 + dy1) / 2, HISTOGRAM_H * 0.17, 0, 2 * Math.PI);
831          ctx.fill();
832        }
833      }
834
835
836      // Affects whether this req will be reflected in the aggregate info
837      //     section
838      if ((isAggregateSelection == false) ||
839          (isAggregateSelection == true && isHighlighted == true)) {
840        if (!isNaN(duration)) {
841          vars.numVisibleRequestsCurrLine++;
842          vars.totalSecsCurrLine += duration;
843        } else {
844          vars.numFailedRequestsCurrLine++;
845        }
846
847        // If a histogram exists for Titles[j], process the highlighted
848        //     histogram buckets
849        if (GetHistoryHistogram()[this.Titles[data_line_idx].title] != undefined) {
850          let histogramEntry = GetHistoryHistogram()[this.Titles[data_line_idx].title];
851          let bucketInterval = (histogramEntry[1] - histogramEntry[0]) /
852              histogramEntry[2].length;
853          let bucketIndex =
854              Math.floor(
855                  (durationUsec - histogramEntry[0]) / bucketInterval) /
856              histogramEntry[2].length;
857
858          if (this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title] == undefined) {
859            this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title] = new Set();
860          }
861          let entry = this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title];
862          entry.add(bucketIndex);
863        }
864      }
865    }  // end for (i=0 to interval_j.length-1)
866
867    if (!ShouldShowDebugInfo()) {
868      ctx.strokeRect(last_dx_begin, dy0,
869        /*dx0+dw-last_dx_begin*/
870        last_dx_end - last_dx_begin, // At least 1 pixel wide
871        dy1-dy0);
872    }
873  }
874
875  // For the header:
876  do_RenderHeader(ctx, header, j, dy0, dy1,
877    data_line_idx, visual_line_offset_within_data_line,
878    isAggregateSelection,
879    vars, is_in_viewport) {
880
881    const dy = (dy0+dy1) / 2;
882    ctx.fillStyle = "rgba(192,192,255, 1)";
883
884    ctx.strokeStyle = "rgba(192,192,255, 1)"
885
886    const title_text = header.title + " (" + header.intervals_idxes.length + ")";
887    let skip_render = false;
888
889    ctx.save();
890
891    if (this.HeaderCollapsed[header.title] == false) {  // Expanded
892      const x0 = LEFT_MARGIN - LINE_HEIGHT;
893      if (is_in_viewport) {
894        ctx.fillRect(0, dy-LINE_HEIGHT/2, x0, LINE_HEIGHT);
895
896        ctx.beginPath();
897        ctx.moveTo(x0, dy0);
898        ctx.lineTo(x0, dy1);
899        ctx.lineTo(x0 + LINE_HEIGHT, dy1);
900        ctx.fill();
901        ctx.closePath();
902
903        ctx.beginPath();
904        ctx.lineWidth = 1.5;
905        ctx.moveTo(0, dy1);
906        ctx.lineTo(RIGHT_MARGIN, dy1);
907        ctx.stroke();
908        ctx.closePath();
909
910        ctx.fillStyle = '#003';
911        ctx.textBaseline = 'center';
912        ctx.textAlign = 'right';
913        ctx.fillText(title_text, LEFT_MARGIN - LINE_HEIGHT, dy);
914      }
915
916      // Don't draw the timelines so visual clutter is reduced
917      skip_render = true;
918    } else {
919      const x0 = LEFT_MARGIN - LINE_HEIGHT / 2;
920      if (is_in_viewport) {
921        ctx.fillRect(0, dy-LINE_HEIGHT/2, x0, LINE_HEIGHT);
922
923        ctx.beginPath();
924        ctx.lineWidth = 1.5;
925        ctx.moveTo(x0, dy0);
926        ctx.lineTo(x0 + LINE_HEIGHT/2, dy);
927        ctx.lineTo(x0, dy1);
928        ctx.closePath();
929        ctx.fill();
930
931        /*
932        ctx.beginPath();
933        ctx.moveTo(0, dy);
934        ctx.lineTo(RIGHT_MARGIN, dy);
935        ctx.stroke();
936        ctx.closePath();
937        */
938
939        ctx.fillStyle = '#003';
940        ctx.textBaseline = 'center';
941        ctx.textAlign = 'right';
942        ctx.fillText(title_text, LEFT_MARGIN - LINE_HEIGHT, dy);
943      }
944    }
945
946    ctx.fillStyle = "rgba(160,120,255,0.8)";
947
948    ctx.restore();
949
950    // Draw the merged intervals
951    // Similar to drawing the actual messages in do_Render(), but no collision detection against the mouse, and no hovering tooltip processing involved
952    const merged_intervals = header.merged_intervals;
953    let dxx0 = undefined, dxx1 = undefined;
954    for (let i=0; i<merged_intervals.length; i++) {
955      const lb = merged_intervals[i][0], ub = merged_intervals[i][1], weight = merged_intervals[i][2];
956      let duration = ub-lb;
957      let duration_usec = duration * 1000000;
958      const lbub = [lb, ub];
959
960      let isHighlighted = false;
961      if (this.IsHighlighted()) {
962        if (IsIntersected(lbub, vars.highlightedInterval)) {
963          vars.numIntersected += weight;
964          isHighlighted = true;
965        }
966      }
967
968      if (ub < this.LowerBoundTime) continue;
969      if (lb > this.UpperBoundTime) continue;
970
971      // Render only if collapsed
972      if (!skip_render) {
973        let dx0 = MapXCoord(
974          lb, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
975          this.UpperBoundTime),
976            dx1 = MapXCoord(
977          ub, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
978          this.UpperBoundTime);
979        dx0 = Math.max(dx0, LEFT_MARGIN);
980        dx1 = Math.min(dx1, RIGHT_MARGIN);
981        let dw = Math.max(1, dx1 - dx0);  // At least 1 pixel wide during rendering
982
983        // Draw this interval
984        //ctx.fillRect(dx0, dy0, dw, dy1-dy0);
985        if (dxx0 == undefined || dxx1 == undefined) {
986          dxx0 = dx0;
987        }
988
989        const MERGE_THRESH = 0.5;  // Pixels
990
991        let should_draw = true;
992        if (dxx1 == undefined || dx0 < dxx1 + MERGE_THRESH) should_draw = false;
993        if (i == merged_intervals.length - 1) {
994          should_draw = true;
995          dxx1 = dx1 + MERGE_THRESH;
996        }
997
998        if (should_draw) {
999          //console.log(dxx0 + ", " + dy0 + ", " + (dx1-dxx0) + ", " + LINE_HEIGHT);
1000          if (is_in_viewport) {
1001            ctx.fillRect(dxx0, dy0, dxx1-dxx0, LINE_HEIGHT);
1002          }
1003          dxx0 = undefined; dxx1 = undefined;
1004        } else {
1005          // merge
1006          dxx1 = dx1 + MERGE_THRESH;
1007        }
1008      }
1009
1010      if ((isAggregateSelection == false) ||
1011          (isAggregateSelection == true && isHighlighted == true)) {
1012        vars.totalSecsCurrLine += duration;
1013        vars.numVisibleRequestsCurrLine += weight;
1014      }
1015    }
1016  }
1017
1018  Render(ctx) {
1019    // Wait for initialization
1020    if (this.Canvas == undefined) return;
1021
1022    // Update
1023    let toFixedPrecision = 2;
1024    const extent = this.UpperBoundTime - this.LowerBoundTime;
1025    {
1026      if (extent < 0.1) {
1027        toFixedPrecision = 4;
1028      } else if (extent < 1) {
1029        toFixedPrecision = 3;
1030      }
1031    }
1032
1033    let dx = this.CurrDeltaX;
1034    if (dx != 0) {
1035      if (this.CurrShiftFlag) dx *= 5;
1036      this.LowerBoundTime += dx * extent;
1037      this.UpperBoundTime += dx * extent;
1038      this.IsCanvasDirty = true;
1039    }
1040
1041    // Hovered interval for display
1042    let theHoveredReq = undefined;
1043    let theHoveredInterval = undefined;
1044    let currHighlightedReqs = [];
1045
1046    let dz = this.CurrDeltaZoom;
1047    this.Zoom(dz);
1048    this.UpdateAnimation();
1049
1050    this.LastTimeLowerBound = this.LowerBoundTime;
1051    this.LastTimeUpperBound = this.UpperBoundTime;
1052
1053    if (this.IsCanvasDirty) {
1054      this.IsCanvasDirty = false;
1055      // Shorthand for HighlightedRegion.t{0,1}
1056      let t0 = undefined, t1 = undefined;
1057
1058      // Highlight
1059      let highlightedInterval = [];
1060      let numIntersected =
1061          0;  // How many requests intersect with highlighted area
1062      if (this.IsHighlighted()) {
1063        t0 = Math.min(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
1064        t1 = Math.max(this.HighlightedRegion.t0, this.HighlightedRegion.t1);
1065        highlightedInterval = [t0, t1];
1066      }
1067      this.IpmiVizHistHighlighted = {};
1068
1069      const width = this.Canvas.width;
1070      const height = this.Canvas.height;
1071
1072      ctx.globalCompositeOperation = 'source-over';
1073      ctx.clearRect(0, 0, width, height);
1074      ctx.strokeStyle = '#000';
1075      ctx.fillStyle = '#000';
1076      ctx.lineWidth = 1;
1077
1078      ctx.font = '12px Monospace';
1079
1080      // Highlight current line
1081      if (this.MouseState.hoveredVisibleLineIndex != undefined) {
1082        const hv_lidx = this.MouseState.hoveredVisibleLineIndex + this.VisualLineStartIdx;
1083        if (hv_lidx >= 0 &&
1084            hv_lidx < this.Titles.length) {
1085          ctx.fillStyle = 'rgba(32,32,32,0.2)';
1086          let dy = YBEGIN + LINE_SPACING * this.MouseState.hoveredVisibleLineIndex -
1087              LINE_SPACING / 2;
1088          ctx.fillRect(0, dy, RIGHT_MARGIN, LINE_SPACING);
1089        }
1090      }
1091
1092      // Draw highlighted background over time labels when the mouse is hovering over
1093      // the time axis
1094      ctx.fillStyle = "#FF9";
1095      if (this.MouseState.hoveredSide == "top_horizontal_scrollbar") {
1096        ctx.fillRect(LEFT_MARGIN, 0, RIGHT_MARGIN-LEFT_MARGIN, TOP_HORIZONTAL_SCROLLBAR_HEIGHT);
1097      } else if (this.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
1098        ctx.fillRect(LEFT_MARGIN, height-BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT, RIGHT_MARGIN-LEFT_MARGIN, BOTTOM_HORIZONTAL_SCROLLBAR_HEIGHT);
1099      }
1100
1101      ctx.fillStyle = '#000';
1102      // Time marks at the beginning & end of the visible range
1103      ctx.textBaseline = 'bottom';
1104      ctx.textAlign = 'left';
1105      ctx.fillText(
1106          '' + this.LowerBoundTime.toFixed(toFixedPrecision) + 's',
1107          LEFT_MARGIN + 3, height);
1108      ctx.textAlign = 'end';
1109      ctx.fillText(
1110          '' + this.UpperBoundTime.toFixed(toFixedPrecision) + 's',
1111          RIGHT_MARGIN - 3, height);
1112
1113      ctx.textBaseline = 'top';
1114      ctx.textAlign = 'left';
1115      ctx.fillText(
1116          '' + this.LowerBoundTime.toFixed(toFixedPrecision) + 's',
1117          LEFT_MARGIN + 3, TEXT_Y0);
1118      ctx.textAlign = 'right';
1119      ctx.fillText(
1120          '' + this.UpperBoundTime.toFixed(toFixedPrecision) + 's',
1121          RIGHT_MARGIN - 3, TEXT_Y0);
1122
1123      let y = YBEGIN;
1124      let numVisibleRequests = 0;
1125
1126      ctx.beginPath();
1127      ctx.moveTo(LEFT_MARGIN, 0);
1128      ctx.lineTo(LEFT_MARGIN, height);
1129      ctx.stroke();
1130
1131      ctx.beginPath();
1132      ctx.moveTo(RIGHT_MARGIN, 0);
1133      ctx.lineTo(RIGHT_MARGIN, height);
1134      ctx.stroke();
1135
1136      // Column Titles
1137      ctx.fillStyle = '#000';
1138      let columnTitle = '(All requests)';
1139      if (this.GroupByStr.length > 0) {
1140        columnTitle = this.GroupByStr;
1141      }
1142      ctx.textAlign = 'right';
1143      ctx.textBaseline = 'top';
1144      // Split into lines
1145      {
1146        let lines = this.ToLines(columnTitle, this.TitleDispLengthLimit)
1147        for (let i = 0; i < lines.length; i++) {
1148          ctx.fillText(lines[i], LEFT_MARGIN - 3, 3 + i * LINE_HEIGHT);
1149        }
1150      }
1151
1152      if (this.IsTimeDistributionEnabled) {
1153        // "Histogram" title
1154        ctx.fillStyle = '#000';
1155        ctx.textBaseline = 'top';
1156        ctx.textAlign = 'center';
1157        ctx.fillText('Time Distribution', HISTOGRAM_X, TEXT_Y0);
1158
1159        ctx.textAlign = 'right'
1160        ctx.fillText('In dataset /', HISTOGRAM_X, TEXT_Y0 + LINE_SPACING - 2);
1161
1162        ctx.fillStyle = '#00F';
1163
1164        ctx.textAlign = 'left'
1165        if (this.IsHighlighted()) {
1166          ctx.fillText(
1167              ' In selection', HISTOGRAM_X, TEXT_Y0 + LINE_SPACING - 2);
1168        }
1169        else {
1170          ctx.fillText(' In viewport', HISTOGRAM_X, TEXT_Y0 + LINE_SPACING - 2);
1171        }
1172      }
1173
1174      ctx.fillStyle = '#000';
1175
1176      // Time Axis Breaks
1177      const breakWidths = [
1178        86400,  10800,  3600,    1800,    1200,   600,   300,   120,
1179        60,     30,     10,      5,       2,      1,     0.5,   0.2,
1180        0.1,    0.05,   0.02,    0.01,    0.005,  0.002, 0.001, 0.0005,
1181        0.0002, 0.0001, 0.00005, 0.00002, 0.00001
1182      ];
1183      const BreakDrawLimit = 1000;  // Only draw up to this many grid lines
1184
1185      let bidx = 0;
1186      while (bidx < breakWidths.length &&
1187             breakWidths[bidx] > this.UpperBoundTime - this.LowerBoundTime) {
1188        bidx++;
1189      }
1190      let breakWidth = breakWidths[bidx + 1];
1191      if (bidx < breakWidths.length) {
1192        let t2 = 0;  // Cannot name as "t0" otherwise clash
1193        bidx = 0;
1194        while (bidx < breakWidths.length) {
1195          while (t2 + breakWidths[bidx] < this.LowerBoundTime) {
1196            t2 += breakWidths[bidx];
1197          }
1198          if (t2 + breakWidths[bidx] >= this.LowerBoundTime &&
1199              t2 + breakWidths[bidx] <= this.UpperBoundTime) {
1200            break;
1201          }
1202          bidx++;
1203        }
1204        let draw_count = 0;
1205        if (bidx < breakWidths.length) {
1206          for (; t2 < this.UpperBoundTime; t2 += breakWidth) {
1207            if (t2 > this.LowerBoundTime) {
1208              ctx.beginPath();
1209              let dx = MapXCoord(
1210                  t2, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
1211                  this.UpperBoundTime);
1212              ctx.strokeStyle = '#C0C0C0';
1213              ctx.moveTo(dx, 0);
1214              ctx.lineTo(dx, height);
1215              ctx.stroke();
1216              ctx.closePath();
1217              ctx.fillStyle = '#C0C0C0';
1218
1219              ctx.textAlign = 'left';
1220              ctx.textBaseline = 'bottom';
1221              let label2 = t2.toFixed(toFixedPrecision) + 's';
1222              let w = ctx.measureText(label2).width;
1223              if (dx + w > RIGHT_MARGIN) ctx.textAlign = 'right';
1224              ctx.fillText(label2, dx, height);
1225
1226              ctx.textBaseline = 'top';
1227              ctx.fillText(label2, dx, TEXT_Y0);
1228
1229              draw_count++;
1230              if (draw_count > BreakDrawLimit) break;
1231            }
1232          }
1233        }
1234      }
1235
1236      // Whether we aggregate selected requests or visible requests
1237      let isAggregateSelection = false;
1238      if (this.IsHighlighted()) isAggregateSelection = true;
1239      let numVisibleRequestsPerLine = {}; // DataLineIndex -> Count
1240      let numFailedRequestsPerLine = {};
1241      let totalSecondsPerLine = {};
1242
1243      // Range of Titles that were displayed
1244      let title_start_idx = this.VisualLineStartIdx, title_end_idx = title_start_idx;
1245
1246      const tvh = this.TotalVisualHeight();
1247
1248      // This is used to handle Intervals that have overlapping entries
1249      let last_data_line_idx = -999;//this.VisualLineIndexToDataLineIndex(this.VisualLineStartIdx);
1250
1251      // 'j' denotes a line index; if the viewport starts from the middle of an overlapping series of
1252      // lines, 'j' will be rewinded to the first one in the series to make the counts correct.
1253      let j0 = this.VisualLineStartIdx;
1254      while (j0 > 0 && this.VisualLineIndexToDataLineIndex(j0)[1] > 0) { j0--; }
1255
1256      // If should_render is false, we're counting the entries outisde the viewport
1257      // If should_render is true, do the rendering
1258      let should_render = false;
1259
1260      // 'j' then iterates over the "visual rows" that need to be displayed.
1261      // A "visual row" might be one of:
1262      // 1. a "header" line
1263      // 2. an actual row of data (in the Intervals variable)
1264
1265      // 'j' needs to go PAST the viewport if the last row is overlapping and spans beyond the viewport.
1266      for (let j = j0; j < tvh; j++) {
1267
1268        if (j >= this.VisualLineStartIdx) { should_render = true; }
1269        if (y > height) { should_render = false; }
1270
1271        const tmp = this.VisualLineIndexToDataLineIndex(j);
1272        if (tmp == undefined) break;
1273        const data_line_idx = tmp[0];
1274        const visual_line_offset_within_data_line = tmp[1];
1275
1276        const should_render_title = (data_line_idx != last_data_line_idx) ||
1277                                       (j == this.VisualLineStartIdx); // The first visible line should always be drawn
1278        last_data_line_idx = data_line_idx;
1279
1280        if (should_render_title && data_line_idx != -999 && should_render) { // Only draw line title and histogram per data line index not visual line index
1281          ctx.textBaseline = 'middle';
1282          ctx.textAlign = 'right';
1283          let desc_width = 0;
1284          if (NetFnCmdToDescription[this.Titles[data_line_idx].title] != undefined) {
1285            let desc = ' (' + NetFnCmdToDescription[this.Titles[data_line_idx].title] + ')';
1286            desc_width = ctx.measureText(desc).width;
1287            ctx.fillStyle = '#888';  // Grey
1288            ctx.fillText(desc, LEFT_MARGIN - 3, y);
1289          }
1290
1291
1292          // Plot histogram
1293          if (this.IsTimeDistributionEnabled == true) {
1294            const t = this.Titles[data_line_idx].title;
1295            if (GetHistoryHistogram()[t] != undefined) {
1296              if (this.IpmiVizHistogramImageData[t] == undefined) {
1297                let tmp = RenderHistogramForImageData(ctx, t);
1298                this.IpmiVizHistogramImageData[t] = tmp[0];
1299                HistogramThresholds[t] = tmp[1];
1300              }
1301              this.RenderHistogram(ctx, t, HISTOGRAM_X, y);
1302              ctx.textAlignment = 'right';
1303            } else {
1304            }
1305          }
1306
1307          // If is HEADER: do not draw here, darw in do_RenderHeader()
1308          if (this.Titles[data_line_idx].header == false) {
1309            ctx.textAlignment = 'right';
1310            ctx.textBaseline = 'middle';
1311            ctx.fillStyle = '#000000';  // Revert to Black
1312            ctx.strokeStyle = '#000000';
1313            let tj_draw = this.Titles[data_line_idx].title;
1314            const title_disp_length_limit = this.GetTitleWidthLimit();
1315            if (tj_draw != undefined && tj_draw.length > title_disp_length_limit) {
1316              tj_draw = tj_draw.substr(0, title_disp_length_limit) + '...'
1317            }
1318            ctx.fillText(tj_draw, LEFT_MARGIN - 3 - desc_width, y);
1319          }
1320        } else if (should_render_title && data_line_idx == -999) {
1321          continue;
1322        }
1323
1324        let numOverflowEntriesToTheLeft = 0;  // #entries below the lower bound
1325        let numOverflowEntriesToTheRight =
1326            0;                               // #entries beyond the upper bound
1327        let numVisibleRequestsCurrLine = 0;  // #entries visible
1328        let totalSecsCurrLine = 0;           // Total duration in seconds
1329        let numFailedRequestsCurrLine = 0;
1330
1331        const intervals_idxes = this.Titles[data_line_idx].intervals_idxes;
1332
1333        let intervals_j = undefined;
1334        if (intervals_idxes.length == 1) {
1335          intervals_j = this.Intervals[intervals_idxes[0]];
1336        }
1337
1338        // Draw the contents in the set of intervals
1339        // The drawing method depends on whether this data line is a header or not
1340
1341        // Save the context for reference for the rendering routines
1342        let vars = {
1343          "theHoveredReq": theHoveredReq,
1344          "theHoveredInterval": theHoveredInterval,
1345          "numIntersected": numIntersected,
1346          "numOverflowEntriesToTheLeft": numOverflowEntriesToTheLeft,
1347          "numOverflowEntriesToTheRight": numOverflowEntriesToTheRight,
1348          "currHighlightedReqs": currHighlightedReqs,
1349          "totalSecondsPerLine": totalSecondsPerLine,
1350          "highlightedInterval": highlightedInterval,
1351          "numVisibleRequestsCurrLine": numVisibleRequestsCurrLine,
1352          "totalSecsCurrLine": totalSecsCurrLine,
1353        }  // Emulate a reference
1354
1355        let dy0 = y - LINE_HEIGHT / 2, dy1 = y + LINE_HEIGHT / 2;
1356        if (this.Titles[data_line_idx].header == false) {
1357          if (intervals_j != undefined) {
1358            this.do_RenderIntervals(ctx, intervals_j, j, dy0, dy1,
1359              data_line_idx, visual_line_offset_within_data_line, isAggregateSelection, vars, should_render);
1360          }
1361        } else {
1362          this.do_RenderHeader(ctx, this.Titles[data_line_idx],
1363            j, dy0, dy1,
1364            data_line_idx, visual_line_offset_within_data_line, isAggregateSelection, vars, should_render);
1365        }
1366
1367        // Update the context variables with updated values
1368        theHoveredReq = vars.theHoveredReq;
1369        theHoveredInterval = vars.theHoveredInterval;
1370        numIntersected = vars.numIntersected;
1371        numOverflowEntriesToTheLeft = vars.numOverflowEntriesToTheLeft;
1372        numOverflowEntriesToTheRight = vars.numOverflowEntriesToTheRight;
1373        currHighlightedReqs = vars.currHighlightedReqs;
1374        totalSecondsPerLine = vars.totalSecondsPerLine;
1375        highlightedInterval = vars.highlightedInterval;
1376        numVisibleRequestsCurrLine = vars.numVisibleRequestsCurrLine;
1377        totalSecsCurrLine = vars.totalSecsCurrLine;
1378
1379        // Triangle markers for entries outside of the viewport
1380        {
1381          const PAD = 2, H = LINE_SPACING;
1382          if (this.MouseState.hoveredVisibleLineIndex + this.VisualLineStartIdx == data_line_idx &&
1383              this.MouseState.hoveredSide == 'left') {
1384            ctx.fillStyle = '#0000FF';
1385          } else {
1386            ctx.fillStyle = 'rgba(128,128,0,0.5)';
1387          }
1388          if (numOverflowEntriesToTheLeft > 0) {
1389            ctx.beginPath();
1390            ctx.moveTo(LEFT_MARGIN + PAD + H / 2, y - H / 2);
1391            ctx.lineTo(LEFT_MARGIN + PAD, y);
1392            ctx.lineTo(LEFT_MARGIN + PAD + H / 2, y + H / 2);
1393            ctx.closePath();
1394            ctx.fill();
1395            ctx.textAlign = 'left';
1396            ctx.textBaseline = 'center';
1397            ctx.fillText(
1398                '+' + numOverflowEntriesToTheLeft,
1399                LEFT_MARGIN + 2 * PAD + H / 2, y);
1400          }
1401
1402          if (this.MouseState.hoveredVisibleLineIndex + this.VisualLineStartIdx == j &&
1403              this.MouseState.hoveredSide == 'right') {
1404            ctx.fillStyle = '#0000FF';
1405          } else {
1406            ctx.fillStyle = 'rgba(128,128,0,0.5)';
1407          }
1408          if (numOverflowEntriesToTheRight > 0) {
1409            ctx.beginPath();
1410            ctx.moveTo(RIGHT_MARGIN - PAD - H / 2, y - H / 2);
1411            ctx.lineTo(RIGHT_MARGIN - PAD, y);
1412            ctx.lineTo(RIGHT_MARGIN - PAD - H / 2, y + H / 2);
1413            ctx.closePath();
1414            ctx.fill();
1415            ctx.textAlign = 'right';
1416            ctx.fillText(
1417                '+' + numOverflowEntriesToTheRight,
1418                RIGHT_MARGIN - 2 * PAD - H / 2, y);
1419          }
1420        }
1421
1422        if (should_render)
1423          y = y + LINE_SPACING;
1424
1425        // Should aggregate.
1426        if (!(data_line_idx in numVisibleRequestsPerLine)) { numVisibleRequestsPerLine[data_line_idx] = 0; }
1427        numVisibleRequestsPerLine[data_line_idx] += numVisibleRequestsCurrLine;
1428
1429        if (!(data_line_idx in numFailedRequestsPerLine)) { numFailedRequestsPerLine[data_line_idx] = 0; }
1430        numFailedRequestsPerLine[data_line_idx] += numFailedRequestsCurrLine;
1431
1432        if (!(data_line_idx in totalSecondsPerLine)) { totalSecondsPerLine[data_line_idx] = 0; }
1433        totalSecondsPerLine[data_line_idx] += totalSecsCurrLine;
1434
1435        title_end_idx = j;
1436
1437        if (y > height) {
1438          // Make sure we don't miss the entry count of the rows beyond the viewport
1439          if (visual_line_offset_within_data_line == 0) {
1440            break;
1441          }
1442        }
1443      }
1444
1445      {
1446        let nbreaks = this.TotalVisualHeight();
1447        // Draw a scroll bar on the left
1448        if (!(title_start_idx == 0 && title_end_idx == nbreaks - 1)) {
1449
1450          const y0 = title_start_idx * height / nbreaks;
1451          const y1 = (1 + title_end_idx) * height / nbreaks;
1452
1453          let highlighted = false;
1454          const THRESH = 8;
1455          if (this.MouseState.IsDraggingScrollBar()) {
1456            highlighted = true;
1457          }
1458          this.ScrollBarState.highlighted = highlighted;
1459
1460          // If not dragging, let title_start_idx drive y0 and y1, else let the
1461          // user's input drive y0 and y1 and title_start_idx
1462          if (!this.MouseState.IsDraggingScrollBar()) {
1463            this.ScrollBarState.y0 = y0;
1464            this.ScrollBarState.y1 = y1;
1465          }
1466
1467          if (highlighted) {
1468            ctx.fillStyle = "#FF3";
1469          } else {
1470            ctx.fillStyle = this.AccentColor;
1471          }
1472          ctx.fillRect(0, y0, SCROLL_BAR_WIDTH, y1 - y0);
1473
1474        } else {
1475          this.ScrollBarState.y0 = undefined;
1476          this.ScrollBarState.y1 = undefined;
1477          this.ScrollBarState.highlighted = false;
1478        }
1479      }
1480
1481      // Draw highlighted sections for the histograms
1482      if (this.IsTimeDistributionEnabled) {
1483        y = YBEGIN;
1484        for (let j = this.TitleStartIdx; j < this.Intervals.length; j++) {
1485          if (this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title] != undefined) {
1486            let entry = HistogramThresholds[this.Titles[data_line_idx].title];
1487            const theSet =
1488                Array.from(this.IpmiVizHistHighlighted[this.Titles[data_line_idx].title]);
1489            for (let i = 0; i < theSet.length; i++) {
1490              bidx = theSet[i];
1491              if (entry != undefined) {
1492                if (bidx < entry[0][0]) {
1493                  if (bidx < 0) {
1494                    bidx = 0;
1495                  }
1496                  ctx.fillStyle = 'rgba(0, 255, 0, 0.3)';
1497                } else if (bidx > entry[1][0]) {
1498                  if (bidx > 1) {
1499                    bidx = 1;
1500                  }
1501                  ctx.fillStyle = 'rgba(255,0,0,0.3)';
1502                } else {
1503                  ctx.fillStyle = 'rgba(0,0,255,0.3)';
1504                }
1505              } else {
1506                ctx.fillStyle = 'rgba(0,0,255,0.3)';
1507              }
1508              const dx = HISTOGRAM_X - HISTOGRAM_W / 2 + HISTOGRAM_W * bidx;
1509
1510              const r = HISTOGRAM_H * 0.17;
1511              ctx.beginPath();
1512              ctx.ellipse(dx, y, r, r, 0, 0, 3.14159 * 2);
1513              ctx.fill();
1514            }
1515          }
1516          y += LINE_SPACING;
1517        }
1518      }
1519
1520      // Render number of visible requests versus totals
1521      ctx.textAlign = 'left';
1522      ctx.textBaseline = 'top';
1523      let totalOccs = 0, totalSecs = 0;
1524      if (this.IsHighlighted()) {
1525        ctx.fillStyle = '#00F';
1526        ctx.fillText('# / time', 3, TEXT_Y0);
1527        ctx.fillText('in selection', 3, TEXT_Y0 + LINE_SPACING - 2);
1528      } else {
1529        ctx.fillStyle = '#000';
1530        ctx.fillText('# / time', 3, TEXT_Y0);
1531        ctx.fillText('in viewport', 3, TEXT_Y0 + LINE_SPACING - 2);
1532      }
1533
1534      let timeDesc = '';
1535      ctx.textBaseline = 'middle';
1536      last_data_line_idx = -999;
1537
1538      for (let j = this.VisualLineStartIdx, i = 0;
1539               j < tvh && (YBEGIN + i*LINE_SPACING)<height; j++, i++) {
1540        const x = this.VisualLineIndexToDataLineIndex(j);
1541        if (x == undefined) break;
1542        const data_line_idx = x[0];
1543        if (data_line_idx == undefined) break;
1544        if (data_line_idx != last_data_line_idx) {
1545          let y1 = YBEGIN + LINE_SPACING * (i);
1546          let totalSeconds = totalSecondsPerLine[data_line_idx];
1547          if (totalSeconds < 1) {
1548            timeDesc = (totalSeconds * 1000.0).toFixed(toFixedPrecision) + 'ms';
1549          } else if (totalSeconds != undefined) {
1550            timeDesc = totalSeconds.toFixed(toFixedPrecision) + 's';
1551          } else {
1552            timeDesc = "???"
1553          }
1554
1555          const n0 = numVisibleRequestsPerLine[data_line_idx];
1556          const n1 = numFailedRequestsPerLine[data_line_idx];
1557          let txt = '';
1558          if (n1 > 0) {
1559            txt = '' + n0 + '+' + n1 + ' / ' + timeDesc;
1560          } else {
1561            txt = '' + n0 + ' / ' + timeDesc;
1562          }
1563
1564          const tw = ctx.measureText(txt).width;
1565          const PAD = 8;
1566
1567          ctx.fillStyle = '#000';
1568          ctx.fillText(txt, 3, y1);
1569          totalOccs += numVisibleRequestsPerLine[data_line_idx];
1570          totalSecs += totalSeconds;
1571        }
1572        last_data_line_idx = data_line_idx;
1573      }
1574
1575      // This does not get displayed correctly, so disabling for now
1576      //timeDesc = '';
1577      //if (totalSecs < 1) {
1578      //  timeDesc = '' + (totalSecs * 1000).toFixed(toFixedPrecision) + 'ms';
1579      //} else {
1580      //  timeDesc = '' + totalSecs.toFixed(toFixedPrecision) + 's';
1581      //}
1582
1583      //ctx.fillText('Sum:', 3, y + 2 * LINE_SPACING);
1584      //ctx.fillText('' + totalOccs + ' / ' + timeDesc, 3, y + 3 * LINE_SPACING);
1585
1586      // Update highlighted requests
1587      if (this.IsHighlightDirty) {
1588        this.HighlightedRequests = currHighlightedReqs;
1589        this.IsHighlightDirty = false;
1590
1591        // Todo: This callback will be different for the DBus pane
1592        OnHighlightedChanged(HighlightedRequests);
1593      }
1594
1595      // Render highlight statistics
1596      if (this.IsHighlighted()) {
1597        ctx.fillStyle = 'rgba(128,128,255,0.5)';
1598        let x0 = MapXCoord(
1599            t0, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
1600            this.UpperBoundTime);
1601        let x1 = MapXCoord(
1602            t1, LEFT_MARGIN, RIGHT_MARGIN, this.LowerBoundTime,
1603            this.UpperBoundTime);
1604        ctx.fillRect(x0, 0, x1 - x0, height);
1605
1606        let label0 = '' + t0.toFixed(toFixedPrecision) + 's';
1607        let label1 = '' + t1.toFixed(toFixedPrecision) + 's';
1608        let width0 = ctx.measureText(label0).width;
1609        let width1 = ctx.measureText(label1).width;
1610        let dispWidth = x1 - x0;
1611        // Draw time marks outside or inside?
1612        ctx.fillStyle = '#0000FF';
1613        ctx.textBaseline = 'top';
1614        if (dispWidth > width0 + width1) {
1615          ctx.textAlign = 'left';
1616          ctx.fillText(label0, x0, LINE_SPACING + TEXT_Y0);
1617          ctx.textAlign = 'right';
1618          ctx.fillText(label1, x1, LINE_SPACING + TEXT_Y0);
1619        } else {
1620          ctx.textAlign = 'right';
1621          ctx.fillText(label0, x0, LINE_SPACING + TEXT_Y0);
1622          ctx.textAlign = 'left';
1623          ctx.fillText(label1, x1, LINE_SPACING + TEXT_Y0);
1624        }
1625
1626        // This was calculated earlier
1627        ctx.textAlign = 'center';
1628        label1 = 'Duration: ' + (t1 - t0).toFixed(toFixedPrecision) + 's';
1629        ctx.fillText(label1, (x0 + x1) / 2, height - LINE_SPACING * 2);
1630      }
1631
1632      // Hovering cursor
1633      // Only draw when the mouse is not over any hotizontal scroll bar
1634      let should_hide_cursor = false;
1635
1636      if (this.MouseState.hoveredSide == "top_horizontal_scrollbar" ||
1637          this.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
1638        should_hide_cursor = true;
1639      }
1640      this.linked_views.forEach((v) => {
1641        if (v.MouseState.hoveredSide == "top_horizontal_scrollbar" ||
1642            v.MouseState.hoveredSide == "bottom_horizontal_scrollbar") {
1643          should_hide_cursor = true;
1644        }
1645      })
1646
1647      if (this.MouseState.hovered == true &&
1648          this.MouseState.hoveredSide == undefined &&
1649          should_hide_cursor == false) {
1650        ctx.beginPath();
1651        ctx.strokeStyle = '#0000FF';
1652        ctx.lineWidth = 1;
1653        if (this.IsHighlighted()) {
1654          ctx.moveTo(this.MouseState.x, 0);
1655          ctx.lineTo(this.MouseState.x, height);
1656        } else {
1657          ctx.moveTo(this.MouseState.x, LINE_SPACING * 2);
1658          ctx.lineTo(this.MouseState.x, height - LINE_SPACING * 2);
1659        }
1660        ctx.stroke();
1661
1662        if (this.IsHighlighted() == false) {
1663          let dispWidth = this.MouseState.x - LEFT_MARGIN;
1664          let label = '' +
1665              this.MouseXToTimestamp(this.MouseState.x)
1666                  .toFixed(toFixedPrecision) +
1667              's';
1668          let width0 = ctx.measureText(label).width;
1669          ctx.fillStyle = '#0000FF';
1670          ctx.textBaseline = 'bottom';
1671          ctx.textAlign = 'center';
1672          ctx.fillText(label, this.MouseState.x, height - LINE_SPACING);
1673          ctx.textBaseline = 'top';
1674          ctx.fillText(label, this.MouseState.x, LINE_SPACING + TEXT_Y0);
1675        }
1676      }
1677
1678      // Tooltip box next to hovered entry
1679      if (theHoveredReq !== undefined) {
1680        this.RenderToolTip(
1681            ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height);
1682      }
1683    }  // End IsCanvasDirty
1684  }
1685};
1686
1687// The extended classes have their own way of drawing popups for hovered entries
1688class IPMITimelineView extends TimelineView {
1689  RenderToolTip(
1690      ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1691    if (theHoveredReq == undefined) {
1692      return;
1693    }
1694    const PAD = 2, DELTA_Y = 14;
1695
1696    let labels = [];
1697    let netFn = theHoveredReq[0];
1698    let cmd = theHoveredReq[1];
1699    let t0 = theHoveredInterval[0];
1700    let t1 = theHoveredInterval[1];
1701
1702    labels.push('Netfn and CMD : (' + netFn + ', ' + cmd + ')');
1703    let key = netFn + ', ' + cmd;
1704
1705    if (NetFnCmdToDescription[key] != undefined) {
1706      labels.push('Description   : ' + NetFnCmdToDescription[key]);
1707    }
1708
1709    if (theHoveredReq.offset != undefined) {
1710      labels.push('Offset      : ' + theHoveredReq.offset);
1711    }
1712
1713    let req = theHoveredReq[4];
1714    labels.push('Request Data  : ' + req.length + ' bytes');
1715    if (req.length > 0) {
1716      labels.push('Hex   : ' + ToHexString(req, '', ' '));
1717      labels.push('ASCII : ' + ToASCIIString(req));
1718    }
1719    let resp = theHoveredReq[5];
1720    labels.push('Response Data : ' + theHoveredReq[5].length + ' bytes');
1721    if (resp.length > 0) {
1722      labels.push('Hex   : ' + ToHexString(resp, '', ' '));
1723      labels.push('ASCII : ' + ToASCIIString(resp));
1724    }
1725    labels.push('Start         : ' + t0.toFixed(toFixedPrecision) + 's');
1726    labels.push('End           : ' + t1.toFixed(toFixedPrecision) + 's');
1727    labels.push('Duration      : ' + ((t1 - t0) * 1000).toFixed(3) + 'ms');
1728
1729
1730    let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1731    for (let i = 0; i < labels.length; i++) {
1732      w = Math.max(w, ctx.measureText(labels[i]).width);
1733    }
1734    let dy = this.MouseState.y + DELTA_Y;
1735    if (dy + h > height) {
1736      dy = height - h;
1737    }
1738    let dx = this.MouseState.x;
1739    if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1740
1741    ctx.fillStyle = 'rgba(0,0,0,0.5)';
1742    ctx.fillRect(dx, dy, w + 2 * PAD, h);
1743
1744    ctx.textAlign = 'left';
1745    ctx.textBaseline = 'middle';
1746    ctx.fillStyle = '#FFFFFF';
1747    for (let i = 0; i < labels.length; i++) {
1748      ctx.fillText(
1749          labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1750    }
1751  }
1752};
1753
1754class DBusTimelineView extends TimelineView {
1755  RenderToolTip(
1756      ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1757    if (theHoveredReq == undefined) {
1758      return;
1759    }
1760    const PAD = 2, DELTA_Y = 14;
1761
1762    let labels = [];
1763    let msg_type = theHoveredReq[0];
1764    let serial = theHoveredReq[2];
1765    let sender = theHoveredReq[3];
1766    let destination = theHoveredReq[4];
1767    let path = theHoveredReq[5];
1768    let iface = theHoveredReq[6];
1769    let member = theHoveredReq[7];
1770
1771    let t0 = theHoveredInterval[0];
1772    let t1 = theHoveredInterval[1];
1773
1774    labels.push('Message type: ' + msg_type);
1775    labels.push('Serial      : ' + serial);
1776    labels.push('Sender      : ' + sender);
1777    labels.push('Destination : ' + destination);
1778    labels.push('Path        : ' + path);
1779    labels.push('Interface   : ' + iface);
1780    labels.push('Member      : ' + member);
1781
1782    let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1783    for (let i = 0; i < labels.length; i++) {
1784      w = Math.max(w, ctx.measureText(labels[i]).width);
1785    }
1786    let dy = this.MouseState.y + DELTA_Y;
1787    if (dy + h > height) {
1788      dy = height - h;
1789    }
1790    let dx = this.MouseState.x;
1791    if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1792
1793    ctx.fillStyle = 'rgba(0,0,0,0.5)';
1794    ctx.fillRect(dx, dy, w + 2 * PAD, h);
1795
1796    ctx.textAlign = 'left';
1797    ctx.textBaseline = 'middle';
1798    ctx.fillStyle = '#FFFFFF';
1799    for (let i = 0; i < labels.length; i++) {
1800      ctx.fillText(
1801          labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1802    }
1803  }
1804};
1805
1806class BoostASIOHandlerTimelineView extends TimelineView {
1807  RenderToolTip(
1808      ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1809    if (theHoveredReq == undefined) {
1810      return;
1811    }
1812    const PAD = 2, DELTA_Y = 14;
1813
1814    let labels = [];
1815    let create_time = theHoveredReq[2];
1816    let enter_time = theHoveredReq[3];
1817    let exit_time = theHoveredReq[4];
1818    let desc = theHoveredReq[5];
1819
1820    let t0 = theHoveredInterval[0];
1821    let t1 = theHoveredInterval[1];
1822
1823    labels.push('Creation time: ' + create_time);
1824    labels.push('Entry time   : ' + enter_time);
1825    labels.push('Exit time    : ' + exit_time);
1826    labels.push('Creation->Entry : ' + (enter_time - create_time));
1827    labels.push('Entry->Exit     : ' + (exit_time - enter_time));
1828    labels.push('Description  : ' + desc);
1829
1830    let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1831    for (let i = 0; i < labels.length; i++) {
1832      w = Math.max(w, ctx.measureText(labels[i]).width);
1833    }
1834    let dy = this.MouseState.y + DELTA_Y;
1835    if (dy + h > height) {
1836      dy = height - h;
1837    }
1838    let dx = this.MouseState.x;
1839    if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1840
1841    ctx.fillStyle = 'rgba(0,0,0,0.5)';
1842    ctx.fillRect(dx, dy, w + 2 * PAD, h);
1843
1844    ctx.textAlign = 'left';
1845    ctx.textBaseline = 'middle';
1846    ctx.fillStyle = '#FFFFFF';
1847    for (let i = 0; i < labels.length; i++) {
1848      ctx.fillText(
1849          labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1850    }
1851  }
1852}
1853