xref: /openbmc/openbmc-tools/dbus-vis/timeline_view.js (revision 1ee5007139cd897eb8850455f63bd2ddf2ee1f9d)
1 const { TouchBarScrubber } = require("electron");
2 
3 // Default range: 0 to 300s, shared between both views
4 var RANGE_LEFT_INIT = 0;
5 var RANGE_RIGHT_INIT = 300;
6 
7 // Global timeline start
8 var g_StartingSec = undefined;
9 
10 function ShouldShowDebugInfo() {
11   if (g_cb_debug_info.checked) return true;
12   else return false;
13 }
14 
15 function GetHistoryHistogram() {
16   return HistoryHistogram;
17 }
18 
19 function 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 
90 function 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
125 class 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
1718 class 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 
1784 class 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 packet_idx = -1;
1813     if (msg_type == 'mc') {
1814       packet_idx = 10;
1815     } else if (msg_type == 'sig') {
1816       packet_idx = 9;
1817     }
1818 
1819     if (packet_idx != -1) {
1820       let packet = theHoveredReq[packet_idx];
1821       if (packet.length >= 2) {
1822         const args = packet[1].length;
1823         if (args.length < 1) {
1824           labels.push("(no args)");
1825         } else {
1826           for (let i = 0; i < packet[1].length; i ++) {
1827             labels.push('args[' + i + "]: " + packet[1][i]);
1828           }
1829         }
1830       }
1831     }
1832 
1833     let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1834     for (let i = 0; i < labels.length; i++) {
1835       w = Math.max(w, ctx.measureText(labels[i]).width);
1836     }
1837     let dy = this.MouseState.y + DELTA_Y;
1838     if (dy + h > height) {
1839       dy = height - h;
1840     }
1841     let dx = this.MouseState.x;
1842     if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1843 
1844     ctx.fillStyle = 'rgba(0,0,0,0.5)';
1845     ctx.fillRect(dx, dy, w + 2 * PAD, h);
1846 
1847     ctx.textAlign = 'left';
1848     ctx.textBaseline = 'middle';
1849     ctx.fillStyle = '#FFFFFF';
1850     for (let i = 0; i < labels.length; i++) {
1851       ctx.fillText(
1852           labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1853     }
1854   }
1855 };
1856 
1857 class BoostASIOHandlerTimelineView extends TimelineView {
1858   RenderToolTip(
1859       ctx, theHoveredReq, theHoveredInterval, toFixedPrecision, height) {
1860     if (theHoveredReq == undefined) {
1861       return;
1862     }
1863     const PAD = 2, DELTA_Y = 14;
1864 
1865     let labels = [];
1866     let create_time = theHoveredReq[2];
1867     let enter_time = theHoveredReq[3];
1868     let exit_time = theHoveredReq[4];
1869     let desc = theHoveredReq[5];
1870 
1871     let t0 = theHoveredInterval[0];
1872     let t1 = theHoveredInterval[1];
1873 
1874     labels.push('Creation time: ' + create_time);
1875     labels.push('Entry time   : ' + enter_time);
1876     labels.push('Exit time    : ' + exit_time);
1877     labels.push('Creation->Entry : ' + (enter_time - create_time));
1878     labels.push('Entry->Exit     : ' + (exit_time - enter_time));
1879     labels.push('Description  : ' + desc);
1880 
1881     let w = 1, h = LINE_SPACING * labels.length + 2 * PAD;
1882     for (let i = 0; i < labels.length; i++) {
1883       w = Math.max(w, ctx.measureText(labels[i]).width);
1884     }
1885     let dy = this.MouseState.y + DELTA_Y;
1886     if (dy + h > height) {
1887       dy = height - h;
1888     }
1889     let dx = this.MouseState.x;
1890     if (RIGHT_MARGIN - dx < w) dx -= (w + 2 * PAD);
1891 
1892     ctx.fillStyle = 'rgba(0,0,0,0.5)';
1893     ctx.fillRect(dx, dy, w + 2 * PAD, h);
1894 
1895     ctx.textAlign = 'left';
1896     ctx.textBaseline = 'middle';
1897     ctx.fillStyle = '#FFFFFF';
1898     for (let i = 0; i < labels.length; i++) {
1899       ctx.fillText(
1900           labels[i], dx + PAD, dy + PAD + i * LINE_SPACING + LINE_SPACING / 2);
1901     }
1902   }
1903 }
1904