// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/* eslint-disable @devtools/no-imperative-dom-api */

import * as i18n from '../../core/i18n/i18n.js';
import * as Trace from '../../models/trace/trace.js';
import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

const UIStrings = {
  /**
   * @description Short for Network. Label for the network requests section of the Performance panel.
   */
  net: 'NET',
  /**
   * @description Text in Timeline Event Overview of the Performance panel
   */
  cpu: 'CPU',
  /**
   * @description Text in Timeline Event Overview of the Performance panel
   */
  heap: 'HEAP',
  /**
   * @description Heap size label text content in Timeline Event Overview of the Performance panel
   * @example {10 MB} PH1
   * @example {30 MB} PH2
   */
  sSDash: '{PH1} – {PH2}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineEventOverview.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export abstract class TimelineEventOverview extends PerfUI.TimelineOverviewPane.TimelineOverviewBase {
  constructor(id: string, title: string|null) {
    super();
    this.element.id = 'timeline-overview-' + id;
    this.element.classList.add('overview-strip');
    if (title) {
      this.element.createChild('div', 'timeline-overview-strip-title').textContent = title;
    }
  }

  renderBar(begin: number, end: number, position: number, height: number, color: string): void {
    const x = begin;
    const width = end - begin;
    const ctx = this.context();
    ctx.fillStyle = color;
    ctx.fillRect(x, position, width, height);
  }
}

export class TimelineEventOverviewNetwork extends TimelineEventOverview {
  #parsedTrace: Trace.TraceModel.ParsedTrace;
  constructor(parsedTrace: Trace.TraceModel.ParsedTrace) {
    super('network', i18nString(UIStrings.net));
    this.#parsedTrace = parsedTrace;
  }

  override update(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void {
    this.resetCanvas();
    this.#renderWithParsedTrace(start, end);
  }

  #renderWithParsedTrace(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void {
    if (!this.#parsedTrace) {
      return;
    }

    // Because the UI is in milliseconds, we work with milliseconds through
    // this function to get the right scale and sizing
    const traceBoundsMilli = (start && end) ?
        {
          min: start,
          max: end,
          range: end - start,
        } :
        Trace.Helpers.Timing.traceWindowMilliSeconds(this.#parsedTrace.data.Meta.traceBounds);

    // We draw two paths, so each can take up half the height
    const pathHeight = this.height() / 2;

    const canvasWidth = this.width();
    const scale = canvasWidth / traceBoundsMilli.range;

    // We draw network requests in two chunks:
    // Requests with a priority of Medium or higher go onto the first path
    // Other requests go onto the second path.
    const highPath = new Path2D();
    const lowPath = new Path2D();

    for (const request of this.#parsedTrace.data.NetworkRequests.byTime) {
      const path = Trace.Helpers.Network.isSyntheticNetworkRequestHighPriority(request) ? highPath : lowPath;
      const {startTime, endTime} = Trace.Helpers.Timing.eventTimingsMilliSeconds(request);
      const rectStart = Math.max(Math.floor((startTime - traceBoundsMilli.min) * scale), 0);
      const rectEnd = Math.min(Math.ceil((endTime - traceBoundsMilli.min) * scale + 1), canvasWidth);

      path.rect(rectStart, 0, rectEnd - rectStart, pathHeight - 1);
    }

    const ctx = this.context();
    ctx.save();
    // Draw the high path onto the canvas.
    ctx.fillStyle = 'hsl(214, 60%, 60%)';
    ctx.fill(highPath);
    // Now jump down by the height of the high path, and then draw the low path.
    ctx.translate(0, pathHeight);
    ctx.fillStyle = 'hsl(214, 80%, 80%)';
    ctx.fill(lowPath);
    ctx.restore();
  }
}

const categoryToIndex = new WeakMap<Trace.Styles.TimelineCategory, number>();

export class TimelineEventOverviewCPUActivity extends TimelineEventOverview {
  private backgroundCanvas: HTMLCanvasElement;
  #parsedTrace: Trace.TraceModel.ParsedTrace;
  #drawn = false;
  #start: Trace.Types.Timing.Milli;
  #end: Trace.Types.Timing.Milli;

  constructor(parsedTrace: Trace.TraceModel.ParsedTrace) {
    // During the sync tracks migration this component can use either legacy
    // Performance Model data or the new engine's data. Once the migration is
    // complete this will be updated to only use the new engine and mentions of
    // the PerformanceModel will be removed.
    super('cpu-activity', i18nString(UIStrings.cpu));
    this.#parsedTrace = parsedTrace;
    this.backgroundCanvas = this.element.createChild('canvas', 'fill background');
    this.#start = Trace.Helpers.Timing.traceWindowMilliSeconds(parsedTrace.data.Meta.traceBounds).min;
    this.#end = Trace.Helpers.Timing.traceWindowMilliSeconds(parsedTrace.data.Meta.traceBounds).max;
  }

  #entryCategory(entry: Trace.Types.Events.Event): Trace.Styles.EventCategory|undefined {
    // Special case: in CPU Profiles we get a lot of ProfileCalls that
    // represent Idle time. We typically represent ProfileCalls in the
    // Scripting Category, but if they represent idle time, we do not want
    // that.
    if (Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === '(idle)') {
      return Trace.Styles.EventCategory.IDLE;
    }
    if (Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === '(program)') {
      return Trace.Styles.EventCategory.OTHER;
    }
    const eventStyle = Trace.Styles.getEventStyle(entry.name as Trace.Types.Events.Name)?.category ||
        Trace.Styles.getCategoryStyles().other;
    const categoryName = eventStyle.name;
    return categoryName;
  }

  override resetCanvas(): void {
    super.resetCanvas();
    this.#drawn = false;
    this.backgroundCanvas.width = this.element.clientWidth * window.devicePixelRatio;
    this.backgroundCanvas.height = this.element.clientHeight * window.devicePixelRatio;
  }

  #draw(parsedTrace: Trace.TraceModel.ParsedTrace): void {
    const quantSizePx = 4 * window.devicePixelRatio;
    const width = this.width();
    const height = this.height();
    const baseLine = height;
    const timeRange = this.#end - this.#start;
    const scale = width / timeRange;
    const quantTime = quantSizePx / scale;
    const categories = Trace.Styles.getCategoryStyles();
    const categoryOrder = Trace.Styles.getTimelineMainEventCategories();
    const otherIndex = categoryOrder.indexOf(Trace.Styles.EventCategory.OTHER);
    const idleIndex = 0;
    console.assert(idleIndex === categoryOrder.indexOf(Trace.Styles.EventCategory.IDLE));
    for (let i = 0; i < categoryOrder.length; ++i) {
      categoryToIndex.set(categories[categoryOrder[i]], i);
    }

    const drawThreadEntries =
        (context: CanvasRenderingContext2D, threadData: Trace.Handlers.Threads.ThreadData): void => {
          const quantizer = new Quantizer(this.#start, quantTime, drawSample);
          let x = 0;
          const categoryIndexStack: number[] = [];
          const paths: Path2D[] = [];
          const lastY: number[] = [];
          for (let i = 0; i < categoryOrder.length; ++i) {
            paths[i] = new Path2D();
            paths[i].moveTo(0, height);
            lastY[i] = height;
          }

          function drawSample(counters: number[]): void {
            let y = baseLine;
            for (let i = idleIndex + 1; i < categoryOrder.length; ++i) {
              const h = (counters[i] || 0) / quantTime * height;
              y -= h;
              paths[i].bezierCurveTo(x, lastY[i], x, y, x + quantSizePx / 2, y);
              lastY[i] = y;
            }
            x += quantSizePx;
          }

          const onEntryStart = (entry: Trace.Types.Events.Event): void => {
            const category = this.#entryCategory(entry);
            if (!category || category === 'idle') {
              // Idle event won't show in CPU activity, so just skip them.
              return;
            }
            const startTimeMilli = Trace.Helpers.Timing.microToMilli(entry.ts);
            const index = categoryIndexStack.length ? categoryIndexStack[categoryIndexStack.length - 1] : idleIndex;
            quantizer.appendInterval(startTimeMilli, index);
            const categoryIndex = categoryOrder.indexOf(category);
            categoryIndexStack.push(categoryIndex || otherIndex);
          };

          function onEntryEnd(entry: Trace.Types.Events.Event): void {
            const endTimeMilli = Trace.Helpers.Timing.microToMilli(entry.ts) +
                Trace.Helpers.Timing.microToMilli(Trace.Types.Timing.Micro(entry.dur || 0));
            const lastCategoryIndex = categoryIndexStack.pop();
            if (endTimeMilli !== undefined && lastCategoryIndex) {
              quantizer.appendInterval(endTimeMilli, lastCategoryIndex);
            }
          }
          const startMicro = Trace.Helpers.Timing.milliToMicro(this.#start);
          const endMicro = Trace.Helpers.Timing.milliToMicro(this.#end);
          const bounds = {
            min: startMicro,
            max: endMicro,
            range: Trace.Types.Timing.Micro(endMicro - startMicro),
          };

          // Filter out tiny events - they don't make a visual impact to the
          // canvas as they are so small, but they do impact the time it takes
          // to walk the tree and render the events.
          // However, if the entire range we are showing is 200ms or less, then show all events.
          const minDuration = Trace.Types.Timing.Micro(
              bounds.range > 200_000 ? 16_000 : 0,
          );
          Trace.Helpers.TreeHelpers.walkEntireTree(
              threadData.entryToNode, threadData.tree, onEntryStart, onEntryEnd, bounds, minDuration);
          quantizer.appendInterval(this.#start + timeRange + quantTime, idleIndex);  // Kick drawing the last bucket.
          for (let i = categoryOrder.length - 1; i > 0; --i) {
            paths[i].lineTo(width, height);
            const computedColorValue =
                ThemeSupport.ThemeSupport.instance().getComputedValue(categories[categoryOrder[i]].cssVariable);
            context.fillStyle = computedColorValue;
            context.fill(paths[i]);
            context.strokeStyle = 'white';
            context.lineWidth = 1;
            context.stroke(paths[i]);
          }
        };
    const backgroundContext = (this.backgroundCanvas.getContext('2d'));
    if (!backgroundContext) {
      throw new Error('Could not find 2d canvas');
    }

    const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
    const mainThreadContext = this.context();
    for (const thread of threads) {
      // We treat CPU_PROFILE as main thread because in a CPU Profile trace there is only ever one thread.
      const isMainThread = thread.type === Trace.Handlers.Threads.ThreadType.MAIN_THREAD ||
          thread.type === Trace.Handlers.Threads.ThreadType.CPU_PROFILE;
      if (isMainThread) {
        drawThreadEntries(mainThreadContext, thread);
      } else {
        drawThreadEntries(backgroundContext, thread);
      }
    }

    function applyPattern(ctx: CanvasRenderingContext2D): void {
      const step = 4 * window.devicePixelRatio;
      ctx.save();
      ctx.lineWidth = step / Math.sqrt(8);
      for (let x = 0.5; x < width + height; x += step) {
        ctx.moveTo(x, 0);
        ctx.lineTo(x - height, height);
      }
      ctx.globalCompositeOperation = 'destination-out';
      ctx.stroke();
      ctx.restore();
    }

    applyPattern(backgroundContext);
  }

  override update(): void {
    const traceBoundsState = TraceBounds.TraceBounds.BoundsManager.instance().state();
    const bounds = traceBoundsState?.milli.minimapTraceBounds;
    if (!bounds) {
      return;
    }
    if (bounds.min === this.#start && bounds.max === this.#end && this.#drawn) {
      return;
    }
    this.#start = bounds.min;
    this.#end = bounds.max;
    // Order matters here, resetCanvas will set this.#drawn to false.
    this.resetCanvas();
    this.#drawn = true;
    this.#draw(this.#parsedTrace);
  }
}

export class TimelineEventOverviewResponsiveness extends TimelineEventOverview {
  #parsedTrace: Trace.TraceModel.ParsedTrace;
  constructor(parsedTrace: Trace.TraceModel.ParsedTrace) {
    super('responsiveness', null);
    this.#parsedTrace = parsedTrace;
  }

  #gatherEventsWithRelevantWarnings(): Set<Trace.Types.Events.Event> {
    const {topLevelRendererIds} = this.#parsedTrace.data.Meta;

    // All the warnings that we care about regarding responsiveness and want to represent on the overview.
    const warningsForResponsiveness = new Set<Trace.Handlers.ModelHandlers.Warnings.Warning>([
      'LONG_TASK',
      'FORCED_REFLOW',
      'IDLE_CALLBACK_OVER_TIME',
    ]);

    const allWarningEvents = new Set<Trace.Types.Events.Event>();
    for (const warning of warningsForResponsiveness) {
      const eventsForWarning = this.#parsedTrace.data.Warnings.perWarning.get(warning);
      if (!eventsForWarning) {
        continue;
      }

      for (const event of eventsForWarning) {
        // Only keep events whose PID is a top level renderer, which means it
        // was on the main thread. This avoids showing issues from iframes or
        // other sub-frames in the minimap overview.
        if (topLevelRendererIds.has(event.pid)) {
          allWarningEvents.add(event);
        }
      }
    }
    return allWarningEvents;
  }

  override update(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void {
    this.resetCanvas();

    const height = this.height();
    const visibleTimeWindow = !(start && end) ? this.#parsedTrace.data.Meta.traceBounds : {
      min: Trace.Helpers.Timing.milliToMicro(start),
      max: Trace.Helpers.Timing.milliToMicro(end),
      range: Trace.Helpers.Timing.milliToMicro(Trace.Types.Timing.Milli(end - start)),
    };
    const timeSpan = visibleTimeWindow.range;
    const scale = this.width() / timeSpan;
    const ctx = this.context();
    const fillPath = new Path2D();
    const markersPath = new Path2D();

    const eventsWithWarning = this.#gatherEventsWithRelevantWarnings();
    for (const event of eventsWithWarning) {
      paintWarningDecoration(event);
    }

    ctx.fillStyle = 'hsl(0, 80%, 90%)';
    ctx.strokeStyle = 'red';
    ctx.lineWidth = 2 * window.devicePixelRatio;
    ctx.fill(fillPath);
    ctx.stroke(markersPath);

    function paintWarningDecoration(event: Trace.Types.Events.Event): void {
      const {startTime, duration} = Trace.Helpers.Timing.eventTimingsMicroSeconds(event);
      const x = Math.round(scale * (startTime - visibleTimeWindow.min));
      const width = Math.round(scale * duration);
      fillPath.rect(x, 0, width, height);
      markersPath.moveTo(x + width, 0);
      markersPath.lineTo(x + width, height);
    }
  }
}

export class TimelineFilmStripOverview extends TimelineEventOverview {
  private frameToImagePromise: Map<Trace.Extras.FilmStrip.Frame, Promise<HTMLImageElement>>;
  private lastFrame: Trace.Extras.FilmStrip.Frame|null = null;
  private lastElement: Element|null;
  private drawGeneration?: symbol;
  private emptyImage?: HTMLImageElement;
  #filmStrip: Trace.Extras.FilmStrip.Data|null = null;

  constructor(filmStrip: Trace.Extras.FilmStrip.Data) {
    super('filmstrip', null);
    this.element.setAttribute('jslog', `${VisualLogging.section('film-strip')}`);
    this.frameToImagePromise = new Map();
    this.#filmStrip = filmStrip;
    this.lastFrame = null;
    this.lastElement = null;
    this.reset();
  }

  override update(customStartTime?: Trace.Types.Timing.Milli, customEndTime?: Trace.Types.Timing.Milli): void {
    this.resetCanvas();
    const frames = this.#filmStrip ? this.#filmStrip.frames : [];
    if (!frames.length) {
      return;
    }

    if (this.height() === 0) {
      // Height of 0 causes the maths below to get off and generate very large
      // negative numbers that cause an extremely long loop when attempting to
      // draw images by frame. Rather than that, let's warn and exist early.
      console.warn('TimelineFilmStrip could not be drawn as its canvas height is 0');
      return;
    }

    const drawGeneration = Symbol('drawGeneration');
    this.drawGeneration = drawGeneration;
    void this.imageByFrame(frames[0]).then(image => {
      if (this.drawGeneration !== drawGeneration) {
        return;
      }
      if (!image?.naturalWidth || !image.naturalHeight) {
        return;
      }
      const imageHeight = this.height() - 2 * TimelineFilmStripOverview.Padding;
      const imageWidth = Math.ceil(imageHeight * image.naturalWidth / image.naturalHeight);
      const popoverScale = Math.min(200 / image.naturalWidth, 1);
      this.emptyImage = new Image(image.naturalWidth * popoverScale, image.naturalHeight * popoverScale);
      this.drawFrames(imageWidth, imageHeight, customStartTime, customEndTime);
    });
  }

  private async imageByFrame(frame: Trace.Extras.FilmStrip.Frame): Promise<HTMLImageElement|null> {
    let imagePromise: Promise<HTMLImageElement|null>|undefined = this.frameToImagePromise.get(frame);
    if (!imagePromise) {
      // TODO(paulirish): Adopt Util.ImageCache
      const uri = Trace.Handlers.ModelHandlers.Screenshots.screenshotImageDataUri(frame.screenshotEvent);
      imagePromise = UI.UIUtils.loadImage(uri);
      this.frameToImagePromise.set(frame, (imagePromise as Promise<HTMLImageElement>));
    }
    return await imagePromise;
  }

  private drawFrames(
      imageWidth: number, imageHeight: number, customStartTime?: Trace.Types.Timing.Milli,
      customEndTime?: Trace.Types.Timing.Milli): void {
    if (!imageWidth) {
      return;
    }
    if (!this.#filmStrip || this.#filmStrip.frames.length < 1) {
      return;
    }
    const padding = TimelineFilmStripOverview.Padding;
    const width = this.width();

    const zeroTime = customStartTime ?? Trace.Helpers.Timing.microToMilli(this.#filmStrip.zeroTime);
    const spanTime =
        customEndTime ? customEndTime - zeroTime : Trace.Helpers.Timing.microToMilli(this.#filmStrip.spanTime);
    const scale = spanTime / width;
    const context = this.context();
    const drawGeneration = this.drawGeneration;

    context.beginPath();
    for (let x = padding; x < width; x += imageWidth + 2 * padding) {
      const time = Trace.Types.Timing.Milli(zeroTime + (x + imageWidth / 2) * scale);
      const timeMicroSeconds = Trace.Helpers.Timing.milliToMicro(time);
      const frame = Trace.Extras.FilmStrip.frameClosestToTimestamp(this.#filmStrip, timeMicroSeconds);
      if (!frame) {
        continue;
      }
      context.rect(x - 0.5, 0.5, imageWidth + 1, imageHeight + 1);
      void this.imageByFrame(frame).then(drawFrameImage.bind(this, x));
    }
    context.strokeStyle = '#ddd';
    context.stroke();

    function drawFrameImage(this: TimelineFilmStripOverview, x: number, image: HTMLImageElement|null): void {
      // Ignore draws deferred from a previous update call.
      if (this.drawGeneration !== drawGeneration || !image) {
        return;
      }
      context.drawImage(image, x, 1, imageWidth, imageHeight);
    }
  }

  override async overviewInfoPromise(x: number): Promise<Element|null> {
    if (!this.#filmStrip || this.#filmStrip.frames.length === 0) {
      return null;
    }

    const calculator = this.calculator();
    if (!calculator) {
      return null;
    }
    const timeMilliSeconds = calculator.positionToTime(x);
    const timeMicroSeconds = Trace.Helpers.Timing.milliToMicro(timeMilliSeconds);
    const frame = Trace.Extras.FilmStrip.frameClosestToTimestamp(this.#filmStrip, timeMicroSeconds);
    if (frame === this.lastFrame) {
      return this.lastElement;
    }
    const imagePromise = frame ? this.imageByFrame(frame) : Promise.resolve(this.emptyImage);
    const image = await imagePromise;
    const element = document.createElement('div');
    element.classList.add('frame');
    if (image) {
      element.createChild('div', 'thumbnail').appendChild(image);
    }
    this.lastFrame = frame;
    this.lastElement = element;
    return element;
  }

  override reset(): void {
    this.lastFrame = null;
    this.lastElement = null;
    this.frameToImagePromise = new Map();
  }

  // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
  // eslint-disable-next-line @typescript-eslint/naming-convention
  static readonly Padding = 2;
}

export class TimelineEventOverviewMemory extends TimelineEventOverview {
  private heapSizeLabel: HTMLElement;
  #parsedTrace: Trace.TraceModel.ParsedTrace;

  constructor(parsedTrace: Trace.TraceModel.ParsedTrace) {
    super('memory', i18nString(UIStrings.heap));
    this.heapSizeLabel = this.element.createChild('div', 'memory-graph-label');
    this.#parsedTrace = parsedTrace;
  }

  resetHeapSizeLabels(): void {
    this.heapSizeLabel.textContent = '';
  }

  override update(start?: Trace.Types.Timing.Milli, end?: Trace.Types.Timing.Milli): void {
    this.resetCanvas();
    const ratio = window.devicePixelRatio;

    if (this.#parsedTrace.data.Memory.updateCountersByProcess.size === 0) {
      this.resetHeapSizeLabels();
      return;
    }

    const mainRendererIds = Array.from(this.#parsedTrace.data.Meta.topLevelRendererIds);
    const counterEventsPerTrack =
        mainRendererIds.map(pid => this.#parsedTrace.data.Memory.updateCountersByProcess.get(pid) || [])
            .filter(eventsPerRenderer => eventsPerRenderer.length > 0);

    const lowerOffset = 3 * ratio;
    let maxUsedHeapSize = 0;
    let minUsedHeapSize = 100000000000;

    const boundsMs = (start && end) ?
        {
          min: start,
          max: end,
          range: end - start,
        } :
        Trace.Helpers.Timing.traceWindowMilliSeconds(this.#parsedTrace.data.Meta.traceBounds);
    const minTime = boundsMs.min;
    const maxTime = boundsMs.max;

    function calculateMinMaxSizes(event: Trace.Types.Events.UpdateCounters): void {
      const counters = event.args.data;
      if (!counters || !counters.jsHeapSizeUsed) {
        return;
      }
      maxUsedHeapSize = Math.max(maxUsedHeapSize, counters.jsHeapSizeUsed);
      minUsedHeapSize = Math.min(minUsedHeapSize, counters.jsHeapSizeUsed);
    }

    for (let i = 0; i < counterEventsPerTrack.length; i++) {
      counterEventsPerTrack[i].forEach(calculateMinMaxSizes);
    }

    minUsedHeapSize = Math.min(minUsedHeapSize, maxUsedHeapSize);

    const lineWidth = 1;
    const width = this.width();
    const height = this.height() - lowerOffset;
    const xFactor = width / (maxTime - minTime);
    const yFactor = (height - lineWidth) / Math.max(maxUsedHeapSize - minUsedHeapSize, 1);

    const histogram = new Array(width);

    function buildHistogram(event: Trace.Types.Events.UpdateCounters): void {
      const counters = event.args.data;
      if (!counters || !counters.jsHeapSizeUsed) {
        return;
      }
      const {startTime} = Trace.Helpers.Timing.eventTimingsMilliSeconds(event);
      const x = Math.round((startTime - minTime) * xFactor);
      const y = Math.round((counters.jsHeapSizeUsed - minUsedHeapSize) * yFactor);
      histogram[x] = Math.max(histogram[x] || 0, y);
    }
    for (let i = 0; i < counterEventsPerTrack.length; i++) {
      counterEventsPerTrack[i].forEach(buildHistogram);
    }

    const ctx = this.context();
    const heightBeyondView = height + lowerOffset + lineWidth;

    ctx.translate(0.5, 0.5);
    ctx.beginPath();
    ctx.moveTo(-lineWidth, heightBeyondView);
    let y = 0;
    let isFirstPoint = true;
    let lastX = 0;
    for (let x = 0; x < histogram.length; x++) {
      if (typeof histogram[x] === 'undefined') {
        continue;
      }
      if (isFirstPoint) {
        isFirstPoint = false;
        y = histogram[x];
        ctx.lineTo(-lineWidth, height - y);
      }
      const nextY = histogram[x];
      if (Math.abs(nextY - y) > 2 && Math.abs(x - lastX) > 1) {
        ctx.lineTo(x, height - y);
      }
      y = nextY;
      ctx.lineTo(x, height - y);
      lastX = x;
    }
    ctx.lineTo(width + lineWidth, height - y);
    ctx.lineTo(width + lineWidth, heightBeyondView);
    ctx.closePath();

    ctx.fillStyle = 'hsla(220, 90%, 70%, 0.2)';
    ctx.fill();
    ctx.lineWidth = lineWidth;
    ctx.strokeStyle = 'hsl(220, 90%, 70%)';
    ctx.stroke();

    this.heapSizeLabel.textContent = i18nString(UIStrings.sSDash, {
      PH1: i18n.ByteUtilities.bytesToString(minUsedHeapSize),
      PH2: i18n.ByteUtilities.bytesToString(maxUsedHeapSize),
    });
  }
}

export class Quantizer {
  private lastTime: number;
  private quantDuration: number;
  private readonly callback: (arg0: number[]) => void;
  private counters: number[];
  private remainder: number;
  constructor(startTime: number, quantDuration: number, callback: (arg0: number[]) => void) {
    this.lastTime = startTime;
    this.quantDuration = quantDuration;
    this.callback = callback;
    this.counters = [];
    this.remainder = quantDuration;
  }

  appendInterval(time: number, group: number): void {
    let interval = time - this.lastTime;
    if (interval <= this.remainder) {
      this.counters[group] = (this.counters[group] || 0) + interval;
      this.remainder -= interval;
      this.lastTime = time;
      return;
    }
    this.counters[group] = (this.counters[group] || 0) + this.remainder;
    this.callback(this.counters);
    interval -= this.remainder;
    while (interval >= this.quantDuration) {
      const counters = [];
      counters[group] = this.quantDuration;
      this.callback(counters);
      interval -= this.quantDuration;
    }
    this.counters = [];
    this.counters[group] = interval;
    this.lastTime = time;
    this.remainder = this.quantDuration - interval;
  }
}
