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