11cb0ef41Sopenharmony_ci// Copyright 2020 the V8 project authors. All rights reserved.
21cb0ef41Sopenharmony_ci// Use of this source code is governed by a BSD-style license that can be
31cb0ef41Sopenharmony_ci// found in the LICENSE file.
41cb0ef41Sopenharmony_ci
51cb0ef41Sopenharmony_ciimport {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs';
61cb0ef41Sopenharmony_ciimport {SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
71cb0ef41Sopenharmony_ciimport {CSSColor, delay, DOM, formatDurationMicros, V8CustomElement} from '../helper.mjs';
81cb0ef41Sopenharmony_ci
91cb0ef41Sopenharmony_ciexport const kTimelineHeight = 200;
101cb0ef41Sopenharmony_ci
111cb0ef41Sopenharmony_ciexport class TimelineTrackBase extends V8CustomElement {
121cb0ef41Sopenharmony_ci  _timeline;
131cb0ef41Sopenharmony_ci  _nofChunks = 500;
141cb0ef41Sopenharmony_ci  _chunks = [];
151cb0ef41Sopenharmony_ci  _selectedEntry;
161cb0ef41Sopenharmony_ci  _focusedEntry;
171cb0ef41Sopenharmony_ci  _timeToPixel;
181cb0ef41Sopenharmony_ci  _timeStartPixelOffset;
191cb0ef41Sopenharmony_ci  _legend;
201cb0ef41Sopenharmony_ci  _lastContentWidth = 0;
211cb0ef41Sopenharmony_ci
221cb0ef41Sopenharmony_ci  _cachedTimelineBoundingClientRect;
231cb0ef41Sopenharmony_ci  _cachedTimelineScrollLeft;
241cb0ef41Sopenharmony_ci
251cb0ef41Sopenharmony_ci  constructor(templateText) {
261cb0ef41Sopenharmony_ci    super(templateText);
271cb0ef41Sopenharmony_ci    this._selectionHandler = new SelectionHandler(this);
281cb0ef41Sopenharmony_ci    this._legend = new Legend(this.$('#legendTable'));
291cb0ef41Sopenharmony_ci
301cb0ef41Sopenharmony_ci    this.timelineChunks = this.$('#timelineChunks');
311cb0ef41Sopenharmony_ci    this.timelineSamples = this.$('#timelineSamples');
321cb0ef41Sopenharmony_ci    this.timelineNode = this.$('#timeline');
331cb0ef41Sopenharmony_ci    this.toolTipTargetNode = this.$('#toolTipTarget');
341cb0ef41Sopenharmony_ci    this.hitPanelNode = this.$('#hitPanel');
351cb0ef41Sopenharmony_ci    this.timelineAnnotationsNode = this.$('#timelineAnnotations');
361cb0ef41Sopenharmony_ci    this.timelineMarkersNode = this.$('#timelineMarkers');
371cb0ef41Sopenharmony_ci    this._scalableContentNode = this.$('#scalableContent');
381cb0ef41Sopenharmony_ci    this.isLocked = false;
391cb0ef41Sopenharmony_ci    this.setAttribute('tabindex', 0);
401cb0ef41Sopenharmony_ci  }
411cb0ef41Sopenharmony_ci
421cb0ef41Sopenharmony_ci  _initEventListeners() {
431cb0ef41Sopenharmony_ci    this._legend.onFilter = this._handleFilterTimeline.bind(this);
441cb0ef41Sopenharmony_ci    this.timelineNode.addEventListener(
451cb0ef41Sopenharmony_ci        'scroll', this._handleTimelineScroll.bind(this));
461cb0ef41Sopenharmony_ci    this.hitPanelNode.onclick = this._handleClick.bind(this);
471cb0ef41Sopenharmony_ci    this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this);
481cb0ef41Sopenharmony_ci    this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this);
491cb0ef41Sopenharmony_ci    this.$('#selectionForeground')
501cb0ef41Sopenharmony_ci        .addEventListener('mousemove', this._handleMouseMove.bind(this));
511cb0ef41Sopenharmony_ci    window.addEventListener('resize', () => this._resetCachedDimensions());
521cb0ef41Sopenharmony_ci  }
531cb0ef41Sopenharmony_ci
541cb0ef41Sopenharmony_ci  static get observedAttributes() {
551cb0ef41Sopenharmony_ci    return ['title'];
561cb0ef41Sopenharmony_ci  }
571cb0ef41Sopenharmony_ci
581cb0ef41Sopenharmony_ci  attributeChangedCallback(name, oldValue, newValue) {
591cb0ef41Sopenharmony_ci    if (name == 'title') {
601cb0ef41Sopenharmony_ci      this.$('#title').innerHTML = newValue;
611cb0ef41Sopenharmony_ci    }
621cb0ef41Sopenharmony_ci  }
631cb0ef41Sopenharmony_ci
641cb0ef41Sopenharmony_ci  _handleFilterTimeline(type) {
651cb0ef41Sopenharmony_ci    this._updateChunks();
661cb0ef41Sopenharmony_ci    this._legend.update(true);
671cb0ef41Sopenharmony_ci  }
681cb0ef41Sopenharmony_ci
691cb0ef41Sopenharmony_ci  set data(timeline) {
701cb0ef41Sopenharmony_ci    console.assert(timeline);
711cb0ef41Sopenharmony_ci    if (!this._timeline) this._initEventListeners();
721cb0ef41Sopenharmony_ci    this._timeline = timeline;
731cb0ef41Sopenharmony_ci    this._legend.timeline = timeline;
741cb0ef41Sopenharmony_ci    this.$('.content').style.display = timeline.isEmpty() ? 'none' : 'relative';
751cb0ef41Sopenharmony_ci    this._updateChunks();
761cb0ef41Sopenharmony_ci  }
771cb0ef41Sopenharmony_ci
781cb0ef41Sopenharmony_ci  set timeSelection({start, end, focus = false, zoom = false}) {
791cb0ef41Sopenharmony_ci    this._selectionHandler.timeSelection = {start, end};
801cb0ef41Sopenharmony_ci    this.updateSelection();
811cb0ef41Sopenharmony_ci    if (focus || zoom) {
821cb0ef41Sopenharmony_ci      if (!Number.isFinite(start) || !Number.isFinite(end)) {
831cb0ef41Sopenharmony_ci        throw new Error('Invalid number ranges');
841cb0ef41Sopenharmony_ci      }
851cb0ef41Sopenharmony_ci      if (focus) {
861cb0ef41Sopenharmony_ci        this.currentTime = (start + end) / 2;
871cb0ef41Sopenharmony_ci      }
881cb0ef41Sopenharmony_ci      if (zoom) {
891cb0ef41Sopenharmony_ci        const margin = 0.2;
901cb0ef41Sopenharmony_ci        const newVisibleTime = (end - start) * (1 + 2 * margin);
911cb0ef41Sopenharmony_ci        const currentVisibleTime =
921cb0ef41Sopenharmony_ci            this._cachedTimelineBoundingClientRect.width / this._timeToPixel;
931cb0ef41Sopenharmony_ci        this.nofChunks = this.nofChunks * (currentVisibleTime / newVisibleTime);
941cb0ef41Sopenharmony_ci      }
951cb0ef41Sopenharmony_ci    }
961cb0ef41Sopenharmony_ci  }
971cb0ef41Sopenharmony_ci
981cb0ef41Sopenharmony_ci  updateSelection() {
991cb0ef41Sopenharmony_ci    this._selectionHandler.update();
1001cb0ef41Sopenharmony_ci    this._legend.update();
1011cb0ef41Sopenharmony_ci  }
1021cb0ef41Sopenharmony_ci
1031cb0ef41Sopenharmony_ci  get _timelineBoundingClientRect() {
1041cb0ef41Sopenharmony_ci    if (this._cachedTimelineBoundingClientRect === undefined) {
1051cb0ef41Sopenharmony_ci      this._cachedTimelineBoundingClientRect =
1061cb0ef41Sopenharmony_ci          this.timelineNode.getBoundingClientRect();
1071cb0ef41Sopenharmony_ci    }
1081cb0ef41Sopenharmony_ci    return this._cachedTimelineBoundingClientRect;
1091cb0ef41Sopenharmony_ci  }
1101cb0ef41Sopenharmony_ci
1111cb0ef41Sopenharmony_ci  get _timelineScrollLeft() {
1121cb0ef41Sopenharmony_ci    if (this._cachedTimelineScrollLeft === undefined) {
1131cb0ef41Sopenharmony_ci      this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft;
1141cb0ef41Sopenharmony_ci    }
1151cb0ef41Sopenharmony_ci    return this._cachedTimelineScrollLeft;
1161cb0ef41Sopenharmony_ci  }
1171cb0ef41Sopenharmony_ci
1181cb0ef41Sopenharmony_ci  _resetCachedDimensions() {
1191cb0ef41Sopenharmony_ci    this._cachedTimelineBoundingClientRect = undefined;
1201cb0ef41Sopenharmony_ci    this._cachedTimelineScrollLeft = undefined;
1211cb0ef41Sopenharmony_ci  }
1221cb0ef41Sopenharmony_ci
1231cb0ef41Sopenharmony_ci  // Maps the clicked x position to the x position on timeline
1241cb0ef41Sopenharmony_ci  positionOnTimeline(pagePosX) {
1251cb0ef41Sopenharmony_ci    let rect = this._timelineBoundingClientRect;
1261cb0ef41Sopenharmony_ci    let posClickedX = pagePosX - rect.left + this._timelineScrollLeft;
1271cb0ef41Sopenharmony_ci    return posClickedX;
1281cb0ef41Sopenharmony_ci  }
1291cb0ef41Sopenharmony_ci
1301cb0ef41Sopenharmony_ci  positionToTime(pagePosX) {
1311cb0ef41Sopenharmony_ci    return this.relativePositionToTime(this.positionOnTimeline(pagePosX));
1321cb0ef41Sopenharmony_ci  }
1331cb0ef41Sopenharmony_ci
1341cb0ef41Sopenharmony_ci  relativePositionToTime(timelineRelativeX) {
1351cb0ef41Sopenharmony_ci    const timelineAbsoluteX = timelineRelativeX + this._timeStartPixelOffset;
1361cb0ef41Sopenharmony_ci    return (timelineAbsoluteX / this._timeToPixel) | 0;
1371cb0ef41Sopenharmony_ci  }
1381cb0ef41Sopenharmony_ci
1391cb0ef41Sopenharmony_ci  timeToPosition(time) {
1401cb0ef41Sopenharmony_ci    let relativePosX = time * this._timeToPixel;
1411cb0ef41Sopenharmony_ci    relativePosX -= this._timeStartPixelOffset;
1421cb0ef41Sopenharmony_ci    return relativePosX;
1431cb0ef41Sopenharmony_ci  }
1441cb0ef41Sopenharmony_ci
1451cb0ef41Sopenharmony_ci  set nofChunks(count) {
1461cb0ef41Sopenharmony_ci    const centerTime = this.currentTime;
1471cb0ef41Sopenharmony_ci    const kMinNofChunks = 100;
1481cb0ef41Sopenharmony_ci    if (count < kMinNofChunks) count = kMinNofChunks;
1491cb0ef41Sopenharmony_ci    const kMaxNofChunks = 10 * 1000;
1501cb0ef41Sopenharmony_ci    if (count > kMaxNofChunks) count = kMaxNofChunks;
1511cb0ef41Sopenharmony_ci    this._nofChunks = count | 0;
1521cb0ef41Sopenharmony_ci    this._updateChunks();
1531cb0ef41Sopenharmony_ci    this.currentTime = centerTime;
1541cb0ef41Sopenharmony_ci  }
1551cb0ef41Sopenharmony_ci
1561cb0ef41Sopenharmony_ci  get nofChunks() {
1571cb0ef41Sopenharmony_ci    return this._nofChunks;
1581cb0ef41Sopenharmony_ci  }
1591cb0ef41Sopenharmony_ci
1601cb0ef41Sopenharmony_ci  _updateChunks() {
1611cb0ef41Sopenharmony_ci    this._chunks = undefined;
1621cb0ef41Sopenharmony_ci    this._updateDimensions();
1631cb0ef41Sopenharmony_ci    this.requestUpdate();
1641cb0ef41Sopenharmony_ci  }
1651cb0ef41Sopenharmony_ci
1661cb0ef41Sopenharmony_ci  get chunks() {
1671cb0ef41Sopenharmony_ci    if (this._chunks?.length != this.nofChunks) {
1681cb0ef41Sopenharmony_ci      this._chunks =
1691cb0ef41Sopenharmony_ci          this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
1701cb0ef41Sopenharmony_ci      console.assert(this._chunks.length == this._nofChunks);
1711cb0ef41Sopenharmony_ci    }
1721cb0ef41Sopenharmony_ci    return this._chunks;
1731cb0ef41Sopenharmony_ci  }
1741cb0ef41Sopenharmony_ci
1751cb0ef41Sopenharmony_ci  set selectedEntry(value) {
1761cb0ef41Sopenharmony_ci    this._selectedEntry = value;
1771cb0ef41Sopenharmony_ci  }
1781cb0ef41Sopenharmony_ci
1791cb0ef41Sopenharmony_ci  get selectedEntry() {
1801cb0ef41Sopenharmony_ci    return this._selectedEntry;
1811cb0ef41Sopenharmony_ci  }
1821cb0ef41Sopenharmony_ci
1831cb0ef41Sopenharmony_ci  get focusedEntry() {
1841cb0ef41Sopenharmony_ci    return this._focusedEntry;
1851cb0ef41Sopenharmony_ci  }
1861cb0ef41Sopenharmony_ci
1871cb0ef41Sopenharmony_ci  set focusedEntry(entry) {
1881cb0ef41Sopenharmony_ci    this._focusedEntry = entry;
1891cb0ef41Sopenharmony_ci    if (entry) this._drawAnnotations(entry);
1901cb0ef41Sopenharmony_ci  }
1911cb0ef41Sopenharmony_ci
1921cb0ef41Sopenharmony_ci  set scrollLeft(offset) {
1931cb0ef41Sopenharmony_ci    this.timelineNode.scrollLeft = offset;
1941cb0ef41Sopenharmony_ci    this._cachedTimelineScrollLeft = offset;
1951cb0ef41Sopenharmony_ci  }
1961cb0ef41Sopenharmony_ci
1971cb0ef41Sopenharmony_ci  get scrollLeft() {
1981cb0ef41Sopenharmony_ci    return this._cachedTimelineScrollLeft;
1991cb0ef41Sopenharmony_ci  }
2001cb0ef41Sopenharmony_ci
2011cb0ef41Sopenharmony_ci  set currentTime(time) {
2021cb0ef41Sopenharmony_ci    const position = this.timeToPosition(time);
2031cb0ef41Sopenharmony_ci    const centerOffset = this._timelineBoundingClientRect.width / 2;
2041cb0ef41Sopenharmony_ci    this.scrollLeft = Math.max(0, position - centerOffset);
2051cb0ef41Sopenharmony_ci  }
2061cb0ef41Sopenharmony_ci
2071cb0ef41Sopenharmony_ci  get currentTime() {
2081cb0ef41Sopenharmony_ci    const centerOffset =
2091cb0ef41Sopenharmony_ci        this._timelineBoundingClientRect.width / 2 + this.scrollLeft;
2101cb0ef41Sopenharmony_ci    return this.relativePositionToTime(centerOffset);
2111cb0ef41Sopenharmony_ci  }
2121cb0ef41Sopenharmony_ci
2131cb0ef41Sopenharmony_ci  handleEntryTypeDoubleClick(e) {
2141cb0ef41Sopenharmony_ci    this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries));
2151cb0ef41Sopenharmony_ci  }
2161cb0ef41Sopenharmony_ci
2171cb0ef41Sopenharmony_ci  timelineIndicatorMove(offset) {
2181cb0ef41Sopenharmony_ci    this.timelineNode.scrollLeft += offset;
2191cb0ef41Sopenharmony_ci    this._cachedTimelineScrollLeft = undefined;
2201cb0ef41Sopenharmony_ci  }
2211cb0ef41Sopenharmony_ci
2221cb0ef41Sopenharmony_ci  _handleTimelineScroll(e) {
2231cb0ef41Sopenharmony_ci    let scrollLeft = e.currentTarget.scrollLeft;
2241cb0ef41Sopenharmony_ci    this._cachedTimelineScrollLeft = scrollLeft;
2251cb0ef41Sopenharmony_ci    this.dispatchEvent(new CustomEvent(
2261cb0ef41Sopenharmony_ci        'scrolltrack', {bubbles: true, composed: true, detail: scrollLeft}));
2271cb0ef41Sopenharmony_ci  }
2281cb0ef41Sopenharmony_ci
2291cb0ef41Sopenharmony_ci  _updateDimensions() {
2301cb0ef41Sopenharmony_ci    // No data in this timeline, no need to resize
2311cb0ef41Sopenharmony_ci    if (!this._timeline) return;
2321cb0ef41Sopenharmony_ci
2331cb0ef41Sopenharmony_ci    const centerOffset = this._timelineBoundingClientRect.width / 2;
2341cb0ef41Sopenharmony_ci    const time =
2351cb0ef41Sopenharmony_ci        this.relativePositionToTime(this._timelineScrollLeft + centerOffset);
2361cb0ef41Sopenharmony_ci    const start = this._timeline.startTime;
2371cb0ef41Sopenharmony_ci    const width = this._nofChunks * kChunkWidth;
2381cb0ef41Sopenharmony_ci    this._lastContentWidth = parseInt(this.timelineMarkersNode.style.width);
2391cb0ef41Sopenharmony_ci    this._timeToPixel = width / this._timeline.duration();
2401cb0ef41Sopenharmony_ci    this._timeStartPixelOffset = start * this._timeToPixel;
2411cb0ef41Sopenharmony_ci    this.timelineChunks.style.width = `${width}px`;
2421cb0ef41Sopenharmony_ci    this.timelineMarkersNode.style.width = `${width}px`;
2431cb0ef41Sopenharmony_ci    this.timelineAnnotationsNode.style.width = `${width}px`;
2441cb0ef41Sopenharmony_ci    this.hitPanelNode.style.width = `${width}px`;
2451cb0ef41Sopenharmony_ci    this._drawMarkers();
2461cb0ef41Sopenharmony_ci    this._selectionHandler.update();
2471cb0ef41Sopenharmony_ci    this._scaleContent(width);
2481cb0ef41Sopenharmony_ci    this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft =
2491cb0ef41Sopenharmony_ci        this.timeToPosition(time) - centerOffset;
2501cb0ef41Sopenharmony_ci  }
2511cb0ef41Sopenharmony_ci
2521cb0ef41Sopenharmony_ci  _scaleContent(currentWidth) {
2531cb0ef41Sopenharmony_ci    if (!this._lastContentWidth) return;
2541cb0ef41Sopenharmony_ci    const ratio = currentWidth / this._lastContentWidth;
2551cb0ef41Sopenharmony_ci    this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
2561cb0ef41Sopenharmony_ci  }
2571cb0ef41Sopenharmony_ci
2581cb0ef41Sopenharmony_ci  _adjustHeight(height) {
2591cb0ef41Sopenharmony_ci    const dataHeight = Math.max(height, 200);
2601cb0ef41Sopenharmony_ci    const viewHeight = Math.min(dataHeight, 400);
2611cb0ef41Sopenharmony_ci    this.style.setProperty('--data-height', dataHeight + 'px');
2621cb0ef41Sopenharmony_ci    this.style.setProperty('--view-height', viewHeight + 'px');
2631cb0ef41Sopenharmony_ci    this.timelineNode.style.overflowY =
2641cb0ef41Sopenharmony_ci        (height > kTimelineHeight) ? 'scroll' : 'hidden';
2651cb0ef41Sopenharmony_ci  }
2661cb0ef41Sopenharmony_ci
2671cb0ef41Sopenharmony_ci  _update() {
2681cb0ef41Sopenharmony_ci    this._legend.update();
2691cb0ef41Sopenharmony_ci    this._drawContent().then(() => this._drawAnnotations(this.selectedEntry));
2701cb0ef41Sopenharmony_ci    this._resetCachedDimensions();
2711cb0ef41Sopenharmony_ci  }
2721cb0ef41Sopenharmony_ci
2731cb0ef41Sopenharmony_ci  async _drawContent() {
2741cb0ef41Sopenharmony_ci    if (this._timeline.isEmpty()) return;
2751cb0ef41Sopenharmony_ci    await delay(5);
2761cb0ef41Sopenharmony_ci    const chunks = this.chunks;
2771cb0ef41Sopenharmony_ci    const max = chunks.max(each => each.size());
2781cb0ef41Sopenharmony_ci    let buffer = '';
2791cb0ef41Sopenharmony_ci    for (let i = 0; i < chunks.length; i++) {
2801cb0ef41Sopenharmony_ci      const chunk = chunks[i];
2811cb0ef41Sopenharmony_ci      const height = (chunk.size() / max * kChunkHeight);
2821cb0ef41Sopenharmony_ci      chunk.height = height;
2831cb0ef41Sopenharmony_ci      if (chunk.isEmpty()) continue;
2841cb0ef41Sopenharmony_ci      buffer += '<g>';
2851cb0ef41Sopenharmony_ci      buffer += this._drawChunk(i, chunk);
2861cb0ef41Sopenharmony_ci      buffer += '</g>'
2871cb0ef41Sopenharmony_ci    }
2881cb0ef41Sopenharmony_ci    this._scalableContentNode.innerHTML = buffer;
2891cb0ef41Sopenharmony_ci    this._scalableContentNode.style.transform = 'scale(1, 1)';
2901cb0ef41Sopenharmony_ci  }
2911cb0ef41Sopenharmony_ci
2921cb0ef41Sopenharmony_ci  _drawChunk(chunkIndex, chunk) {
2931cb0ef41Sopenharmony_ci    const groups = chunk.getBreakdown(event => event.type);
2941cb0ef41Sopenharmony_ci    let buffer = '';
2951cb0ef41Sopenharmony_ci    const kHeight = chunk.height;
2961cb0ef41Sopenharmony_ci    let lastHeight = kTimelineHeight;
2971cb0ef41Sopenharmony_ci    for (let i = 0; i < groups.length; i++) {
2981cb0ef41Sopenharmony_ci      const group = groups[i];
2991cb0ef41Sopenharmony_ci      if (group.length == 0) break;
3001cb0ef41Sopenharmony_ci      const height = (group.length / chunk.size() * kHeight) | 0;
3011cb0ef41Sopenharmony_ci      lastHeight -= height;
3021cb0ef41Sopenharmony_ci      const color = this._legend.colorForType(group.key);
3031cb0ef41Sopenharmony_ci      buffer += `<rect x=${chunkIndex * kChunkWidth} y=${lastHeight} height=${
3041cb0ef41Sopenharmony_ci          height} width=${kChunkVisualWidth} fill=${color} />`
3051cb0ef41Sopenharmony_ci    }
3061cb0ef41Sopenharmony_ci    return buffer;
3071cb0ef41Sopenharmony_ci  }
3081cb0ef41Sopenharmony_ci
3091cb0ef41Sopenharmony_ci  _drawMarkers() {
3101cb0ef41Sopenharmony_ci    // Put a time marker roughly every 20 chunks.
3111cb0ef41Sopenharmony_ci    const expected = this._timeline.duration() / this._nofChunks * 20;
3121cb0ef41Sopenharmony_ci    let interval = (10 ** Math.floor(Math.log10(expected)));
3131cb0ef41Sopenharmony_ci    let correction = Math.log10(expected / interval);
3141cb0ef41Sopenharmony_ci    correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
3151cb0ef41Sopenharmony_ci    interval *= correction;
3161cb0ef41Sopenharmony_ci
3171cb0ef41Sopenharmony_ci    const start = this._timeline.startTime;
3181cb0ef41Sopenharmony_ci    let time = start;
3191cb0ef41Sopenharmony_ci    let buffer = '';
3201cb0ef41Sopenharmony_ci    while (time < this._timeline.endTime) {
3211cb0ef41Sopenharmony_ci      const delta = time - start;
3221cb0ef41Sopenharmony_ci      const text = `${(delta / 1000) | 0} ms`;
3231cb0ef41Sopenharmony_ci      const x = (delta * this._timeToPixel) | 0;
3241cb0ef41Sopenharmony_ci      buffer += `<text x=${x - 2} y=0 class=markerText >${text}</text>`
3251cb0ef41Sopenharmony_ci      buffer +=
3261cb0ef41Sopenharmony_ci          `<line x1=${x} x2=${x} y1=12 y2=2000 dy=100% class=markerLine />`
3271cb0ef41Sopenharmony_ci      time += interval;
3281cb0ef41Sopenharmony_ci    }
3291cb0ef41Sopenharmony_ci    this.timelineMarkersNode.innerHTML = buffer;
3301cb0ef41Sopenharmony_ci  }
3311cb0ef41Sopenharmony_ci
3321cb0ef41Sopenharmony_ci  _drawAnnotations(logEntry, time) {
3331cb0ef41Sopenharmony_ci    if (!this._focusedEntry) return;
3341cb0ef41Sopenharmony_ci    this._drawEntryMark(this._focusedEntry);
3351cb0ef41Sopenharmony_ci  }
3361cb0ef41Sopenharmony_ci
3371cb0ef41Sopenharmony_ci  _drawEntryMark(entry) {
3381cb0ef41Sopenharmony_ci    const [x, y] = this._positionForEntry(entry);
3391cb0ef41Sopenharmony_ci    const color = this._legend.colorForType(entry.type);
3401cb0ef41Sopenharmony_ci    const mark =
3411cb0ef41Sopenharmony_ci        `<circle cx=${x} cy=${y} r=3 stroke=${color} class=annotationPoint />`;
3421cb0ef41Sopenharmony_ci    this.timelineAnnotationsNode.innerHTML = mark;
3431cb0ef41Sopenharmony_ci  }
3441cb0ef41Sopenharmony_ci
3451cb0ef41Sopenharmony_ci  _handleUnlockedMouseEvent(event) {
3461cb0ef41Sopenharmony_ci    this._focusedEntry = this._getEntryForEvent(event);
3471cb0ef41Sopenharmony_ci    if (!this._focusedEntry) return false;
3481cb0ef41Sopenharmony_ci    this._updateToolTip(event);
3491cb0ef41Sopenharmony_ci    const time = this.positionToTime(event.pageX);
3501cb0ef41Sopenharmony_ci    this._drawAnnotations(this._focusedEntry, time);
3511cb0ef41Sopenharmony_ci  }
3521cb0ef41Sopenharmony_ci
3531cb0ef41Sopenharmony_ci  _updateToolTip(event) {
3541cb0ef41Sopenharmony_ci    if (!this._focusedEntry) return false;
3551cb0ef41Sopenharmony_ci    this.dispatchEvent(
3561cb0ef41Sopenharmony_ci        new ToolTipEvent(this._focusedEntry, this.toolTipTargetNode));
3571cb0ef41Sopenharmony_ci    event.stopImmediatePropagation();
3581cb0ef41Sopenharmony_ci  }
3591cb0ef41Sopenharmony_ci
3601cb0ef41Sopenharmony_ci  _handleClick(event) {
3611cb0ef41Sopenharmony_ci    if (event.button !== 0) return;
3621cb0ef41Sopenharmony_ci    if (event.target === this.timelineChunks) return;
3631cb0ef41Sopenharmony_ci    this.isLocked = !this.isLocked;
3641cb0ef41Sopenharmony_ci    // Do this unconditionally since we want the tooltip to be update to the
3651cb0ef41Sopenharmony_ci    // latest locked state.
3661cb0ef41Sopenharmony_ci    this._handleUnlockedMouseEvent(event);
3671cb0ef41Sopenharmony_ci    return false;
3681cb0ef41Sopenharmony_ci  }
3691cb0ef41Sopenharmony_ci
3701cb0ef41Sopenharmony_ci  _handleDoubleClick(event) {
3711cb0ef41Sopenharmony_ci    if (event.button !== 0) return;
3721cb0ef41Sopenharmony_ci    this._selectionHandler.clearSelection();
3731cb0ef41Sopenharmony_ci    const time = this.positionToTime(event.pageX);
3741cb0ef41Sopenharmony_ci    const chunk = this._getChunkForEvent(event)
3751cb0ef41Sopenharmony_ci    if (!chunk) return;
3761cb0ef41Sopenharmony_ci    event.stopImmediatePropagation();
3771cb0ef41Sopenharmony_ci    this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end));
3781cb0ef41Sopenharmony_ci    return false;
3791cb0ef41Sopenharmony_ci  }
3801cb0ef41Sopenharmony_ci
3811cb0ef41Sopenharmony_ci  _handleMouseMove(event) {
3821cb0ef41Sopenharmony_ci    if (event.button !== 0) return;
3831cb0ef41Sopenharmony_ci    if (this._selectionHandler.isSelecting) return false;
3841cb0ef41Sopenharmony_ci    if (this.isLocked && this._focusedEntry) {
3851cb0ef41Sopenharmony_ci      this._updateToolTip(event);
3861cb0ef41Sopenharmony_ci      return false;
3871cb0ef41Sopenharmony_ci    }
3881cb0ef41Sopenharmony_ci    this._handleUnlockedMouseEvent(event);
3891cb0ef41Sopenharmony_ci  }
3901cb0ef41Sopenharmony_ci
3911cb0ef41Sopenharmony_ci  _getChunkForEvent(event) {
3921cb0ef41Sopenharmony_ci    const time = this.positionToTime(event.pageX);
3931cb0ef41Sopenharmony_ci    return this._chunkForTime(time);
3941cb0ef41Sopenharmony_ci  }
3951cb0ef41Sopenharmony_ci
3961cb0ef41Sopenharmony_ci  _chunkForTime(time) {
3971cb0ef41Sopenharmony_ci    const chunkIndex = ((time - this._timeline.startTime) /
3981cb0ef41Sopenharmony_ci                        this._timeline.duration() * this._nofChunks) |
3991cb0ef41Sopenharmony_ci        0;
4001cb0ef41Sopenharmony_ci    return this.chunks[chunkIndex];
4011cb0ef41Sopenharmony_ci  }
4021cb0ef41Sopenharmony_ci
4031cb0ef41Sopenharmony_ci  _positionForEntry(entry) {
4041cb0ef41Sopenharmony_ci    const chunk = this._chunkForTime(entry.time);
4051cb0ef41Sopenharmony_ci    if (chunk === undefined) return [-1, -1];
4061cb0ef41Sopenharmony_ci    const xFrom = (chunk.index * kChunkWidth + kChunkVisualWidth / 2) | 0;
4071cb0ef41Sopenharmony_ci    const yFrom = kTimelineHeight - chunk.yOffset(entry) | 0;
4081cb0ef41Sopenharmony_ci    return [xFrom, yFrom];
4091cb0ef41Sopenharmony_ci  }
4101cb0ef41Sopenharmony_ci
4111cb0ef41Sopenharmony_ci  _getEntryForEvent(event) {
4121cb0ef41Sopenharmony_ci    const chunk = this._getChunkForEvent(event);
4131cb0ef41Sopenharmony_ci    if (chunk?.isEmpty() ?? true) return false;
4141cb0ef41Sopenharmony_ci    const relativeIndex = Math.round(
4151cb0ef41Sopenharmony_ci        (kTimelineHeight - event.layerY) / chunk.height * (chunk.size() - 1));
4161cb0ef41Sopenharmony_ci    if (relativeIndex > chunk.size()) return false;
4171cb0ef41Sopenharmony_ci    const logEntry = chunk.at(relativeIndex);
4181cb0ef41Sopenharmony_ci    const style = this.toolTipTargetNode.style;
4191cb0ef41Sopenharmony_ci    style.left = `${chunk.index * kChunkWidth}px`;
4201cb0ef41Sopenharmony_ci    style.top = `${kTimelineHeight - chunk.height}px`;
4211cb0ef41Sopenharmony_ci    style.height = `${chunk.height}px`;
4221cb0ef41Sopenharmony_ci    style.width = `${kChunkVisualWidth}px`;
4231cb0ef41Sopenharmony_ci    return logEntry;
4241cb0ef41Sopenharmony_ci  }
4251cb0ef41Sopenharmony_ci};
4261cb0ef41Sopenharmony_ci
4271cb0ef41Sopenharmony_ciclass SelectionHandler {
4281cb0ef41Sopenharmony_ci  // TODO turn into static field once Safari supports it.
4291cb0ef41Sopenharmony_ci  static get SELECTION_OFFSET() {
4301cb0ef41Sopenharmony_ci    return 10
4311cb0ef41Sopenharmony_ci  };
4321cb0ef41Sopenharmony_ci
4331cb0ef41Sopenharmony_ci  _timeSelection = {start: -1, end: Infinity};
4341cb0ef41Sopenharmony_ci  _selectionOriginTime = -1;
4351cb0ef41Sopenharmony_ci
4361cb0ef41Sopenharmony_ci  constructor(timeline) {
4371cb0ef41Sopenharmony_ci    this._timeline = timeline;
4381cb0ef41Sopenharmony_ci    this._timelineNode = this._timeline.$('#timeline');
4391cb0ef41Sopenharmony_ci    this._timelineNode.addEventListener(
4401cb0ef41Sopenharmony_ci        'mousedown', this._handleMouseDown.bind(this));
4411cb0ef41Sopenharmony_ci    this._timelineNode.addEventListener(
4421cb0ef41Sopenharmony_ci        'mouseup', this._handleMouseUp.bind(this));
4431cb0ef41Sopenharmony_ci    this._timelineNode.addEventListener(
4441cb0ef41Sopenharmony_ci        'mousemove', this._handleMouseMove.bind(this));
4451cb0ef41Sopenharmony_ci    this._selectionNode = this._timeline.$('#selection');
4461cb0ef41Sopenharmony_ci    this._selectionForegroundNode = this._timeline.$('#selectionForeground');
4471cb0ef41Sopenharmony_ci    this._selectionForegroundNode.addEventListener(
4481cb0ef41Sopenharmony_ci        'dblclick', this._handleDoubleClick.bind(this));
4491cb0ef41Sopenharmony_ci    this._selectionBackgroundNode = this._timeline.$('#selectionBackground');
4501cb0ef41Sopenharmony_ci    this._leftHandleNode = this._timeline.$('#leftHandle');
4511cb0ef41Sopenharmony_ci    this._rightHandleNode = this._timeline.$('#rightHandle');
4521cb0ef41Sopenharmony_ci  }
4531cb0ef41Sopenharmony_ci
4541cb0ef41Sopenharmony_ci  update() {
4551cb0ef41Sopenharmony_ci    if (!this.hasSelection) {
4561cb0ef41Sopenharmony_ci      this._selectionNode.style.display = 'none';
4571cb0ef41Sopenharmony_ci      return;
4581cb0ef41Sopenharmony_ci    }
4591cb0ef41Sopenharmony_ci    this._selectionNode.style.display = 'inherit';
4601cb0ef41Sopenharmony_ci    const startPosition = this.timeToPosition(this._timeSelection.start);
4611cb0ef41Sopenharmony_ci    const endPosition = this.timeToPosition(this._timeSelection.end);
4621cb0ef41Sopenharmony_ci    this._leftHandleNode.style.left = startPosition + 'px';
4631cb0ef41Sopenharmony_ci    this._rightHandleNode.style.left = endPosition + 'px';
4641cb0ef41Sopenharmony_ci    const delta = endPosition - startPosition;
4651cb0ef41Sopenharmony_ci    this._selectionForegroundNode.style.left = startPosition + 'px';
4661cb0ef41Sopenharmony_ci    this._selectionForegroundNode.style.width = delta + 'px';
4671cb0ef41Sopenharmony_ci    this._selectionBackgroundNode.style.left = startPosition + 'px';
4681cb0ef41Sopenharmony_ci    this._selectionBackgroundNode.style.width = delta + 'px';
4691cb0ef41Sopenharmony_ci  }
4701cb0ef41Sopenharmony_ci
4711cb0ef41Sopenharmony_ci  set timeSelection(selection) {
4721cb0ef41Sopenharmony_ci    this._timeSelection.start = selection.start;
4731cb0ef41Sopenharmony_ci    this._timeSelection.end = selection.end;
4741cb0ef41Sopenharmony_ci  }
4751cb0ef41Sopenharmony_ci
4761cb0ef41Sopenharmony_ci  clearSelection() {
4771cb0ef41Sopenharmony_ci    this._timeline.dispatchEvent(new SelectTimeEvent());
4781cb0ef41Sopenharmony_ci  }
4791cb0ef41Sopenharmony_ci
4801cb0ef41Sopenharmony_ci  timeToPosition(posX) {
4811cb0ef41Sopenharmony_ci    return this._timeline.timeToPosition(posX);
4821cb0ef41Sopenharmony_ci  }
4831cb0ef41Sopenharmony_ci
4841cb0ef41Sopenharmony_ci  positionToTime(posX) {
4851cb0ef41Sopenharmony_ci    return this._timeline.positionToTime(posX);
4861cb0ef41Sopenharmony_ci  }
4871cb0ef41Sopenharmony_ci
4881cb0ef41Sopenharmony_ci  get isSelecting() {
4891cb0ef41Sopenharmony_ci    return this._selectionOriginTime >= 0;
4901cb0ef41Sopenharmony_ci  }
4911cb0ef41Sopenharmony_ci
4921cb0ef41Sopenharmony_ci  get hasSelection() {
4931cb0ef41Sopenharmony_ci    return this._timeSelection.start >= 0 &&
4941cb0ef41Sopenharmony_ci        this._timeSelection.end != Infinity;
4951cb0ef41Sopenharmony_ci  }
4961cb0ef41Sopenharmony_ci
4971cb0ef41Sopenharmony_ci  get _leftHandlePosX() {
4981cb0ef41Sopenharmony_ci    return this._leftHandleNode.getBoundingClientRect().x;
4991cb0ef41Sopenharmony_ci  }
5001cb0ef41Sopenharmony_ci
5011cb0ef41Sopenharmony_ci  get _rightHandlePosX() {
5021cb0ef41Sopenharmony_ci    return this._rightHandleNode.getBoundingClientRect().x;
5031cb0ef41Sopenharmony_ci  }
5041cb0ef41Sopenharmony_ci
5051cb0ef41Sopenharmony_ci  _isOnLeftHandle(posX) {
5061cb0ef41Sopenharmony_ci    return Math.abs(this._leftHandlePosX - posX) <=
5071cb0ef41Sopenharmony_ci        SelectionHandler.SELECTION_OFFSET;
5081cb0ef41Sopenharmony_ci  }
5091cb0ef41Sopenharmony_ci
5101cb0ef41Sopenharmony_ci  _isOnRightHandle(posX) {
5111cb0ef41Sopenharmony_ci    return Math.abs(this._rightHandlePosX - posX) <=
5121cb0ef41Sopenharmony_ci        SelectionHandler.SELECTION_OFFSET;
5131cb0ef41Sopenharmony_ci  }
5141cb0ef41Sopenharmony_ci
5151cb0ef41Sopenharmony_ci  _handleMouseDown(event) {
5161cb0ef41Sopenharmony_ci    if (event.button !== 0) return;
5171cb0ef41Sopenharmony_ci    let xPosition = event.clientX
5181cb0ef41Sopenharmony_ci    // Update origin time in case we click on a handle.
5191cb0ef41Sopenharmony_ci    if (this._isOnLeftHandle(xPosition)) {
5201cb0ef41Sopenharmony_ci      xPosition = this._rightHandlePosX;
5211cb0ef41Sopenharmony_ci    }
5221cb0ef41Sopenharmony_ci    else if (this._isOnRightHandle(xPosition)) {
5231cb0ef41Sopenharmony_ci      xPosition = this._leftHandlePosX;
5241cb0ef41Sopenharmony_ci    }
5251cb0ef41Sopenharmony_ci    this._selectionOriginTime = this.positionToTime(xPosition);
5261cb0ef41Sopenharmony_ci  }
5271cb0ef41Sopenharmony_ci
5281cb0ef41Sopenharmony_ci  _handleMouseMove(event) {
5291cb0ef41Sopenharmony_ci    if (event.button !== 0) return;
5301cb0ef41Sopenharmony_ci    if (!this.isSelecting) return;
5311cb0ef41Sopenharmony_ci    const currentTime = this.positionToTime(event.clientX);
5321cb0ef41Sopenharmony_ci    this._timeline.dispatchEvent(new SynchronizeSelectionEvent(
5331cb0ef41Sopenharmony_ci        Math.min(this._selectionOriginTime, currentTime),
5341cb0ef41Sopenharmony_ci        Math.max(this._selectionOriginTime, currentTime)));
5351cb0ef41Sopenharmony_ci  }
5361cb0ef41Sopenharmony_ci
5371cb0ef41Sopenharmony_ci  _handleMouseUp(event) {
5381cb0ef41Sopenharmony_ci    if (event.button !== 0) return;
5391cb0ef41Sopenharmony_ci    this._selectionOriginTime = -1;
5401cb0ef41Sopenharmony_ci    if (this._timeSelection.start === -1) return;
5411cb0ef41Sopenharmony_ci    const delta = this._timeSelection.end - this._timeSelection.start;
5421cb0ef41Sopenharmony_ci    if (delta <= 1 || isNaN(delta)) return;
5431cb0ef41Sopenharmony_ci    this._timeline.dispatchEvent(new SelectTimeEvent(
5441cb0ef41Sopenharmony_ci        this._timeSelection.start, this._timeSelection.end));
5451cb0ef41Sopenharmony_ci  }
5461cb0ef41Sopenharmony_ci
5471cb0ef41Sopenharmony_ci  _handleDoubleClick(event) {
5481cb0ef41Sopenharmony_ci    if (!this.hasSelection) return;
5491cb0ef41Sopenharmony_ci    // Focus and zoom to the current selection.
5501cb0ef41Sopenharmony_ci    this._timeline.dispatchEvent(new SelectTimeEvent(
5511cb0ef41Sopenharmony_ci        this._timeSelection.start, this._timeSelection.end, true, true));
5521cb0ef41Sopenharmony_ci  }
5531cb0ef41Sopenharmony_ci}
5541cb0ef41Sopenharmony_ci
5551cb0ef41Sopenharmony_ciclass Legend {
5561cb0ef41Sopenharmony_ci  _timeline;
5571cb0ef41Sopenharmony_ci  _lastSelection;
5581cb0ef41Sopenharmony_ci  _typesFilters = new Map();
5591cb0ef41Sopenharmony_ci  _typeClickHandler = this._handleTypeClick.bind(this);
5601cb0ef41Sopenharmony_ci  _filterPredicate = this.filter.bind(this);
5611cb0ef41Sopenharmony_ci  onFilter = () => {};
5621cb0ef41Sopenharmony_ci
5631cb0ef41Sopenharmony_ci  constructor(table) {
5641cb0ef41Sopenharmony_ci    this._table = table;
5651cb0ef41Sopenharmony_ci    this._enableDuration = false;
5661cb0ef41Sopenharmony_ci  }
5671cb0ef41Sopenharmony_ci
5681cb0ef41Sopenharmony_ci  set timeline(timeline) {
5691cb0ef41Sopenharmony_ci    this._timeline = timeline;
5701cb0ef41Sopenharmony_ci    const groups = timeline.getBreakdown();
5711cb0ef41Sopenharmony_ci    this._typesFilters = new Map(groups.map(each => [each.key, true]));
5721cb0ef41Sopenharmony_ci    this._colors =
5731cb0ef41Sopenharmony_ci        new Map(groups.map(each => [each.key, CSSColor.at(each.id)]));
5741cb0ef41Sopenharmony_ci  }
5751cb0ef41Sopenharmony_ci
5761cb0ef41Sopenharmony_ci  get selection() {
5771cb0ef41Sopenharmony_ci    return this._timeline.selectionOrSelf;
5781cb0ef41Sopenharmony_ci  }
5791cb0ef41Sopenharmony_ci
5801cb0ef41Sopenharmony_ci  get filterPredicate() {
5811cb0ef41Sopenharmony_ci    for (let visible of this._typesFilters.values()) {
5821cb0ef41Sopenharmony_ci      if (!visible) return this._filterPredicate;
5831cb0ef41Sopenharmony_ci    }
5841cb0ef41Sopenharmony_ci    return undefined;
5851cb0ef41Sopenharmony_ci  }
5861cb0ef41Sopenharmony_ci
5871cb0ef41Sopenharmony_ci  colorForType(type) {
5881cb0ef41Sopenharmony_ci    let color = this._colors.get(type);
5891cb0ef41Sopenharmony_ci    if (color === undefined) {
5901cb0ef41Sopenharmony_ci      color = CSSColor.at(this._colors.size);
5911cb0ef41Sopenharmony_ci      this._colors.set(type, color);
5921cb0ef41Sopenharmony_ci    }
5931cb0ef41Sopenharmony_ci    return color;
5941cb0ef41Sopenharmony_ci  }
5951cb0ef41Sopenharmony_ci
5961cb0ef41Sopenharmony_ci  filter(logEntry) {
5971cb0ef41Sopenharmony_ci    return this._typesFilters.get(logEntry.type);
5981cb0ef41Sopenharmony_ci  }
5991cb0ef41Sopenharmony_ci
6001cb0ef41Sopenharmony_ci  update(force = false) {
6011cb0ef41Sopenharmony_ci    if (!force && this._lastSelection === this.selection) return;
6021cb0ef41Sopenharmony_ci    this._lastSelection = this.selection;
6031cb0ef41Sopenharmony_ci    const tbody = DOM.tbody();
6041cb0ef41Sopenharmony_ci    const missingTypes = new Set(this._typesFilters.keys());
6051cb0ef41Sopenharmony_ci    this._checkDurationField();
6061cb0ef41Sopenharmony_ci    let selectionDuration = 0;
6071cb0ef41Sopenharmony_ci    const breakdown =
6081cb0ef41Sopenharmony_ci        this.selection.getBreakdown(undefined, this._enableDuration);
6091cb0ef41Sopenharmony_ci    if (this._enableDuration) {
6101cb0ef41Sopenharmony_ci      if (this.selection.cachedDuration === undefined) {
6111cb0ef41Sopenharmony_ci        this.selection.cachedDuration = this._breakdownTotalDuration(breakdown);
6121cb0ef41Sopenharmony_ci      }
6131cb0ef41Sopenharmony_ci      selectionDuration = this.selection.cachedDuration;
6141cb0ef41Sopenharmony_ci    }
6151cb0ef41Sopenharmony_ci    breakdown.forEach(group => {
6161cb0ef41Sopenharmony_ci      tbody.appendChild(this._addTypeRow(group, selectionDuration));
6171cb0ef41Sopenharmony_ci      missingTypes.delete(group.key);
6181cb0ef41Sopenharmony_ci    });
6191cb0ef41Sopenharmony_ci    missingTypes.forEach(key => {
6201cb0ef41Sopenharmony_ci      const emptyGroup = {key, length: 0, duration: 0};
6211cb0ef41Sopenharmony_ci      tbody.appendChild(this._addTypeRow(emptyGroup, selectionDuration));
6221cb0ef41Sopenharmony_ci    });
6231cb0ef41Sopenharmony_ci    if (this._timeline.selection) {
6241cb0ef41Sopenharmony_ci      tbody.appendChild(this._addRow(
6251cb0ef41Sopenharmony_ci          '', 'Selection', this.selection.length, '100%', selectionDuration,
6261cb0ef41Sopenharmony_ci          '100%'));
6271cb0ef41Sopenharmony_ci    }
6281cb0ef41Sopenharmony_ci    // Showing 100% for 'All' and for 'Selection' would be confusing.
6291cb0ef41Sopenharmony_ci    const allPercent = this._timeline.selection ? '' : '100%';
6301cb0ef41Sopenharmony_ci    tbody.appendChild(this._addRow(
6311cb0ef41Sopenharmony_ci        '', 'All', this._timeline.length, allPercent,
6321cb0ef41Sopenharmony_ci        this._timeline.cachedDuration, allPercent));
6331cb0ef41Sopenharmony_ci    this._table.tBodies[0].replaceWith(tbody);
6341cb0ef41Sopenharmony_ci  }
6351cb0ef41Sopenharmony_ci
6361cb0ef41Sopenharmony_ci  _checkDurationField() {
6371cb0ef41Sopenharmony_ci    if (this._enableDuration) return;
6381cb0ef41Sopenharmony_ci    const example = this.selection.at(0);
6391cb0ef41Sopenharmony_ci    if (!example || !('duration' in example)) return;
6401cb0ef41Sopenharmony_ci    this._enableDuration = true;
6411cb0ef41Sopenharmony_ci    this._table.tHead.rows[0].appendChild(DOM.td('Duration'));
6421cb0ef41Sopenharmony_ci  }
6431cb0ef41Sopenharmony_ci
6441cb0ef41Sopenharmony_ci  _addRow(colorNode, type, count, countPercent, duration, durationPercent) {
6451cb0ef41Sopenharmony_ci    const row = DOM.tr();
6461cb0ef41Sopenharmony_ci    const colorCell = row.appendChild(DOM.td(colorNode, 'color'));
6471cb0ef41Sopenharmony_ci    colorCell.setAttribute('title', `Toggle '${type}' entries.`);
6481cb0ef41Sopenharmony_ci    const typeCell = row.appendChild(DOM.td(type, 'text'));
6491cb0ef41Sopenharmony_ci    typeCell.setAttribute('title', type);
6501cb0ef41Sopenharmony_ci    row.appendChild(DOM.td(count.toString()));
6511cb0ef41Sopenharmony_ci    row.appendChild(DOM.td(countPercent));
6521cb0ef41Sopenharmony_ci    if (this._enableDuration) {
6531cb0ef41Sopenharmony_ci      row.appendChild(DOM.td(formatDurationMicros(duration ?? 0)));
6541cb0ef41Sopenharmony_ci      row.appendChild(DOM.td(durationPercent ?? '0%'));
6551cb0ef41Sopenharmony_ci    }
6561cb0ef41Sopenharmony_ci    return row
6571cb0ef41Sopenharmony_ci  }
6581cb0ef41Sopenharmony_ci
6591cb0ef41Sopenharmony_ci  _addTypeRow(group, selectionDuration) {
6601cb0ef41Sopenharmony_ci    const color = this.colorForType(group.key);
6611cb0ef41Sopenharmony_ci    const classes = ['colorbox'];
6621cb0ef41Sopenharmony_ci    if (group.length == 0) classes.push('empty');
6631cb0ef41Sopenharmony_ci    const colorDiv = DOM.div(classes);
6641cb0ef41Sopenharmony_ci    colorDiv.style.borderColor = color;
6651cb0ef41Sopenharmony_ci    if (this._typesFilters.get(group.key)) {
6661cb0ef41Sopenharmony_ci      colorDiv.style.backgroundColor = color;
6671cb0ef41Sopenharmony_ci    } else {
6681cb0ef41Sopenharmony_ci      colorDiv.style.backgroundColor = CSSColor.backgroundImage;
6691cb0ef41Sopenharmony_ci    }
6701cb0ef41Sopenharmony_ci    let duration = 0;
6711cb0ef41Sopenharmony_ci    let durationPercent = '';
6721cb0ef41Sopenharmony_ci    if (this._enableDuration) {
6731cb0ef41Sopenharmony_ci      // group.duration was added in _breakdownTotalDuration.
6741cb0ef41Sopenharmony_ci      duration = group.duration;
6751cb0ef41Sopenharmony_ci      durationPercent = selectionDuration == 0 ?
6761cb0ef41Sopenharmony_ci          '0%' :
6771cb0ef41Sopenharmony_ci          this._formatPercent(duration / selectionDuration);
6781cb0ef41Sopenharmony_ci    }
6791cb0ef41Sopenharmony_ci    const countPercent =
6801cb0ef41Sopenharmony_ci        this._formatPercent(group.length / this.selection.length);
6811cb0ef41Sopenharmony_ci    const row = this._addRow(
6821cb0ef41Sopenharmony_ci        colorDiv, group.key, group.length, countPercent, duration,
6831cb0ef41Sopenharmony_ci        durationPercent);
6841cb0ef41Sopenharmony_ci    row.className = 'clickable';
6851cb0ef41Sopenharmony_ci    row.onclick = this._typeClickHandler;
6861cb0ef41Sopenharmony_ci    row.data = group.key;
6871cb0ef41Sopenharmony_ci    return row;
6881cb0ef41Sopenharmony_ci  }
6891cb0ef41Sopenharmony_ci
6901cb0ef41Sopenharmony_ci  _handleTypeClick(e) {
6911cb0ef41Sopenharmony_ci    const type = e.currentTarget.data;
6921cb0ef41Sopenharmony_ci    this._typesFilters.set(type, !this._typesFilters.get(type));
6931cb0ef41Sopenharmony_ci    this.onFilter(type);
6941cb0ef41Sopenharmony_ci  }
6951cb0ef41Sopenharmony_ci
6961cb0ef41Sopenharmony_ci  _breakdownTotalDuration(breakdown) {
6971cb0ef41Sopenharmony_ci    let duration = 0;
6981cb0ef41Sopenharmony_ci    breakdown.forEach(group => {
6991cb0ef41Sopenharmony_ci      group.duration = this._groupDuration(group);
7001cb0ef41Sopenharmony_ci      duration += group.duration;
7011cb0ef41Sopenharmony_ci    })
7021cb0ef41Sopenharmony_ci    return duration;
7031cb0ef41Sopenharmony_ci  }
7041cb0ef41Sopenharmony_ci
7051cb0ef41Sopenharmony_ci  _groupDuration(group) {
7061cb0ef41Sopenharmony_ci    let duration = 0;
7071cb0ef41Sopenharmony_ci    const entries = group.entries;
7081cb0ef41Sopenharmony_ci    for (let i = 0; i < entries.length; i++) {
7091cb0ef41Sopenharmony_ci      duration += entries[i].duration;
7101cb0ef41Sopenharmony_ci    }
7111cb0ef41Sopenharmony_ci    return duration;
7121cb0ef41Sopenharmony_ci  }
7131cb0ef41Sopenharmony_ci
7141cb0ef41Sopenharmony_ci  _formatPercent(ratio) {
7151cb0ef41Sopenharmony_ci    return `${(ratio * 100).toFixed(1)}%`;
7161cb0ef41Sopenharmony_ci  }
7171cb0ef41Sopenharmony_ci}
718